rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
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, :Oidc) do
5
7
  # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
@@ -15,6 +17,7 @@ module Rodauth
15
17
  issuer
16
18
  authorization_endpoint
17
19
  end_session_endpoint
20
+ backchannel_logout_session_supported
18
21
  token_endpoint
19
22
  userinfo_endpoint
20
23
  jwks_uri
@@ -60,78 +63,98 @@ module Rodauth
60
63
  id_token_signing_alg_values_supported
61
64
  ].freeze
62
65
 
63
- depends :account_expiration, :oauth_jwt
66
+ depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
64
67
 
65
- auth_value_method :oauth_application_default_scope, "openid"
66
68
  auth_value_method :oauth_application_scopes, %w[openid]
67
69
 
68
- auth_value_method :oauth_applications_id_token_signed_response_alg_column, :id_token_signed_response_alg
69
- auth_value_method :oauth_applications_id_token_encrypted_response_alg_column, :id_token_encrypted_response_alg
70
- auth_value_method :oauth_applications_id_token_encrypted_response_enc_column, :id_token_encrypted_response_enc
71
- auth_value_method :oauth_applications_userinfo_signed_response_alg_column, :userinfo_signed_response_alg
72
- auth_value_method :oauth_applications_userinfo_encrypted_response_alg_column, :userinfo_encrypted_response_alg
73
- auth_value_method :oauth_applications_userinfo_encrypted_response_enc_column, :userinfo_encrypted_response_enc
70
+ %i[
71
+ subject_type application_type sector_identifier_uri
72
+ id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
73
+ userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
74
+ ].each do |column|
75
+ auth_value_method :"oauth_applications_#{column}_column", column
76
+ end
74
77
 
75
- auth_value_method :oauth_grants_nonce_column, :nonce
76
- auth_value_method :oauth_grants_acr_column, :acr
77
- auth_value_method :oauth_tokens_nonce_column, :nonce
78
- auth_value_method :oauth_tokens_acr_column, :acr
78
+ %i[nonce acr claims_locales claims].each do |column|
79
+ auth_value_method :"oauth_grants_#{column}_column", column
80
+ end
79
81
 
80
- translatable_method :invalid_scope_message, "The Access Token expired"
82
+ auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
83
+ auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
81
84
 
82
- auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
85
+ translatable_method :oauth_invalid_scope_message, "The Access Token expired"
83
86
 
84
87
  auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
85
88
  auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
86
89
  auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
87
90
 
88
- # logout
89
- auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
90
- auth_value_method :use_rp_initiated_logout?, false
91
-
92
91
  auth_value_methods(
92
+ :oauth_acr_values_supported,
93
+ :get_oidc_account_last_login_at,
94
+ :oidc_authorize_on_prompt_none?,
93
95
  :get_oidc_param,
94
96
  :get_additional_param,
95
97
  :require_acr_value_phr,
96
98
  :require_acr_value_phrh,
97
- :require_acr_value
99
+ :require_acr_value,
100
+ :json_webfinger_payload
98
101
  )
99
102
 
100
103
  # /userinfo
101
- route(:userinfo) do |r|
102
- next unless is_authorization_server?
103
-
104
+ auth_server_route(:userinfo) do |r|
104
105
  r.on method: %i[get post] do
105
106
  catch_error do
106
- oauth_token = authorization_token
107
+ claims = authorization_token
107
108
 
108
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
109
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless claims
109
110
 
110
- oauth_scopes = oauth_token["scope"].split(" ")
111
+ oauth_scopes = claims["scope"].split(" ")
111
112
 
112
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
113
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
113
114
 
114
- account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
115
+ account = db[accounts_table].where(account_id_column => claims["sub"]).first
115
116
 
116
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
117
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
117
118
 
118
119
  oauth_scopes.delete("openid")
119
120
 
120
- oidc_claims = { "sub" => oauth_token["sub"] }
121
+ oidc_claims = { "sub" => claims["sub"] }
121
122
 
122
- fill_with_account_claims(oidc_claims, account, oauth_scopes)
123
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
123
124
 
124
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
125
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless @oauth_application
125
126
 
126
- if (algo = @oauth_application && @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
127
+ oauth_grant = valid_oauth_grant_ds(
128
+ oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
129
+ oauth_grants_account_id_column => account[account_id_column]
130
+ ).first
131
+
132
+ claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
133
+
134
+ if (claims = oauth_grant[oauth_grants_claims_column])
135
+ claims = JSON.parse(claims)
136
+ if (userinfo_essential_claims = claims["userinfo"])
137
+ oauth_scopes |= userinfo_essential_claims.to_a
138
+ end
139
+ end
140
+
141
+ # 5.4 - The Claims requested by the profile, email, address, and phone scope values are returned from the UserInfo Endpoint
142
+ fill_with_account_claims(oidc_claims, account, oauth_scopes, claims_locales)
143
+
144
+ if (algo = @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
127
145
  params = {
128
- jwks: oauth_application_jwks,
146
+ jwks: oauth_application_jwks(@oauth_application),
129
147
  encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
130
148
  encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
131
149
  }.compact
132
150
 
133
151
  jwt = jwt_encode(
134
- oidc_claims,
152
+ oidc_claims.merge(
153
+ # If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value
154
+ # SHOULD be the OP's Issuer Identifier URL. The aud value SHOULD be or include the RP's Client ID value.
155
+ iss: oauth_jwt_issuer,
156
+ aud: @oauth_application[oauth_applications_client_id_column]
157
+ ),
135
158
  signing_algorithm: algo,
136
159
  **params
137
160
  )
@@ -141,92 +164,23 @@ module Rodauth
141
164
  end
142
165
  end
143
166
 
144
- throw_json_response_error(authorization_required_error_status, "invalid_token")
145
- end
146
- end
147
-
148
- # /oidc-logout
149
- route(:oidc_logout) do |r|
150
- next unless use_rp_initiated_logout?
151
-
152
- before_oidc_logout_route
153
- require_authorizable_account
154
-
155
- # OpenID Providers MUST support the use of the HTTP GET and POST methods
156
- r.on method: %i[get post] do
157
- catch_error do
158
- validate_oidc_logout_params
159
-
160
- #
161
- # why this is done:
162
- #
163
- # we need to decode the id token in order to get the application, because, if the
164
- # signing key is application-specific, we don't know how to verify the signature
165
- # beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
166
- # the @oauth_application, and then decode-and-verify.
167
- #
168
- oauth_token = jwt_decode(param("id_token_hint"), verify_claims: false)
169
- oauth_application_id = oauth_token["client_id"]
170
-
171
- # check whether ID token belongs to currently logged-in user
172
- redirect_response_error("invalid_request") unless oauth_token["sub"] == jwt_subject(
173
- oauth_tokens_account_id_column => account_id,
174
- oauth_tokens_oauth_application_id_column => oauth_application_id
175
- )
176
-
177
- # When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
178
- redirect_response_error("invalid_request") unless oauth_token && oauth_token["iss"] == issuer
179
-
180
- # now let's logout from IdP
181
- transaction do
182
- before_logout
183
- logout
184
- after_logout
185
- end
186
-
187
- if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
188
- catch(:default_logout_redirect) do
189
- oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
190
-
191
- throw(:default_logout_redirect) unless oauth_application
192
-
193
- post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uri_column].split(" ")
194
-
195
- throw(:default_logout_redirect) unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
196
-
197
- if (state = param_or_nil("state"))
198
- post_logout_redirect_uri = URI(post_logout_redirect_uri)
199
- params = ["state=#{state}"]
200
- params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
201
- post_logout_redirect_uri.query = params.join("&")
202
- post_logout_redirect_uri = post_logout_redirect_uri.to_s
203
- end
204
-
205
- redirect(post_logout_redirect_uri)
206
- end
207
-
208
- end
209
-
210
- # regular logout procedure
211
- set_notice_flash(logout_notice_flash)
212
- redirect(logout_redirect)
213
- end
214
-
215
- redirect_response_error("invalid_request")
167
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_token")
216
168
  end
217
169
  end
218
170
 
219
- def openid_configuration(alt_issuer = nil)
171
+ def load_openid_configuration_route(alt_issuer = nil)
220
172
  request.on(".well-known/openid-configuration") do
221
173
  allow_cors(request)
222
174
 
223
- request.get do
224
- json_response_success(openid_configuration_body(alt_issuer), cache: true)
175
+ request.is do
176
+ request.get do
177
+ json_response_success(openid_configuration_body(alt_issuer), cache: true)
178
+ end
225
179
  end
226
180
  end
227
181
  end
228
182
 
229
- def webfinger
183
+ def load_webfinger_route
230
184
  request.on(".well-known/webfinger") do
231
185
  request.get do
232
186
  resource = param_or_nil("resource")
@@ -236,14 +190,7 @@ module Rodauth
236
190
  response.status = 200
237
191
  response["Content-Type"] ||= "application/jrd+json"
238
192
 
239
- json_payload = JSON.dump({
240
- subject: resource,
241
- links: [{
242
- rel: webfinger_relation,
243
- href: authorization_server_url
244
- }]
245
- })
246
- return_response(json_payload)
193
+ return_response(json_webfinger_payload)
247
194
  end
248
195
  end
249
196
  end
@@ -257,6 +204,20 @@ module Rodauth
257
204
  end
258
205
  end
259
206
 
207
+ def oauth_response_types_supported
208
+ grant_types = oauth_grant_types_supported
209
+ oidc_response_types = %w[id_token none]
210
+ oidc_response_types |= ["code id_token"] if grant_types.include?("authorization_code")
211
+ oidc_response_types |= ["code token", "id_token token", "code id_token token"] if grant_types.include?("implicit")
212
+ super | oidc_response_types
213
+ end
214
+
215
+ def current_oauth_account
216
+ subject_type = current_oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
217
+
218
+ return super unless subject_type == "pairwise"
219
+ end
220
+
260
221
  private
261
222
 
262
223
  if defined?(::I18n)
@@ -272,18 +233,62 @@ module Rodauth
272
233
  end
273
234
  end
274
235
 
236
+ def oauth_acr_values_supported
237
+ acr_values = []
238
+ acr_values << "phrh" if features.include?(:webauthn_login)
239
+ acr_values << "phr" if respond_to?(:require_two_factor_authenticated)
240
+ acr_values
241
+ end
242
+
243
+ def oidc_authorize_on_prompt_none?(_account)
244
+ false
245
+ end
246
+
275
247
  def validate_authorize_params
276
- return super unless (max_age = param_or_nil("max_age"))
248
+ if (max_age = param_or_nil("max_age"))
277
249
 
278
- max_age = Integer(max_age)
250
+ max_age = Integer(max_age)
279
251
 
280
- redirect_response_error("invalid_request") unless max_age.positive?
252
+ redirect_response_error("invalid_request") unless max_age.positive?
281
253
 
282
- if Time.now - last_account_login_at > max_age
283
- # force user to re-login
284
- clear_session
285
- set_session_value(login_redirect_session_key, request.fullpath)
286
- redirect require_login_redirect
254
+ if Time.now - get_oidc_account_last_login_at(session_value) > max_age
255
+ # force user to re-login
256
+ clear_session
257
+ set_session_value(login_redirect_session_key, request.fullpath)
258
+ redirect require_login_redirect
259
+ end
260
+ end
261
+
262
+ if (claims = param_or_nil("claims"))
263
+ # The value is a JSON object listing the requested Claims.
264
+ claims = JSON.parse(claims)
265
+
266
+ claims.each do |_, individual_claims|
267
+ redirect_response_error("invalid_request") unless individual_claims.is_a?(Hash)
268
+
269
+ individual_claims.each do |_, claim|
270
+ redirect_response_error("invalid_request") unless claim.nil? || individual_claims.is_a?(Hash)
271
+ end
272
+ end
273
+ end
274
+
275
+ sc = scopes
276
+
277
+ if sc && sc.include?("offline_access")
278
+
279
+ sc.delete("offline_access")
280
+
281
+ # MUST ensure that the prompt parameter contains consent
282
+ # MUST ignore the offline_access request unless the Client
283
+ # is using a response_type value that would result in an
284
+ # Authorization Code
285
+ if param_or_nil("prompt") == "consent" && (
286
+ (response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
287
+ )
288
+ request.params["access_type"] = "offline"
289
+ end
290
+
291
+ request.params["scope"] = sc.join(" ")
287
292
  end
288
293
 
289
294
  super
@@ -292,7 +297,42 @@ module Rodauth
292
297
  def require_authorizable_account
293
298
  try_prompt
294
299
  super
295
- try_acr_values
300
+ @acr = try_acr_values
301
+ end
302
+
303
+ def get_oidc_account_last_login_at(account_id)
304
+ get_activity_timestamp(account_id, account_activity_last_activity_column)
305
+ end
306
+
307
+ def jwt_subject(oauth_grant, client_application = oauth_application)
308
+ subject_type = client_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
309
+
310
+ case subject_type
311
+ when "public"
312
+ super
313
+ when "pairwise"
314
+ identifier_uri = client_application[oauth_applications_sector_identifier_uri_column]
315
+
316
+ unless identifier_uri
317
+ identifier_uri = client_application[oauth_applications_redirect_uri_column]
318
+ identifier_uri = identifier_uri.split(" ")
319
+ # If the Client has not provided a value for sector_identifier_uri in Dynamic Client Registration
320
+ # [OpenID.Registration], the Sector Identifier used for pairwise identifier calculation is the host
321
+ # component of the registered redirect_uri. If there are multiple hostnames in the registered redirect_uris,
322
+ # the Client MUST register a sector_identifier_uri.
323
+ if identifier_uri.size > 1
324
+ # return error message
325
+ end
326
+ identifier_uri = identifier_uri.first
327
+ end
328
+
329
+ identifier_uri = URI(identifier_uri).host
330
+
331
+ account_id = oauth_grant[oauth_grants_account_id_column]
332
+ Digest::SHA256.hexdigest("#{identifier_uri}#{account_id}#{oauth_jwt_subject_secret}")
333
+ else
334
+ raise StandardError, "unexpected subject (#{subject_type})"
335
+ end
296
336
  end
297
337
 
298
338
  # this executes before checking for a logged in account
@@ -301,22 +341,18 @@ module Rodauth
301
341
 
302
342
  case prompt
303
343
  when "none"
344
+ return unless request.get?
345
+
304
346
  redirect_response_error("login_required") unless logged_in?
305
347
 
306
348
  require_account
307
349
 
308
- if db[oauth_grants_table].where(
309
- oauth_grants_account_id_column => account_id,
310
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
311
- oauth_grants_redirect_uri_column => redirect_uri,
312
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
313
- oauth_grants_access_type_column => "online"
314
- ).count.zero?
315
- redirect_response_error("consent_required")
316
- end
350
+ redirect_response_error("interaction_required") unless oidc_authorize_on_prompt_none?(account_from_session)
317
351
 
318
352
  request.env["REQUEST_METHOD"] = "POST"
319
353
  when "login"
354
+ return unless request.get?
355
+
320
356
  if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
321
357
  ::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
322
358
  return
@@ -333,18 +369,17 @@ module Rodauth
333
369
 
334
370
  redirect require_login_redirect
335
371
  when "consent"
372
+ return unless request.post?
373
+
336
374
  require_account
337
375
 
338
- if db[oauth_grants_table].where(
339
- oauth_grants_account_id_column => account_id,
340
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
341
- oauth_grants_redirect_uri_column => redirect_uri,
342
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
343
- oauth_grants_access_type_column => "online"
344
- ).count.zero?
345
- redirect_response_error("consent_required")
346
- end
376
+ sc = scopes || []
377
+
378
+ redirect_response_error("consent_required") if sc.empty?
379
+
347
380
  when "select-account"
381
+ return unless request.get?
382
+
348
383
  # only works if select_account plugin is available
349
384
  require_select_account if respond_to?(:require_select_account)
350
385
  else
@@ -356,89 +391,140 @@ module Rodauth
356
391
  return unless (acr_values = param_or_nil("acr_values"))
357
392
 
358
393
  acr_values.split(" ").each do |acr_value|
394
+ next unless oauth_acr_values_supported.include?(acr_value)
395
+
359
396
  case acr_value
360
- when "phr" then require_acr_value_phr
361
- when "phrh" then require_acr_value_phrh
397
+ when "phr"
398
+ return acr_value if require_acr_value_phr
399
+ when "phrh"
400
+ return acr_value if require_acr_value_phrh
362
401
  else
363
- require_acr_value(acr_value)
402
+ return acr_value if require_acr_value(acr_value)
364
403
  end
365
404
  end
405
+
406
+ nil
366
407
  end
367
408
 
368
409
  def require_acr_value_phr
369
- return unless respond_to?(:require_two_factor_authenticated)
410
+ return false unless respond_to?(:require_two_factor_authenticated)
370
411
 
371
412
  require_two_factor_authenticated
413
+ true
372
414
  end
373
415
 
374
416
  def require_acr_value_phrh
417
+ return false unless features.include?(:webauthn_login)
418
+
375
419
  require_acr_value_phr && two_factor_login_type_match?("webauthn")
376
420
  end
377
421
 
378
- def require_acr_value(_acr); end
422
+ def require_acr_value(_acr)
423
+ true
424
+ end
379
425
 
380
426
  def create_oauth_grant(create_params = {})
381
- if (nonce = param_or_nil("nonce"))
382
- create_params[oauth_grants_nonce_column] = nonce
383
- end
384
- if (acr = param_or_nil("acr"))
385
- create_params[oauth_grants_acr_column] = acr
386
- end
427
+ create_params.replace(oidc_grant_params.merge(create_params))
387
428
  super
388
429
  end
389
430
 
390
- def create_oauth_token_from_authorization_code(oauth_grant, create_params, *)
391
- create_params[oauth_tokens_nonce_column] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
392
- create_params[oauth_tokens_acr_column] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
393
-
394
- super
431
+ def create_oauth_grant_with_token(create_params = {})
432
+ create_params[oauth_grants_type_column] = "hybrid"
433
+ create_params[oauth_grants_account_id_column] = account_id
434
+ create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in)
435
+ authorization_code = create_oauth_grant(create_params)
436
+ access_token = if oauth_jwt_access_tokens
437
+ _generate_jwt_access_token(create_params)
438
+ else
439
+ oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => authorization_code).first
440
+ _generate_access_token(oauth_grant)
441
+ end
442
+
443
+ {
444
+ "code" => authorization_code,
445
+ **json_access_token_payload(oauth_grants_token_column => access_token)
446
+ }
395
447
  end
396
448
 
397
- def create_oauth_token(*)
398
- oauth_token = super
399
- generate_id_token(oauth_token)
400
- oauth_token
449
+ def create_token(*)
450
+ oauth_grant = super
451
+ generate_id_token(oauth_grant)
452
+ oauth_grant
401
453
  end
402
454
 
403
- def generate_id_token(oauth_token)
404
- oauth_scopes = oauth_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
455
+ def generate_id_token(oauth_grant, include_claims = false)
456
+ oauth_scopes = oauth_grant[oauth_grants_scopes_column].split(oauth_scope_separator)
405
457
 
406
458
  return unless oauth_scopes.include?("openid")
407
459
 
408
- id_token_claims = jwt_claims(oauth_token)
460
+ id_token_claims = jwt_claims(oauth_grant)
409
461
 
410
- id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
462
+ id_token_claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
411
463
 
412
- id_token_claims[:acr] = oauth_token[oauth_tokens_acr_column] if oauth_token[oauth_tokens_acr_column]
464
+ id_token_claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
413
465
 
414
466
  # Time when the End-User authentication occurred.
415
- id_token_claims[:auth_time] = last_account_login_at.to_i
467
+ id_token_claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
416
468
 
417
- account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
469
+ account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
418
470
 
419
471
  # this should never happen!
420
472
  # a newly minted oauth token from a grant should have been assigned to an account
421
473
  # who just authorized its generation.
422
474
  return unless account
423
475
 
424
- fill_with_account_claims(id_token_claims, account, oauth_scopes)
476
+ if (claims = oauth_grant[oauth_grants_claims_column])
477
+ claims = JSON.parse(claims)
478
+ if (id_token_essential_claims = claims["id_token"])
479
+ oauth_scopes |= id_token_essential_claims.to_a
480
+
481
+ include_claims = true
482
+ end
483
+ end
484
+
485
+ # 5.4 - However, when no Access Token is issued (which is the case for the response_type value id_token),
486
+ # the resulting Claims are returned in the ID Token.
487
+ fill_with_account_claims(id_token_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
425
488
 
426
489
  params = {
427
- jwks: oauth_application_jwks,
428
- signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm,
490
+ jwks: oauth_application_jwks(oauth_application),
491
+ signing_algorithm: (
492
+ oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
493
+ oauth_jwt_keys.keys.first
494
+ ),
429
495
  encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
430
496
  encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
431
497
  }.compact
432
498
 
433
- oauth_token[:id_token] = jwt_encode(id_token_claims, **params)
499
+ oauth_grant[:id_token] = jwt_encode(id_token_claims, **params)
434
500
  end
435
501
 
436
502
  # aka fill_with_standard_claims
437
- def fill_with_account_claims(claims, account, scopes)
503
+ def fill_with_account_claims(claims, account, scopes, claims_locales)
504
+ additional_claims_info = {}
505
+
438
506
  scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc|
439
507
  next if scope == "openid"
440
508
 
441
- oidc, param = scope.split(".", 2)
509
+ if scope.is_a?(Array)
510
+ # essential claims
511
+ param, additional_info = scope
512
+
513
+ param = param.to_sym
514
+
515
+ oidc, = OIDC_SCOPES_MAP.find do |_, oidc_scopes|
516
+ oidc_scopes.include?(param)
517
+ end || param.to_s
518
+
519
+ param = nil if oidc == param.to_s
520
+
521
+ additional_claims_info[param] = additional_info
522
+ else
523
+
524
+ oidc, param = scope.split(".", 2)
525
+
526
+ param = param.to_sym if param
527
+ end
442
528
 
443
529
  by_oidc[oidc] ||= []
444
530
 
@@ -447,13 +533,11 @@ module Rodauth
447
533
 
448
534
  oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
449
535
 
450
- if (claims_locales = param_or_nil("claims_locales"))
451
- claims_locales = claims_locales.split(" ").map(&:to_sym)
452
- end
536
+ claims_locales = claims_locales.split(" ").map(&:to_sym) if claims_locales
453
537
 
454
538
  unless oidc_scopes.empty?
455
539
  if respond_to?(:get_oidc_param)
456
- get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales)
540
+ get_oidc_param = proxy_get_param(:get_oidc_param, claims, claims_locales, additional_claims_info)
457
541
 
458
542
  oidc_scopes.each do |scope|
459
543
  scope_claims = claims
@@ -474,7 +558,7 @@ module Rodauth
474
558
  return if additional_scopes.empty?
475
559
 
476
560
  if respond_to?(:get_additional_param)
477
- get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales)
561
+ get_additional_param = proxy_get_param(:get_additional_param, claims, claims_locales, additional_claims_info)
478
562
 
479
563
  additional_scopes.each do |scope|
480
564
  get_additional_param[account, scope.to_sym]
@@ -484,32 +568,45 @@ module Rodauth
484
568
  end
485
569
  end
486
570
 
487
- def proxy_get_param(get_param_func, claims, claims_locales)
571
+ def proxy_get_param(get_param_func, claims, claims_locales, additional_claims_info)
488
572
  meth = method(get_param_func)
489
573
  if meth.arity == 2
490
- ->(account, param, cl = claims) { cl[param] = meth[account, param] }
574
+ lambda do |account, param, cl = claims|
575
+ additional_info = additional_claims_info[param] || EMPTY_HASH
576
+ value = additional_info["value"] || meth[account, param]
577
+ value = nil if additional_info["values"] && additional_info["values"].include?(value)
578
+ cl[param] = value if value
579
+ end
491
580
  elsif claims_locales.nil?
492
- ->(account, param, cl = claims) { cl[param] = meth[account, param, nil] }
581
+ lambda do |account, param, cl = claims|
582
+ additional_info = additional_claims_info[param] || EMPTY_HASH
583
+ value = additional_info["value"] || meth[account, param, nil]
584
+ value = nil if additional_info["values"] && additional_info["values"].include?(value)
585
+ cl[param] = value if value
586
+ end
493
587
  else
494
588
  lambda do |account, param, cl = claims|
495
589
  claims_values = claims_locales.map do |locale|
496
- meth[account, param, locale]
497
- end
590
+ additional_info = additional_claims_info[param] || EMPTY_HASH
591
+ value = additional_info["value"] || meth[account, param, locale]
592
+ value = nil if additional_info["values"] && additional_info["values"].include?(value)
593
+ value
594
+ end.compact
498
595
 
499
596
  if claims_values.uniq.size == 1
500
597
  cl[param] = claims_values.first
501
598
  else
502
599
  claims_locales.zip(claims_values).each do |locale, value|
503
- cl["#{param}##{locale}"] = value
600
+ cl["#{param}##{locale}"] = value if value
504
601
  end
505
602
  end
506
603
  end
507
604
  end
508
605
  end
509
606
 
510
- def json_access_token_payload(oauth_token)
607
+ def json_access_token_payload(oauth_grant)
511
608
  payload = super
512
- payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
609
+ payload["id_token"] = oauth_grant[:id_token] if oauth_grant[:id_token]
513
610
  payload
514
611
  end
515
612
 
@@ -517,66 +614,102 @@ module Rodauth
517
614
 
518
615
  def check_valid_response_type?
519
616
  case param_or_nil("response_type")
520
- when "none", "id_token",
521
- "code token", "code id_token", "id_token token", "code id_token token" # multiple
617
+ when "none", "id_token", "code id_token" # multiple
522
618
  true
619
+ when "code token", "id_token token", "code id_token token"
620
+ supports_token_response_type?
523
621
  else
524
622
  super
525
623
  end
526
624
  end
527
625
 
528
- def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
529
- return super unless use_oauth_implicit_grant_type?
626
+ def supported_response_mode?(response_mode, *)
627
+ return super unless response_mode == "none"
628
+
629
+ param("response_type") == "none"
630
+ end
530
631
 
531
- case param("response_type")
632
+ def supports_token_response_type?
633
+ features.include?(:oauth_implicit_grant)
634
+ end
635
+
636
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
637
+ response_type = param("response_type")
638
+ case response_type
532
639
  when "id_token"
533
- response_params.replace(_do_authorize_id_token)
640
+ grant_params = oidc_grant_params
641
+ generate_id_token(grant_params, true)
642
+ response_params.replace("id_token" => grant_params[:id_token])
534
643
  when "code token"
535
- redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
644
+ redirect_response_error("invalid_request") unless supports_token_response_type?
536
645
 
537
- response_params.replace(_do_authorize_code.merge(_do_authorize_token))
646
+ response_params.replace(create_oauth_grant_with_token)
538
647
  when "code id_token"
539
- response_params.replace(_do_authorize_code.merge(_do_authorize_id_token))
648
+ params = _do_authorize_code
649
+ oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
650
+ generate_id_token(oauth_grant)
651
+ response_params.replace(
652
+ "id_token" => oauth_grant[:id_token],
653
+ "code" => params["code"]
654
+ )
540
655
  when "id_token token"
541
- response_params.replace(_do_authorize_id_token.merge(_do_authorize_token))
656
+ redirect_response_error("invalid_request") unless supports_token_response_type?
657
+
658
+ oauth_grant = _do_authorize_token(oauth_grants_type_column => "hybrid")
659
+ generate_id_token(oauth_grant)
660
+
661
+ response_params.replace(json_access_token_payload(oauth_grant))
542
662
  when "code id_token token"
663
+ redirect_response_error("invalid_request") unless supports_token_response_type?
664
+
665
+ params = create_oauth_grant_with_token
666
+ oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
667
+ generate_id_token(oauth_grant)
543
668
 
544
- response_params.replace(_do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token))
669
+ response_params.replace(params.merge("id_token" => oauth_grant[:id_token]))
670
+ when "none"
671
+ response_mode ||= "none"
545
672
  end
546
673
  response_mode ||= "fragment" unless response_params.empty?
547
674
 
548
675
  super(response_params, response_mode)
549
676
  end
550
677
 
551
- def _do_authorize_id_token
552
- create_params = {
553
- oauth_tokens_account_id_column => account_id,
554
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
555
- oauth_tokens_scopes_column => scopes
678
+ def oidc_grant_params
679
+ grant_params = {
680
+ oauth_grants_account_id_column => account_id,
681
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
682
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
556
683
  }
557
684
  if (nonce = param_or_nil("nonce"))
558
- create_params[oauth_grants_nonce_column] = nonce
685
+ grant_params[oauth_grants_nonce_column] = nonce
686
+ end
687
+ grant_params[oauth_grants_acr_column] = @acr if @acr
688
+ if (claims_locales = param_or_nil("claims_locales"))
689
+ grant_params[oauth_grants_claims_locales_column] = claims_locales
559
690
  end
560
- if (acr = param_or_nil("acr"))
561
- create_params[oauth_grants_acr_column] = acr
691
+ if (claims = param_or_nil("claims"))
692
+ grant_params[oauth_grants_claims_column] = claims
562
693
  end
563
- oauth_token = generate_oauth_token(create_params, false)
564
- generate_id_token(oauth_token)
565
- params = json_access_token_payload(oauth_token)
566
- params.delete("access_token")
567
- params
694
+ grant_params
568
695
  end
569
696
 
570
- # Logout
571
-
572
- def validate_oidc_logout_params
573
- redirect_response_error("invalid_request") unless param_or_nil("id_token_hint")
574
- # check if valid token hint type
575
- return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
697
+ def authorize_response(params, mode)
698
+ redirect_url = URI.parse(redirect_uri)
699
+ redirect(redirect_url.to_s) if mode == "none"
700
+ super
701
+ end
576
702
 
577
- return if check_valid_uri?(redirect_uri)
703
+ # Webfinger
578
704
 
579
- redirect_response_error("invalid_request")
705
+ def json_webfinger_payload
706
+ JSON.dump({
707
+ subject: param("resource"),
708
+ links: [{
709
+ rel: "http://openid.net/specs/connect/1.0/issuer",
710
+ href: authorization_server_url
711
+ }]
712
+ })
580
713
  end
581
714
 
582
715
  # Metadata
@@ -598,29 +731,23 @@ module Rodauth
598
731
 
599
732
  scope_claims.unshift("auth_time")
600
733
 
601
- response_types_supported = metadata[:response_types_supported]
602
-
603
- if metadata[:grant_types_supported].include?("implicit")
604
- response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
605
- end
606
-
607
734
  metadata.merge(
608
735
  userinfo_endpoint: userinfo_url,
609
- end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?),
610
- response_types_supported: response_types_supported,
611
- subject_types_supported: [oauth_jwt_subject_type],
736
+ subject_types_supported: %w[public pairwise],
737
+ acr_values_supported: oauth_acr_values_supported,
738
+ claims_parameter_supported: true,
612
739
 
613
- id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
614
- id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
615
- id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
740
+ id_token_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
741
+ id_token_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
742
+ id_token_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
616
743
 
617
- userinfo_signing_alg_values_supported: [],
618
- userinfo_encryption_alg_values_supported: [],
619
- userinfo_encryption_enc_values_supported: [],
744
+ userinfo_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
745
+ userinfo_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
746
+ userinfo_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
620
747
 
621
- request_object_signing_alg_values_supported: [],
622
- request_object_encryption_alg_values_supported: [],
623
- request_object_encryption_enc_values_supported: [],
748
+ request_object_signing_alg_values_supported: oauth_jwt_jws_algorithms_supported,
749
+ request_object_encryption_alg_values_supported: oauth_jwt_jwe_algorithms_supported,
750
+ request_object_encryption_enc_values_supported: oauth_jwt_jwe_encryption_methods_supported,
624
751
 
625
752
  # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
626
753
  # Values defined by this specification are normal, aggregated, and distributed.
@@ -644,5 +771,19 @@ module Rodauth
644
771
  response.status = 200
645
772
  return_response
646
773
  end
774
+
775
+ def jwt_response_success(jwt, cache = false)
776
+ response.status = 200
777
+ response["Content-Type"] ||= "application/jwt"
778
+ if cache
779
+ # defaulting to 1-day for everyone, for now at least
780
+ max_age = 60 * 60 * 24
781
+ response["Cache-Control"] = "private, max-age=#{max_age}"
782
+ else
783
+ response["Cache-Control"] = "no-store"
784
+ response["Pragma"] = "no-cache"
785
+ end
786
+ return_response(jwt)
787
+ end
647
788
  end
648
789
  end