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, :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