rodauth-oauth 0.1.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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