rodauth-oauth 0.1.0 → 0.4.2

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.
@@ -2,33 +2,27 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:oauth_http_mac) do
5
- # :nocov:
6
5
  unless String.method_defined?(:delete_prefix)
7
6
  module PrefixExtensions
8
7
  refine(String) do
9
8
  def delete_suffix(suffix)
10
9
  suffix = suffix.to_s
11
10
  len = suffix.length
12
- if len.positive? && index(suffix, -len)
13
- self[0...-len]
14
- else
15
- dup
16
- end
11
+ return dup unless len.positive? && index(suffix, -len)
12
+
13
+ self[0...-len]
17
14
  end
18
15
 
19
16
  def delete_prefix(prefix)
20
17
  prefix = prefix.to_s
21
- if rindex(prefix, 0)
22
- self[prefix.length..-1]
23
- else
24
- dup
25
- end
18
+ return dup unless rindex(prefix, 0)
19
+
20
+ self[prefix.length..-1]
26
21
  end
27
22
  end
28
23
  end
29
24
  using(PrefixExtensions)
30
25
  end
31
- # :nocov:
32
26
 
33
27
  depends :oauth
34
28
 
@@ -6,6 +6,8 @@ module Rodauth
6
6
  Feature.define(:oauth_jwt) do
7
7
  depends :oauth
8
8
 
9
+ JWKS = OAuth::TtlStore.new
10
+
9
11
  auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
10
12
  auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
11
13
 
@@ -22,6 +24,10 @@ module Rodauth
22
24
  auth_value_method :oauth_jwt_jwe_algorithm, nil
23
25
  auth_value_method :oauth_jwt_jwe_encryption_method, nil
24
26
 
27
+ # values used for rotating keys
28
+ auth_value_method :oauth_jwt_legacy_public_key, nil
29
+ auth_value_method :oauth_jwt_legacy_algorithm, nil
30
+
25
31
  auth_value_method :oauth_jwt_jwe_copyright, nil
26
32
  auth_value_method :oauth_jwt_audience, nil
27
33
 
@@ -35,7 +41,13 @@ module Rodauth
35
41
  :last_account_login_at
36
42
  )
37
43
 
38
- JWKS = OAuth::TtlStore.new
44
+ route(:jwks) do |r|
45
+ next unless is_authorization_server?
46
+
47
+ r.get do
48
+ json_response_success({ keys: jwks_set }, true)
49
+ end
50
+ end
39
51
 
40
52
  def require_oauth_authorization(*scopes)
41
53
  authorization_required unless authorization_token
@@ -88,9 +100,7 @@ module Rodauth
88
100
  jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
89
101
  jwk = oauth_application[oauth_application_jws_jwk_column]
90
102
 
91
- if jwk
92
- jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
93
- end
103
+ jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
94
104
  else
95
105
  redirect_response_error("invalid_request_object")
96
106
  end
@@ -105,8 +115,8 @@ module Rodauth
105
115
  # [RFC7519] specification. The value of "aud" should be the value of
106
116
  # the Authorization Server (AS) "issuer" as defined in RFC8414
107
117
  # [RFC8414].
108
- claims.delete(:iss)
109
- audience = claims.delete(:aud)
118
+ claims.delete("iss")
119
+ audience = claims.delete("aud")
110
120
 
111
121
  redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
112
122
 
@@ -119,11 +129,17 @@ module Rodauth
119
129
 
120
130
  # /token
121
131
 
122
- def before_token
132
+ def require_oauth_application
123
133
  # requset authentication optional for assertions
124
- return if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
134
+ return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
125
135
 
126
- super
136
+ claims = jwt_decode(param("assertion"))
137
+
138
+ redirect_response_error("invalid_grant") unless claims
139
+
140
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
141
+
142
+ authorization_required unless @oauth_application
127
143
  end
128
144
 
129
145
  def validate_oauth_token_params
@@ -145,10 +161,6 @@ module Rodauth
145
161
  def create_oauth_token_from_assertion
146
162
  claims = jwt_decode(param("assertion"))
147
163
 
148
- redirect_response_error("invalid_grant") unless claims
149
-
150
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
151
-
152
164
  account = account_ds(claims["sub"]).first
153
165
 
154
166
  redirect_response_error("invalid_client") unless oauth_application && account
@@ -164,20 +176,22 @@ module Rodauth
164
176
 
165
177
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
166
178
  create_params = {
167
- oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
179
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
168
180
  }.merge(params)
169
181
 
170
- if should_generate_refresh_token
171
- refresh_token = oauth_unique_id_generator
182
+ oauth_token = rescue_from_uniqueness_error do
183
+ if should_generate_refresh_token
184
+ refresh_token = oauth_unique_id_generator
172
185
 
173
- if oauth_tokens_refresh_token_hash_column
174
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
175
- else
176
- create_params[oauth_tokens_refresh_token_column] = refresh_token
186
+ if oauth_tokens_refresh_token_hash_column
187
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
188
+ else
189
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
190
+ end
177
191
  end
178
- end
179
192
 
180
- oauth_token = _generate_oauth_token(create_params)
193
+ _generate_oauth_token(create_params)
194
+ end
181
195
 
182
196
  claims = jwt_claims(oauth_token)
183
197
 
@@ -192,7 +206,7 @@ module Rodauth
192
206
  end
193
207
 
194
208
  def jwt_claims(oauth_token)
195
- issued_at = Time.now.utc.to_i
209
+ issued_at = Time.now.to_i
196
210
 
197
211
  claims = {
198
212
  iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
@@ -213,7 +227,7 @@ module Rodauth
213
227
  aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
214
228
  }
215
229
 
216
- claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
230
+ claims[:auth_time] = last_account_login_at.to_i if last_account_login_at
217
231
 
218
232
  claims
219
233
  end
@@ -231,7 +245,7 @@ module Rodauth
231
245
  end
232
246
  end
233
247
 
234
- def oauth_token_by_token(token, *)
248
+ def oauth_token_by_token(token)
235
249
  jwt_decode(token)
236
250
  end
237
251
 
@@ -293,10 +307,10 @@ module Rodauth
293
307
 
294
308
  # time-to-live
295
309
  ttl = if response.key?("cache-control")
296
- cache_control = response["cache_control"]
297
- cache_control[/max-age=(\d+)/, 1]
310
+ cache_control = response["cache-control"]
311
+ cache_control[/max-age=(\d+)/, 1].to_i
298
312
  elsif response.key?("expires")
299
- Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
313
+ Time.parse(response["expires"]).to_i - Time.now.to_i
300
314
  end
301
315
 
302
316
  [JSON.parse(response.body, symbolize_names: true), ttl]
@@ -304,7 +318,6 @@ module Rodauth
304
318
  end
305
319
 
306
320
  if defined?(JSON::JWT)
307
- # :nocov:
308
321
 
309
322
  def jwk_import(data)
310
323
  JSON::JWK.new(data)
@@ -330,23 +343,27 @@ module Rodauth
330
343
  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
331
344
  token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
332
345
 
333
- @jwt_token = if jws_key
334
- JSON::JWT.decode(token, jws_key)
335
- elsif !is_authorization_server? && auth_server_jwks_set
336
- JSON::JWT.decode(token, JSON::JWK::Set.new(auth_server_jwks_set))
337
- end
346
+ if is_authorization_server?
347
+ if oauth_jwt_legacy_public_key
348
+ JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
349
+ elsif jws_key
350
+ JSON::JWT.decode(token, jws_key)
351
+ end
352
+ elsif (jwks = auth_server_jwks_set)
353
+ JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
354
+ end
338
355
  rescue JSON::JWT::Exception
339
356
  nil
340
357
  end
341
358
 
342
359
  def jwks_set
343
- [
360
+ @jwks_set ||= [
344
361
  (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
362
+ (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
345
363
  (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
346
364
  ].compact
347
365
  end
348
366
 
349
- # :nocov:
350
367
  elsif defined?(JWT)
351
368
 
352
369
  # ruby-jwt
@@ -391,21 +408,30 @@ module Rodauth
391
408
  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
392
409
  # decrypt jwe
393
410
  token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
394
-
395
411
  # decode jwt
396
- @jwt_token = if jws_key
397
- JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
398
- elsif !is_authorization_server? && auth_server_jwks_set
399
- algorithms = auth_server_jwks_set[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
400
- JWT.decode(token, nil, true, jwks: auth_server_jwks_set, algorithms: algorithms).first
401
- end
412
+ if is_authorization_server?
413
+ if oauth_jwt_legacy_public_key
414
+ algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
415
+ JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first
416
+ elsif jws_key
417
+ JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
418
+ end
419
+ elsif (jwks = auth_server_jwks_set)
420
+ algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
421
+ JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first
422
+ end
402
423
  rescue JWT::DecodeError, JWT::JWKError
403
424
  nil
404
425
  end
405
426
 
406
427
  def jwks_set
407
- [
428
+ @jwks_set ||= [
408
429
  (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
430
+ (
431
+ if oauth_jwt_legacy_public_key
432
+ JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
433
+ end
434
+ ),
409
435
  (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
410
436
  ].compact
411
437
  end
@@ -436,13 +462,5 @@ module Rodauth
436
462
 
437
463
  super
438
464
  end
439
-
440
- route(:jwks) do |r|
441
- next unless is_authorization_server?
442
-
443
- r.get do
444
- json_response_success({ keys: jwks_set })
445
- end
446
- end
447
465
  end
448
466
  end
@@ -0,0 +1,104 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "onelogin/ruby-saml"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_saml) do
7
+ depends :oauth
8
+
9
+ auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
10
+ auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
11
+ auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
12
+
13
+ auth_value_method :oauth_saml_security_authn_requests_signed, false
14
+ auth_value_method :oauth_saml_security_metadata_signed, false
15
+ auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
16
+ auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
17
+
18
+ SAML_GRANT_TYPE = "http://oauth.net/grant_type/assertion/saml/2.0/bearer"
19
+
20
+ # /token
21
+
22
+ def require_oauth_application
23
+ # requset authentication optional for assertions
24
+ return super unless param("grant_type") == SAML_GRANT_TYPE && !param_or_nil("client_id")
25
+
26
+ # TODO: invalid grant
27
+ authorization_required unless saml_assertion
28
+
29
+ redirect_uri = saml_assertion.destination
30
+
31
+ @oauth_application = db[oauth_applications_table].where(
32
+ oauth_applications_homepage_url_column => saml_assertion.audiences,
33
+ oauth_applications_redirect_uri_column => redirect_uri
34
+ ).first
35
+
36
+ # The Assertion's <Issuer> element MUST contain a unique identifier
37
+ # for the entity that issued the Assertion.
38
+ authorization_required unless saml_assertion.issuers.all? do |issuer|
39
+ issuer.start_with?(@oauth_application[oauth_applications_homepage_url_column])
40
+ end
41
+
42
+ authorization_required unless @oauth_application
43
+ end
44
+
45
+ private
46
+
47
+ def secret_matches?(oauth_application, secret)
48
+ return super unless param_or_nil("assertion")
49
+
50
+ true
51
+ end
52
+
53
+ def saml_assertion
54
+ return @saml_assertion if defined?(@saml_assertion)
55
+
56
+ @saml_assertion = begin
57
+ settings = OneLogin::RubySaml::Settings.new
58
+ settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
59
+ settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
60
+ settings.name_identifier_format = oauth_saml_name_identifier_format
61
+ settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
62
+ settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
63
+ settings.security[:digest_method] = oauth_saml_security_digest_method
64
+ settings.security[:signature_method] = oauth_saml_security_signature_method
65
+
66
+ response = OneLogin::RubySaml::Response.new(param("assertion"), settings: settings, skip_recipient_check: true)
67
+
68
+ return unless response.is_valid?
69
+
70
+ response
71
+ end
72
+ end
73
+
74
+ def validate_oauth_token_params
75
+ return super unless param("grant_type") == SAML_GRANT_TYPE
76
+
77
+ redirect_response_error("invalid_client") unless param_or_nil("assertion")
78
+
79
+ redirect_response_error("invalid_scope") unless check_valid_scopes?
80
+ end
81
+
82
+ def create_oauth_token
83
+ if param("grant_type") == SAML_GRANT_TYPE
84
+ create_oauth_token_from_saml_assertion
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def create_oauth_token_from_saml_assertion
91
+ account = db[accounts_table].where(login_column => saml_assertion.nameid).first
92
+
93
+ redirect_response_error("invalid_client") unless oauth_application && account
94
+
95
+ create_params = {
96
+ oauth_tokens_account_id_column => account[account_id_column],
97
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
98
+ oauth_tokens_scopes_column => (param_or_nil("scope") || oauth_application[oauth_applications_scopes_column])
99
+ }
100
+
101
+ generate_oauth_token(create_params, false)
102
+ end
103
+ end
104
+ end
@@ -2,14 +2,63 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:oidc) do
5
+ # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
5
6
  OIDC_SCOPES_MAP = {
6
7
  "profile" => %i[name family_name given_name middle_name nickname preferred_username
7
8
  profile picture website gender birthdate zoneinfo locale updated_at].freeze,
8
9
  "email" => %i[email email_verified].freeze,
9
- "address" => %i[address].freeze,
10
+ "address" => %i[formatted street_address locality region postal_code country].freeze,
10
11
  "phone" => %i[phone_number phone_number_verified].freeze
11
12
  }.freeze
12
13
 
14
+ VALID_METADATA_KEYS = %i[
15
+ issuer
16
+ authorization_endpoint
17
+ token_endpoint
18
+ userinfo_endpoint
19
+ jwks_uri
20
+ registration_endpoint
21
+ scopes_supported
22
+ response_types_supported
23
+ response_modes_supported
24
+ grant_types_supported
25
+ acr_values_supported
26
+ subject_types_supported
27
+ id_token_signing_alg_values_supported
28
+ id_token_encryption_alg_values_supported
29
+ id_token_encryption_enc_values_supported
30
+ userinfo_signing_alg_values_supported
31
+ userinfo_encryption_alg_values_supported
32
+ userinfo_encryption_enc_values_supported
33
+ request_object_signing_alg_values_supported
34
+ request_object_encryption_alg_values_supported
35
+ request_object_encryption_enc_values_supported
36
+ token_endpoint_auth_methods_supported
37
+ token_endpoint_auth_signing_alg_values_supported
38
+ display_values_supported
39
+ claim_types_supported
40
+ claims_supported
41
+ service_documentation
42
+ claims_locales_supported
43
+ ui_locales_supported
44
+ claims_parameter_supported
45
+ request_parameter_supported
46
+ request_uri_parameter_supported
47
+ require_request_uri_registration
48
+ op_policy_uri
49
+ op_tos_uri
50
+ ].freeze
51
+
52
+ REQUIRED_METADATA_KEYS = %i[
53
+ issuer
54
+ authorization_endpoint
55
+ token_endpoint
56
+ jwks_uri
57
+ response_types_supported
58
+ subject_types_supported
59
+ id_token_signing_alg_values_supported
60
+ ].freeze
61
+
13
62
  depends :oauth_jwt
14
63
 
15
64
  auth_value_method :oauth_application_default_scope, "openid"
@@ -22,12 +71,47 @@ module Rodauth
22
71
 
23
72
  auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
24
73
 
25
- auth_value_methods(:get_oidc_param)
74
+ auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
75
+ auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
76
+ auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
77
+
78
+ auth_value_methods(:get_oidc_param, :get_additional_param)
79
+
80
+ # /userinfo
81
+ route(:userinfo) do |r|
82
+ next unless is_authorization_server?
83
+
84
+ r.on method: %i[get post] do
85
+ catch_error do
86
+ oauth_token = authorization_token
87
+
88
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
89
+
90
+ oauth_scopes = oauth_token["scope"].split(" ")
91
+
92
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
93
+
94
+ account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
95
+
96
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
97
+
98
+ oauth_scopes.delete("openid")
99
+
100
+ oidc_claims = { "sub" => oauth_token["sub"] }
101
+
102
+ fill_with_account_claims(oidc_claims, account, oauth_scopes)
103
+
104
+ json_response_success(oidc_claims)
105
+ end
106
+
107
+ throw_json_response_error(authorization_required_error_status, "invalid_token")
108
+ end
109
+ end
26
110
 
27
111
  def openid_configuration(issuer = nil)
28
112
  request.on(".well-known/openid-configuration") do
29
113
  request.get do
30
- json_response_success(openid_configuration_body(issuer))
114
+ json_response_success(openid_configuration_body(issuer), cache: true)
31
115
  end
32
116
  end
33
117
  end
@@ -57,6 +141,68 @@ module Rodauth
57
141
 
58
142
  private
59
143
 
144
+ def require_authorizable_account
145
+ try_prompt if param_or_nil("prompt")
146
+ super
147
+ end
148
+
149
+ # this executes before checking for a logged in account
150
+ def try_prompt
151
+ prompt = param_or_nil("prompt")
152
+
153
+ case prompt
154
+ when "none"
155
+ redirect_response_error("login_required") unless logged_in?
156
+
157
+ require_account
158
+
159
+ if db[oauth_grants_table].where(
160
+ oauth_grants_account_id_column => account_id,
161
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
162
+ oauth_grants_redirect_uri_column => redirect_uri,
163
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
164
+ oauth_grants_access_type_column => "online"
165
+ ).count.zero?
166
+ redirect_response_error("consent_required")
167
+ end
168
+
169
+ request.env["REQUEST_METHOD"] = "POST"
170
+ when "login"
171
+ if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
172
+ ::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
173
+ return
174
+ end
175
+
176
+ # logging out
177
+ clear_session
178
+ set_session_value(login_redirect_session_key, request.fullpath)
179
+
180
+ login_cookie_opts = Hash[oauth_prompt_login_cookie_options]
181
+ login_cookie_opts[:value] = "login"
182
+ login_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_prompt_login_interval) # 15 minutes
183
+ ::Rack::Utils.set_cookie_header!(response.headers, oauth_prompt_login_cookie_key, login_cookie_opts)
184
+
185
+ redirect require_login_redirect
186
+ when "consent"
187
+ require_account
188
+
189
+ if db[oauth_grants_table].where(
190
+ oauth_grants_account_id_column => account_id,
191
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
192
+ oauth_grants_redirect_uri_column => redirect_uri,
193
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
194
+ oauth_grants_access_type_column => "online"
195
+ ).count.zero?
196
+ redirect_response_error("consent_required")
197
+ end
198
+ when "select-account"
199
+ # obly works if select_account plugin is available
200
+ require_select_account if respond_to?(:require_select_account)
201
+ else
202
+ redirect_response_error("invalid_request")
203
+ end
204
+ end
205
+
60
206
  def create_oauth_grant(create_params = {})
61
207
  return super unless (nonce = param_or_nil("nonce"))
62
208
 
@@ -100,8 +246,11 @@ module Rodauth
100
246
  oauth_token[:id_token] = jwt_encode(id_token_claims)
101
247
  end
102
248
 
249
+ # aka fill_with_standard_claims
103
250
  def fill_with_account_claims(claims, account, scopes)
104
- scopes_by_oidc = scopes.each_with_object({}) do |scope, by_oidc|
251
+ scopes_by_claim = scopes.each_with_object({}) do |scope, by_oidc|
252
+ next if scope == "openid"
253
+
105
254
  oidc, param = scope.split(".", 2)
106
255
 
107
256
  by_oidc[oidc] ||= []
@@ -109,21 +258,33 @@ module Rodauth
109
258
  by_oidc[oidc] << param.to_sym if param
110
259
  end
111
260
 
112
- oidc_scopes = (OIDC_SCOPES_MAP.keys & scopes_by_oidc.keys)
113
-
114
- return if oidc_scopes.empty?
261
+ oidc_scopes, additional_scopes = scopes_by_claim.keys.partition { |key| OIDC_SCOPES_MAP.key?(key) }
115
262
 
116
- if respond_to?(:get_oidc_param)
117
- oidc_scopes.each do |scope|
118
- params = scopes_by_oidc[scope]
119
- params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
263
+ unless oidc_scopes.empty?
264
+ if respond_to?(:get_oidc_param)
265
+ oidc_scopes.each do |scope|
266
+ scope_claims = claims
267
+ params = scopes_by_claim[scope]
268
+ params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
120
269
 
121
- params.each do |param|
122
- claims[param] = __send__(:get_oidc_param, account, param)
270
+ scope_claims = (claims["address"] = {}) if scope == "address"
271
+ params.each do |param|
272
+ scope_claims[param] = __send__(:get_oidc_param, account, param)
273
+ end
123
274
  end
275
+ else
276
+ warn "`get_oidc_param(account, claim)` must be implemented to use oidc scopes."
277
+ end
278
+ end
279
+
280
+ return if additional_scopes.empty?
281
+
282
+ if respond_to?(:get_additional_param)
283
+ additional_scopes.each do |scope|
284
+ claims[scope] = __send__(:get_additional_param, account, scope.to_sym)
124
285
  end
125
286
  else
126
- warn "`get_oidc_param(token, param)` must be implemented to use oidc scopes."
287
+ warn "`get_additional_param(account, claim)` must be implemented to use oidc scopes."
127
288
  end
128
289
  end
129
290
 
@@ -145,33 +306,27 @@ module Rodauth
145
306
  end
146
307
  end
147
308
 
148
- def do_authorize(redirect_url, query_params = [], fragment_params = [])
309
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
149
310
  return super unless use_oauth_implicit_grant_type?
150
311
 
151
312
  case param("response_type")
152
313
  when "id_token"
153
- fragment_params.replace(_do_authorize_id_token.map { |k, v| "#{k}=#{v}" })
314
+ response_params.replace(_do_authorize_id_token)
154
315
  when "code token"
155
316
  redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
156
317
 
157
- params = _do_authorize_code.merge(_do_authorize_token)
158
-
159
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
318
+ response_params.replace(_do_authorize_code.merge(_do_authorize_token))
160
319
  when "code id_token"
161
- params = _do_authorize_code.merge(_do_authorize_id_token)
162
-
163
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
320
+ response_params.replace(_do_authorize_code.merge(_do_authorize_id_token))
164
321
  when "id_token token"
165
- params = _do_authorize_id_token.merge(_do_authorize_token)
166
-
167
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
322
+ response_params.replace(_do_authorize_id_token.merge(_do_authorize_token))
168
323
  when "code id_token token"
169
- params = _do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token)
170
324
 
171
- fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
325
+ response_params.replace(_do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token))
172
326
  end
327
+ response_mode ||= "fragment" unless response_params.empty?
173
328
 
174
- super(redirect_url, query_params, fragment_params)
329
+ super(response_params, response_mode)
175
330
  end
176
331
 
177
332
  def _do_authorize_id_token
@@ -190,7 +345,9 @@ module Rodauth
190
345
  # Metadata
191
346
 
192
347
  def openid_configuration_body(path)
193
- metadata = oauth_server_metadata_body(path)
348
+ metadata = oauth_server_metadata_body(path).select do |k, _|
349
+ VALID_METADATA_KEYS.include?(k)
350
+ end
194
351
 
195
352
  scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
196
353
  oidc, param = scope.split(".", 2)
@@ -204,63 +361,38 @@ module Rodauth
204
361
 
205
362
  scope_claims.unshift("auth_time") if last_account_login_at
206
363
 
207
- metadata.merge({
208
- userinfo_endpoint: userinfo_url,
209
- response_types_supported: metadata[:response_types_supported] +
210
- ["none", "id_token", %w[code token], %w[code id_token], %w[id_token token], %w[code id_token token]],
211
- response_modes_supported: %w[query fragment],
212
- grant_types_supported: %w[authorization_code implicit],
213
-
214
- subject_types_supported: [oauth_jwt_subject_type],
215
-
216
- id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
217
- id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
218
- id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
219
-
220
- userinfo_signing_alg_values_supported: [],
221
- userinfo_encryption_alg_values_supported: [],
222
- userinfo_encryption_enc_values_supported: [],
223
-
224
- request_object_signing_alg_values_supported: [],
225
- request_object_encryption_alg_values_supported: [],
226
- request_object_encryption_enc_values_supported: [],
227
-
228
- # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
229
- # Values defined by this specification are normal, aggregated, and distributed.
230
- # If omitted, the implementation supports only normal Claims.
231
- claim_types_supported: %w[normal],
232
- claims_supported: %w[sub iss iat exp aud] | scope_claims
233
- })
234
- end
235
-
236
- # /userinfo
237
- route(:userinfo) do |r|
238
- next unless is_authorization_server?
239
-
240
- r.on method: %i[get post] do
241
- catch_error do
242
- oauth_token = authorization_token
243
-
244
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
245
-
246
- oauth_scopes = oauth_token["scope"].split(" ")
247
-
248
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
249
-
250
- account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
251
-
252
- throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
253
-
254
- oauth_scopes.delete("openid")
255
-
256
- oidc_claims = { "sub" => oauth_token["sub"] }
257
-
258
- fill_with_account_claims(oidc_claims, account, oauth_scopes)
259
-
260
- json_response_success(oidc_claims)
261
- end
364
+ response_types_supported = metadata[:response_types_supported]
365
+ if use_oauth_implicit_grant_type?
366
+ response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
367
+ end
262
368
 
263
- throw_json_response_error(authorization_required_error_status, "invalid_token")
369
+ metadata.merge(
370
+ userinfo_endpoint: userinfo_url,
371
+ response_types_supported: response_types_supported,
372
+ subject_types_supported: [oauth_jwt_subject_type],
373
+
374
+ id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
375
+ id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
376
+ id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
377
+
378
+ userinfo_signing_alg_values_supported: [],
379
+ userinfo_encryption_alg_values_supported: [],
380
+ userinfo_encryption_enc_values_supported: [],
381
+
382
+ request_object_signing_alg_values_supported: [],
383
+ request_object_encryption_alg_values_supported: [],
384
+ request_object_encryption_enc_values_supported: [],
385
+
386
+ # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
387
+ # Values defined by this specification are normal, aggregated, and distributed.
388
+ # If omitted, the implementation supports only normal Claims.
389
+ claim_types_supported: %w[normal],
390
+ claims_supported: %w[sub iss iat exp aud] | scope_claims
391
+ ).reject do |key, val|
392
+ # Filter null values in optional items
393
+ (!REQUIRED_METADATA_KEYS.include?(key.to_sym) && val.nil?) ||
394
+ # Claims with zero elements MUST be omitted from the response
395
+ (val.respond_to?(:empty?) && val.empty?)
264
396
  end
265
397
  end
266
398
  end