rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  63. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  64. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  65. data/lib/rodauth/oauth/refinements.rb +0 -48
  66. data/templates/jwt_public_key_field.str +0 -4
  67. data/templates/oauth_application_oauth_tokens.str +0 -52
  68. data/templates/oauth_tokens.str +0 -50
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+
3
5
  module Rodauth
4
6
  Feature.define(:oidc_dynamic_client_registration, :OidcDynamicClientRegistration) do
5
7
  depends :oauth_dynamic_client_registration, :oidc
@@ -8,10 +10,6 @@ module Rodauth
8
10
 
9
11
  private
10
12
 
11
- def registration_metadata
12
- openid_configuration_body
13
- end
14
-
15
13
  def validate_client_registration_params
16
14
  super
17
15
 
@@ -43,11 +41,57 @@ module Rodauth
43
41
  else
44
42
  register_throw_json_response_error("invalid_client_metadata", register_invalid_application_type_message(type))
45
43
  end
46
- elsif (value = @oauth_application_params[oauth_applications_subject_type_column])
44
+ end
45
+
46
+ if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column])
47
+ uri = URI(value)
48
+
49
+ unless uri.scheme == "https" || uri.host == "localhost"
50
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri))
51
+ end
52
+ end
53
+
54
+ if features.include?(:oauth_jwt_secured_authorization_request)
55
+ if (value = @oauth_application_params[oauth_applications_request_uris_column])
56
+ if value.is_a?(Array)
57
+ @oauth_application_params[oauth_applications_request_uris_column] = value.each do |req_uri|
58
+ unless check_valid_uri?(req_uri)
59
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(req_uri))
60
+ end
61
+ end.join(" ")
62
+ else
63
+ register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
64
+ end
65
+ elsif oauth_require_request_uri_registration
66
+ register_throw_json_response_error("invalid_client_metadata", register_required_param_message("request_uris"))
67
+ end
68
+ end
69
+
70
+ if (value = @oauth_application_params[oauth_applications_subject_type_column])
47
71
  unless %w[pairwise public].include?(value)
48
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("subject_type"))
72
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message("subject_type", value))
49
73
  end
50
- elsif (value = @oauth_application_params[oauth_applications_id_token_signed_response_alg_column])
74
+
75
+ if value == "pairwise"
76
+ sector_identifier_uri = @oauth_application_params[oauth_applications_sector_identifier_uri_column]
77
+
78
+ if sector_identifier_uri
79
+ response = http_request(sector_identifier_uri)
80
+ unless response.code.to_i == 200
81
+ register_throw_json_response_error("invalid_client_metadata",
82
+ register_invalid_param_message("sector_identifier_uri"))
83
+ end
84
+ uris = JSON.parse(response.body)
85
+
86
+ if uris != @oauth_application_params[oauth_applications_redirect_uri_column].split(" ")
87
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("sector_identifier_uri"))
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+
94
+ if (value = @oauth_application_params[oauth_applications_id_token_signed_response_alg_column])
51
95
  if value == "none"
52
96
  # The value none MUST NOT be used as the ID Token alg value unless the Client uses only Response Types
53
97
  # that return no ID Token from the Authorization Endpoint
@@ -55,42 +99,77 @@ module Rodauth
55
99
  if response_types && response_types.include?("id_token")
56
100
  register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
57
101
  end
58
- elsif !oauth_jwt_algorithms_supported.include?(value)
59
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
60
- end
61
- elsif (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column])
62
- unless oauth_jwt_jwe_algorithms_supported.include?(value)
63
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_alg"))
64
- end
65
- elsif (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column])
66
- unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
67
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_enc"))
68
- end
69
- elsif (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column])
70
- unless oauth_jwt_algorithms_supported.include?(value)
71
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_signed_response_alg"))
72
- end
73
- elsif (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column])
74
- unless oauth_jwt_jwe_algorithms_supported.include?(value)
75
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_alg"))
102
+ elsif !oauth_jwt_jws_algorithms_supported.include?(value)
103
+ register_throw_json_response_error("invalid_client_metadata",
104
+ register_invalid_client_metadata_message("id_token_signed_response_alg", value))
76
105
  end
77
- elsif (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column])
78
- unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
79
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_enc"))
106
+ end
107
+
108
+ if features.include?(:oauth_jwt_secured_authorization_request)
109
+ if defined?(oauth_applications_request_object_signing_alg_column) &&
110
+ (value = @oauth_application_params[oauth_applications_request_object_signing_alg_column]) &&
111
+ !oauth_jwt_jws_algorithms_supported.include?(value) && !(value == "none" && oauth_request_object_signing_alg_allow_none)
112
+ register_throw_json_response_error("invalid_client_metadata",
113
+ register_invalid_client_metadata_message("request_object_signing_alg", value))
80
114
  end
81
- elsif (value = @oauth_application_params[oauth_applications_request_object_signing_alg_column])
82
- unless oauth_jwt_algorithms_supported.include?(value)
83
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_signing_alg"))
115
+
116
+ if defined?(oauth_applications_request_object_encryption_alg_column) &&
117
+ (value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column]) &&
118
+ !oauth_jwt_jwe_algorithms_supported.include?(value)
119
+ register_throw_json_response_error("invalid_client_metadata",
120
+ register_invalid_client_metadata_message("request_object_encryption_alg", value))
84
121
  end
85
- elsif (value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column])
86
- unless oauth_jwt_jwe_algorithms_supported.include?(value)
87
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_alg"))
122
+
123
+ if defined?(oauth_applications_request_object_encryption_enc_column) &&
124
+ (value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column]) &&
125
+ !oauth_jwt_jwe_encryption_methods_supported.include?(value)
126
+ register_throw_json_response_error("invalid_client_metadata",
127
+ register_invalid_client_metadata_message("request_object_encryption_enc", value))
88
128
  end
89
- elsif (value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column])
90
- unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
91
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_enc"))
129
+ end
130
+
131
+ if features.include?(:oidc_rp_initiated_logout) && (defined?(oauth_applications_post_logout_redirect_uris_column) &&
132
+ (value = @oauth_application_params[oauth_applications_post_logout_redirect_uris_column]))
133
+ if value.is_a?(Array)
134
+ @oauth_application_params[oauth_applications_post_logout_redirect_uris_column] = value.each do |redirect_uri|
135
+ unless check_valid_uri?(redirect_uri)
136
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(redirect_uri))
137
+ end
138
+ end.join(" ")
139
+ else
140
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value))
92
141
  end
93
142
  end
143
+
144
+ if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
145
+ !oauth_jwt_jwe_algorithms_supported.include?(value)
146
+ register_throw_json_response_error("invalid_client_metadata",
147
+ register_invalid_client_metadata_message("id_token_encrypted_response_alg", value))
148
+ end
149
+
150
+ if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column]) &&
151
+ !oauth_jwt_jwe_encryption_methods_supported.include?(value)
152
+ register_throw_json_response_error("invalid_client_metadata",
153
+ register_invalid_client_metadata_message("id_token_encrypted_response_enc", value))
154
+ end
155
+
156
+ if (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column]) &&
157
+ !oauth_jwt_jws_algorithms_supported.include?(value)
158
+ register_throw_json_response_error("invalid_client_metadata",
159
+ register_invalid_client_metadata_message("userinfo_signed_response_alg", value))
160
+ end
161
+
162
+ if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column]) &&
163
+ !oauth_jwt_jwe_algorithms_supported.include?(value)
164
+ register_throw_json_response_error("invalid_client_metadata",
165
+ register_invalid_client_metadata_message("userinfo_encrypted_response_alg", value))
166
+ end
167
+
168
+ if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column]) &&
169
+ !oauth_jwt_jwe_encryption_methods_supported.include?(value)
170
+ register_throw_json_response_error("invalid_client_metadata",
171
+ register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
172
+ end
94
173
  end
95
174
 
96
175
  def validate_client_registration_response_type(response_type, grant_types)
@@ -114,27 +193,24 @@ module Rodauth
114
193
  return_params["application_type"] = "web"
115
194
  "web"
116
195
  end
117
- create_params[oauth_applications_id_token_signed_response_alg_column] ||= begin
118
- return_params["id_token_signed_response_alg"] = oauth_jwt_algorithm
119
- oauth_jwt_algorithm
120
- end
196
+ create_params[oauth_applications_id_token_signed_response_alg_column] ||= return_params["id_token_signed_response_alg"] =
197
+ oauth_jwt_keys.keys.first
198
+
121
199
  if create_params.key?(oauth_applications_id_token_encrypted_response_alg_column)
122
- create_params[oauth_applications_id_token_encrypted_response_enc_column] ||= begin
123
- return_params["id_token_encrypted_response_enc"] = "A128CBC-HS256"
200
+ create_params[oauth_applications_id_token_encrypted_response_enc_column] ||= return_params["id_token_encrypted_response_enc"] =
124
201
  "A128CBC-HS256"
125
- end
202
+
126
203
  end
127
204
  if create_params.key?(oauth_applications_userinfo_encrypted_response_alg_column)
128
- create_params[oauth_applications_userinfo_encrypted_response_enc_column] ||= begin
129
- return_params["userinfo_encrypted_response_enc"] = "A128CBC-HS256"
205
+ create_params[oauth_applications_userinfo_encrypted_response_enc_column] ||= return_params["userinfo_encrypted_response_enc"] =
130
206
  "A128CBC-HS256"
131
- end
207
+
132
208
  end
133
- if create_params.key?(oauth_applications_request_object_encryption_alg_column)
134
- create_params[oauth_applications_request_object_encryption_enc_column] ||= begin
135
- return_params["request_object_encryption_enc"] = "A128CBC-HS256"
209
+ if defined?(oauth_applications_request_object_encryption_alg_column) &&
210
+ create_params.key?(oauth_applications_request_object_encryption_alg_column)
211
+ create_params[oauth_applications_request_object_encryption_enc_column] ||= return_params["request_object_encryption_enc"] =
136
212
  "A128CBC-HS256"
137
- end
213
+
138
214
  end
139
215
 
140
216
  super(return_params)
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oidc_rp_initiated_logout, :OidcRpInitiatedLogout) do
7
+ depends :oidc
8
+
9
+ auth_value_method :oauth_applications_post_logout_redirect_uris_column, :post_logout_redirect_uris
10
+ translatable_method :oauth_invalid_post_logout_redirect_uri_message, "Invalid post logout redirect URI"
11
+
12
+ # /oidc-logout
13
+ auth_server_route(:oidc_logout) do |r|
14
+ require_authorizable_account
15
+ before_oidc_logout_route
16
+
17
+ # OpenID Providers MUST support the use of the HTTP GET and POST methods
18
+ r.on method: %i[get post] do
19
+ catch_error do
20
+ validate_oidc_logout_params
21
+
22
+ #
23
+ # why this is done:
24
+ #
25
+ # we need to decode the id token in order to get the application, because, if the
26
+ # signing key is application-specific, we don't know how to verify the signature
27
+ # beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
28
+ # the @oauth_application, and then decode-and-verify.
29
+ #
30
+ claims = jwt_decode(param("id_token_hint"), verify_claims: false)
31
+
32
+ redirect_logout_with_error(oauth_invalid_client_message) unless claims
33
+
34
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
35
+ oauth_grant = db[oauth_grants_table]
36
+ .where(
37
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
38
+ oauth_grants_account_id_column => account_id
39
+ ).first
40
+
41
+ # check whether ID token belongs to currently logged-in user
42
+ redirect_logout_with_error(oauth_invalid_client_message) unless oauth_grant && claims["sub"] == jwt_subject(oauth_grant,
43
+ oauth_application)
44
+
45
+ # When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
46
+ redirect_logout_with_error(oauth_invalid_client_message) unless claims && claims["iss"] == oauth_jwt_issuer
47
+
48
+ # now let's logout from IdP
49
+ transaction do
50
+ before_logout
51
+ logout
52
+ after_logout
53
+ end
54
+
55
+ error_message = logout_notice_flash
56
+
57
+ if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
58
+ error_message = catch(:default_logout_redirect) do
59
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
60
+
61
+ throw(:default_logout_redirect, oauth_invalid_client_message) unless oauth_application
62
+
63
+ post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uris_column].split(" ")
64
+
65
+ unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
66
+ throw(:default_logout_redirect,
67
+ oauth_invalid_post_logout_redirect_uri_message)
68
+ end
69
+
70
+ if (state = param_or_nil("state"))
71
+ post_logout_redirect_uri = URI(post_logout_redirect_uri)
72
+ params = ["state=#{CGI.escape(state)}"]
73
+ params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
74
+ post_logout_redirect_uri.query = params.join("&")
75
+ post_logout_redirect_uri = post_logout_redirect_uri.to_s
76
+ end
77
+
78
+ redirect(post_logout_redirect_uri)
79
+ end
80
+
81
+ end
82
+
83
+ redirect_logout_with_error(error_message)
84
+ end
85
+
86
+ redirect_response_error("invalid_request")
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Logout
93
+
94
+ def validate_oidc_logout_params
95
+ redirect_logout_with_error(oauth_invalid_client_message) unless param_or_nil("id_token_hint")
96
+ # check if valid token hint type
97
+ return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
98
+
99
+ return if check_valid_no_fragment_uri?(redirect_uri)
100
+
101
+ redirect_logout_with_error(oauth_invalid_client_message)
102
+ end
103
+
104
+ def redirect_logout_with_error(error_message = oauth_invalid_client_message)
105
+ set_notice_flash(error_message)
106
+ redirect(logout_redirect)
107
+ end
108
+
109
+ def oauth_server_metadata_body(*)
110
+ super.tap do |data|
111
+ data[:end_session_endpoint] = oidc_logout_url
112
+ end
113
+ end
114
+ end
115
+ end
@@ -30,13 +30,14 @@ module Rodauth
30
30
  end
31
31
 
32
32
  if dataset.respond_to?(:supports_insert_conflict?) && dataset.supports_insert_conflict?
33
- def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
34
- to_update = params.keys - unique_columns
35
- to_update -= exclude_on_update if exclude_on_update
33
+ def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
34
+ to_update = Hash[(params.keys - unique_columns).map { |attribute| [attribute, Sequel[:excluded][attribute]] }]
35
+
36
+ to_update.merge!(to_update_extra) if to_update_extra
36
37
 
37
38
  dataset = dataset.insert_conflict(
38
39
  target: unique_columns,
39
- update: Hash[ to_update.map { |attribute| [attribute, Sequel[:excluded][attribute]] } ],
40
+ update: to_update,
40
41
  update_where: conds
41
42
  )
42
43
 
@@ -51,7 +52,7 @@ module Rodauth
51
52
  ) || dataset.where(params).first
52
53
  end
53
54
  else
54
- def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
55
+ def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
55
56
  find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
56
57
 
57
58
  dataset_where = dataset.where(find_params)
@@ -67,7 +68,8 @@ module Rodauth
67
68
  end
68
69
 
69
70
  if record
70
- update_params.reject! { |k, _v| exclude_on_update.include?(k) } if exclude_on_update
71
+ update_params.merge!(to_update_extra) if to_update_extra
72
+
71
73
  __update_and_return__(dataset_where, update_params)
72
74
  else
73
75
  __insert_and_return__(dataset, pkey, params)
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "rodauth/oauth/ttl_store"
6
+
7
+ module Rodauth
8
+ module OAuth
9
+ module HTTPExtensions
10
+ REQUEST_CACHE = OAuth::TtlStore.new
11
+
12
+ private
13
+
14
+ def http_request(uri, form_data = nil)
15
+ uri = URI(uri)
16
+
17
+ http = Net::HTTP.new(uri.host, uri.port)
18
+ http.use_ssl = uri.scheme == "https"
19
+ http.open_timeout = 15
20
+ http.read_timeout = 15
21
+ http.write_timeout = 15 if http.respond_to?(:write_timeout)
22
+
23
+ if form_data
24
+ request = Net::HTTP::Post.new(uri.request_uri)
25
+ request["content-type"] = "application/x-www-form-urlencoded"
26
+ request.set_form_data(form_data)
27
+ else
28
+ request = Net::HTTP::Get.new(uri.request_uri)
29
+ end
30
+ request["accept"] = json_response_content_type
31
+
32
+ yield request if block_given?
33
+
34
+ response = http.request(request)
35
+ authorization_required unless response.code.to_i == 200
36
+ response
37
+ end
38
+
39
+ def http_request_with_cache(uri, *args)
40
+ uri = URI(uri)
41
+
42
+ response = http_request_cache[uri]
43
+
44
+ return response if response
45
+
46
+ http_request_cache.set(uri) do
47
+ response = http_request(uri, *args)
48
+ ttl = if response.key?("cache-control")
49
+ cache_control = response["cache-control"]
50
+ if cache_control.include?("no-cache")
51
+ nil
52
+ else
53
+ max_age = cache_control[/max-age=(\d+)/, 1].to_i
54
+ max_age.zero? ? nil : max_age
55
+ end
56
+ elsif response.key?("expires")
57
+ expires = response["expires"]
58
+ begin
59
+ Time.parse(expires).to_i - Time.now.to_i
60
+ rescue ArgumentError
61
+ nil
62
+ end
63
+ end
64
+
65
+ [JSON.parse(response.body, symbolize_names: true), ttl]
66
+ end
67
+ end
68
+
69
+ def http_request_cache
70
+ REQUEST_CACHE
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,7 +2,27 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
+ module ControllerMethods
6
+ def self.included(controller)
7
+ # ActionController::API doesn't have helper methods
8
+ controller.helper_method :current_oauth_account, :current_oauth_application if controller.respond_to?(:helper_method)
9
+ end
10
+
11
+ def current_oauth_account(name = nil)
12
+ rodauth(name).current_oauth_account
13
+ end
14
+
15
+ def current_oauth_application(name = nil)
16
+ rodauth(name).current_oauth_application
17
+ end
18
+ end
19
+
5
20
  class Railtie < ::Rails::Railtie
21
+ initializer "rodauth.controller" do
22
+ ActiveSupport.on_load(:action_controller) do
23
+ include ControllerMethods
24
+ end
25
+ end
6
26
  end
7
27
  end
8
28
  end
@@ -28,6 +28,8 @@ class Rodauth::OAuth::TtlStore
28
28
 
29
29
  payload, ttl = block.call
30
30
 
31
+ return payload unless ttl
32
+
31
33
  @store_mutex.synchronize do
32
34
  # given that the block call triggers network, and two requests for the same key be processed
33
35
  # at the same time, this ensures the first one wins.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.10.4"
5
+ VERSION = "1.0.0-beta2"
6
6
  end
7
7
  end
data/lib/rodauth/oauth.rb CHANGED
@@ -1,7 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rodauth"
4
-
5
4
  require "rodauth/oauth/version"
6
5
 
6
+ module Rodauth
7
+ module OAuth
8
+ module FeatureExtensions
9
+ def auth_server_route(*args, &blk)
10
+ routes = route(*args, &blk)
11
+
12
+ handle_meth = routes.last
13
+
14
+ define_method(:"#{handle_meth}_for_auth_server") do
15
+ next unless is_authorization_server?
16
+
17
+ send(:"#{handle_meth}_not_for_auth_server")
18
+ end
19
+
20
+ alias_method :"#{handle_meth}_not_for_auth_server", handle_meth
21
+ alias_method handle_meth, :"#{handle_meth}_for_auth_server"
22
+ end
23
+
24
+ # override
25
+ def translatable_method(meth, value)
26
+ define_method(meth) { |*args| translate(meth, value, *args) }
27
+ auth_value_methods(meth)
28
+ end
29
+ end
30
+ end
31
+
32
+ Feature.prepend OAuth::FeatureExtensions
33
+ end
34
+
7
35
  require "rodauth/oauth/railtie" if defined?(Rails)
data/locales/en.yml CHANGED
@@ -3,21 +3,29 @@ en:
3
3
  require_authorization_error_flash: "Please authorize to continue"
4
4
  create_oauth_application_error_flash: "There was an error registering your oauth application"
5
5
  create_oauth_application_notice_flash: "Your oauth application has been registered"
6
- revoke_unauthorized_account_error_flash: "You are not authorized to revoke this token"
7
- revoke_oauth_token_notice_flash: "The oauth token has been revoked"
6
+ revoke_unauthorized_account_error_flash: "You are not authorized to revoke this grant"
7
+ revoke_oauth_grant_notice_flash: "The oauth grant has been revoked"
8
8
  device_verification_notice_flash: "The device is verified"
9
9
  user_code_not_found_error_flash: "No device to authorize with the given user code"
10
10
  authorize_page_title: "Authorize"
11
+ authorize_page_lead: "The application %{name} would like to access your data."
12
+ oauth_cancel_button: "Cancel"
11
13
  oauth_applications_page_title: "Oauth Applications"
12
14
  oauth_application_page_title: "Oauth Application"
13
15
  new_oauth_application_page_title: "New Oauth Application"
14
- oauth_application_oauth_tokens_page_title: "Application Oauth Tokens"
15
- oauth_tokens_page_title: "My Oauth Tokens"
16
+ oauth_application_oauth_grants_page_title: "Application Oauth Grants"
17
+ oauth_grants_page_title: "My Oauth Grants"
16
18
  device_verification_page_title: "Device Verification"
17
19
  device_search_page_title: "Device Search"
18
20
  oauth_management_pagination_previous_button: "Previous"
19
21
  oauth_management_pagination_next_button: "Next"
20
- oauth_tokens_scopes_label: "Scopes"
22
+ oauth_grants_type_label: "Grant Type"
23
+ oauth_grants_scopes_label: "Scopes"
24
+ oauth_grants_token_label: "Token"
25
+ oauth_grants_refresh_token_label: "Refresh Token"
26
+ oauth_grants_expires_in_label: "Expires In"
27
+ oauth_grants_revoked_at_label: "Revoked at"
28
+ oauth_no_grants_text: "No oauth grants yet!"
21
29
  oauth_applications_name_label: "Name"
22
30
  oauth_applications_description_label: "Description"
23
31
  oauth_applications_scopes_label: "Default scopes"
@@ -28,30 +36,34 @@ en:
28
36
  oauth_applications_redirect_uri_label: "Redirect URL"
29
37
  oauth_applications_client_secret_label: "Client Secret"
30
38
  oauth_applications_client_id_label: "Client ID"
39
+ oauth_no_applications_text: "No oauth applications yet!"
31
40
  oauth_grant_user_code_label: "User code"
32
41
  oauth_grant_user_jws_jwk_label: "JSON Web Keys"
33
42
  oauth_grant_user_jwt_public_key_label: "Public key"
34
43
  oauth_application_button: "Register"
35
44
  oauth_authorize_button: "Authorize"
36
- oauth_token_revoke_button: "Revoke"
45
+ oauth_grant_revoke_button: "Revoke"
37
46
  oauth_authorize_post_button: "Back to Client Application"
47
+ oauth_device_verification_page_lead: "The device with user code %{user_code} would like to access your data."
38
48
  oauth_device_verification_button: "Verify"
49
+ oauth_device_search_page_lead: "Insert the user code from the device you'd like to authorize."
39
50
  oauth_device_search_button: "Search"
40
- invalid_client_message: "Client authentication failed"
41
- invalid_grant_type_message: "Invalid grant type"
42
- invalid_grant_message: "Invalid grant"
43
- invalid_scope_message: "Invalid scope"
51
+ oauth_invalid_client_message: "Client authentication failed"
52
+ oauth_invalid_grant_type_message: "Invalid grant type"
53
+ oauth_invalid_grant_message: "Invalid grant"
54
+ oauth_invalid_scope_message: "Invalid scope"
44
55
  invalid_url_message: "Invalid URL"
45
- unsupported_token_type_message: "Invalid token type hint"
46
- unique_error_message: "is already in use"
56
+ oauth_unsupported_token_type_message: "Invalid token type hint"
47
57
  null_error_message: "is not filled"
48
- already_in_use_message: "error generating unique token"
49
- expired_token_message: "the device code has expired"
50
- access_denied_message: "the authorization request has been denied"
51
- authorization_pending_message: "the authorization request is still pending"
52
- slow_down_message: "authorization request is still pending but poll interval should be increased"
53
- code_challenge_required_message: "code challenge required"
54
- unsupported_transform_algorithm_message: "transform algorithm not supported"
55
- request_uri_not_supported_message: "request uri is unsupported"
56
- invalid_request_object_message: "request object is invalid"
57
- invalid_scope_message: "The Access Token expired"
58
+ oauth_unsupported_response_type_message: "Unsupported response type"
59
+ oauth_already_in_use_message: "error generating unique token"
60
+ oauth_expired_token_message: "the device code has expired"
61
+ oauth_access_denied_message: "the authorization request has been denied"
62
+ oauth_authorization_pending_message: "the authorization request is still pending"
63
+ oauth_slow_down_message: "authorization request is still pending but poll interval should be increased"
64
+ oauth_code_challenge_required_message: "code challenge required"
65
+ oauth_unsupported_transform_algorithm_message: "transform algorithm not supported"
66
+ oauth_invalid_request_object_message: "request object is invalid"
67
+ oauth_invalid_scope_message: "The Access Token expired"
68
+ oauth_authorize_parameter_required: "'%{parameter}' is a required parameter"
69
+ oauth_invalid_post_logout_redirect_uri_message: "Invalid post logout redirect URI"