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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -0
- data/README.md +2 -1
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -4
- data/lib/rodauth/features/oauth.rb +461 -355
- data/lib/rodauth/features/oauth_http_mac.rb +6 -12
- data/lib/rodauth/features/oauth_jwt.rb +70 -52
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/features/oidc.rb +217 -85
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/ttl_store.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +34 -0
- data/templates/client_secret_field.str +4 -0
- data/templates/description_field.str +4 -0
- data/templates/homepage_url_field.str +4 -0
- data/templates/name_field.str +4 -0
- data/templates/new_oauth_application.str +10 -0
- data/templates/oauth_application.str +11 -0
- data/templates/oauth_applications.str +14 -0
- data/templates/oauth_tokens.str +49 -0
- data/templates/redirect_uri_field.str +4 -0
- data/templates/scope_field.str +10 -0
- metadata +20 -7
@@ -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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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(
|
109
|
-
audience = claims.delete(
|
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
|
132
|
+
def require_oauth_application
|
123
133
|
# requset authentication optional for assertions
|
124
|
-
return
|
134
|
+
return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
125
135
|
|
126
|
-
|
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 =>
|
179
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
168
180
|
}.merge(params)
|
169
181
|
|
170
|
-
|
171
|
-
|
182
|
+
oauth_token = rescue_from_uniqueness_error do
|
183
|
+
if should_generate_refresh_token
|
184
|
+
refresh_token = oauth_unique_id_generator
|
172
185
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
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.
|
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.
|
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["
|
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.
|
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
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
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[
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
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 "`
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
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
|