rodauth-oauth 0.4.3 → 0.6.1
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 +49 -6
- data/README.md +10 -5
- data/lib/generators/{roda → rodauth}/oauth/install_generator.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_application.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_grant.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_token.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/db/migrate/create_rodauth_oauth.rb +1 -1
- data/lib/generators/{roda → rodauth}/oauth/views_generator.rb +1 -1
- data/lib/rodauth/features/oauth.rb +29 -17
- data/lib/rodauth/features/oauth_http_mac.rb +1 -1
- data/lib/rodauth/features/oauth_jwt.rb +111 -32
- data/lib/rodauth/features/oauth_saml.rb +1 -1
- data/lib/rodauth/features/oidc.rb +101 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0c72cd872103e1d10929ad5934312a123a42b3c9cb55c06c118fbcb0d83f4a7
|
4
|
+
data.tar.gz: 57bbcef2981c20627cfc9239b30781af03898e34b5d2861b84a820d778e1dac3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4c48e1ce93074c5dff85f506c8c5b8c7f024409f9c58ae942bb2adb5241303586cb0930a1c849566c793d6d9e508a73b0f5fc5772a82e3c87852415997e7889
|
7
|
+
data.tar.gz: 54e5777b2506ea99f830cd3d9b66ca1755372681cd19013638bc25680b0ce601275fb5fd732b123fcefddb7f56ba0ac1fc6a069f028c377be5a7408d92debb9e
|
data/CHANGELOG.md
CHANGED
@@ -2,40 +2,83 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.6.1 (08/09/2021)
|
6
|
+
|
7
|
+
#### Bugfixes
|
8
|
+
|
9
|
+
* Fixed rails view templates escaping.
|
10
|
+
* Fixed declaration of authorize template in the generator.
|
11
|
+
|
12
|
+
### 0.6.0 (21/05/2021)
|
13
|
+
|
14
|
+
### Improvements
|
15
|
+
|
16
|
+
* RBS signatures
|
17
|
+
|
18
|
+
### Chore
|
19
|
+
|
20
|
+
* Ruby 3 and Truffleruby are now officially supported and tested in CI.
|
21
|
+
|
22
|
+
### 0.5.1 (19/03/2021)
|
23
|
+
|
24
|
+
#### Improvements
|
25
|
+
|
26
|
+
* Changing "Callback URL" to "Redirect URL" in default templates;
|
27
|
+
|
28
|
+
#### Bugfixes
|
29
|
+
|
30
|
+
* (rails integration) Fixed templates location;
|
31
|
+
* (rails integration) Fixed migration name from generator;
|
32
|
+
* (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
|
33
|
+
* `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
|
34
|
+
* (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
|
35
|
+
|
36
|
+
### 0.5.0 (08/02/2021)
|
37
|
+
|
38
|
+
#### RP-Initiated Logout
|
39
|
+
|
40
|
+
The `:oidc` plugin can now do [RP-Initiated Logout](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/RP-Initiated-Logout). It's disabled by default, so read the docs to learn how to enable it.
|
41
|
+
|
42
|
+
#### Security
|
43
|
+
|
44
|
+
The `:oauth_jwt` (and by association, `:oidc`) plugin(s) verifies the claims of used JWT tokens. This is a **very important security fix**, as without it, there is no protection against replay attacks and other types of misuse of the JWT token.
|
45
|
+
|
46
|
+
A new auth method, `generate_jti(claims)`, was [added to the list of oauth_jwt plugin options](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/JWT-Access-Tokens#rodauth-options). By default, it'll hash the `aud` and `iat` claims together, but you can overwrite how this is done.
|
47
|
+
|
5
48
|
### 0.4.3 (09/12/2020)
|
6
49
|
|
7
50
|
* Introspection requests made to an Authorization Server in "resource server" mode are not correctly encoding the body using the "application/x-www-form-urlencoded" format.
|
8
51
|
|
9
52
|
### 0.4.2 (24/11/2020)
|
10
53
|
|
11
|
-
|
54
|
+
#### Bugfixes
|
12
55
|
|
13
56
|
* database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
|
14
57
|
|
15
58
|
### 0.4.1 (24/11/2020)
|
16
59
|
|
17
|
-
|
60
|
+
#### Improvements
|
18
61
|
|
19
62
|
When in "Resource Server" mode, calling `rodauth.authorization_token` will now return an hash of the JSON payload that the Authorization Server responds, and which was already previously used to authorize access to protected resources.
|
20
63
|
|
21
|
-
|
64
|
+
#### Bugfixes
|
22
65
|
|
23
66
|
* An error occurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
|
24
67
|
|
25
68
|
### 0.4.0 (13/11/2020)
|
26
69
|
|
27
|
-
|
70
|
+
#### Features
|
28
71
|
|
29
72
|
* A new method, `get_additional_param(account, claim)`, is now exposed; this method will be called whenever non-OIDC scopes are requested in the emission of the ID token.
|
30
73
|
|
31
74
|
* The `form_post` response is now supported, either by passing the `response_mode=form_post` request param in the authorization URL, or by setting `oauth_response_mode "form_post"` option. This improves the overall security of an Authorization server even more, as authorization codes are sent to client applications via a POST request to the redirect URI.
|
32
75
|
|
33
76
|
|
34
|
-
|
77
|
+
#### Improvements
|
35
78
|
|
36
79
|
* For the OIDC `address` scope, proper claims are now emitted as per the standard, i.e. the "formatted", "street_address", "locality", "region", "postal_code", "country". These will be the ones referenced in the `get_oidc_param` method.
|
37
80
|
|
38
|
-
|
81
|
+
#### Bugfixes
|
39
82
|
|
40
83
|
* The rails templates were missing declarations from a few params, which made some of the flows (the PKCE for example) not work out-of-the box;
|
41
84
|
* rails tests were silently not running in CI;
|
data/README.md
CHANGED
@@ -25,7 +25,12 @@ This gem implements the following RFCs and features of OAuth:
|
|
25
25
|
* [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
26
26
|
* OAuth application and token management dashboards;
|
27
27
|
|
28
|
-
It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides
|
28
|
+
It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides, including:
|
29
|
+
|
30
|
+
* [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
|
31
|
+
* [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
|
32
|
+
* [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
|
33
|
+
* [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
|
29
34
|
|
30
35
|
This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
|
31
36
|
|
@@ -104,7 +109,7 @@ For OpenID, it's very similar to the example above:
|
|
104
109
|
```ruby
|
105
110
|
plugin :rodauth do
|
106
111
|
# enable it in the plugin
|
107
|
-
enable :login, :
|
112
|
+
enable :login, :oidc
|
108
113
|
oauth_application_default_scope %w[openid]
|
109
114
|
oauth_application_scopes %w[openid email profile]
|
110
115
|
end
|
@@ -628,11 +633,11 @@ Although very handy for the mentioned use case, one can't revoke a JWT token on
|
|
628
633
|
|
629
634
|
## Ruby support policy
|
630
635
|
|
631
|
-
The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and
|
636
|
+
The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and truffleruby.
|
632
637
|
|
633
|
-
###
|
638
|
+
### Rails
|
634
639
|
|
635
|
-
If you're interested in using this library
|
640
|
+
If you're interested in using this library with rails, be sure to check `rodauth-rails` policy, as it supports rails 5.2 upwards.
|
636
641
|
|
637
642
|
## Development
|
638
643
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -9,7 +9,7 @@ module Rodauth::OAuth
|
|
9
9
|
source_root "#{__dir__}/templates"
|
10
10
|
namespace "rodauth:oauth:views"
|
11
11
|
|
12
|
-
DEFAULT = %w[
|
12
|
+
DEFAULT = %w[authorize].freeze
|
13
13
|
VIEWS = {
|
14
14
|
oauth_authorize: DEFAULT,
|
15
15
|
oauth_applications: %w[oauth_applications oauth_application new_oauth_application]
|
@@ -9,7 +9,7 @@ require "rodauth/oauth/ttl_store"
|
|
9
9
|
require "rodauth/oauth/database_extensions"
|
10
10
|
|
11
11
|
module Rodauth
|
12
|
-
Feature.define(:oauth) do
|
12
|
+
Feature.define(:oauth, :Oauth) do
|
13
13
|
# RUBY EXTENSIONS
|
14
14
|
unless Regexp.method_defined?(:match?)
|
15
15
|
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
@@ -139,7 +139,15 @@ module Rodauth
|
|
139
139
|
auth_value_method :already_in_use_response_status, 409
|
140
140
|
|
141
141
|
# OAuth Applications
|
142
|
-
auth_value_method :
|
142
|
+
auth_value_method :oauth_applications_route, "oauth-applications"
|
143
|
+
def oauth_applications_path(opts = {})
|
144
|
+
route_path(oauth_applications_route, opts)
|
145
|
+
end
|
146
|
+
|
147
|
+
def oauth_applications_url(opts = {})
|
148
|
+
route_url(oauth_applications_route, opts)
|
149
|
+
end
|
150
|
+
|
143
151
|
auth_value_method :oauth_applications_table, :oauth_applications
|
144
152
|
|
145
153
|
auth_value_method :oauth_applications_id_column, :id
|
@@ -192,6 +200,7 @@ module Rodauth
|
|
192
200
|
auth_value_method :oauth_unique_id_generation_retries, 3
|
193
201
|
|
194
202
|
auth_value_methods(
|
203
|
+
:oauth_application_path,
|
195
204
|
:fetch_access_token,
|
196
205
|
:oauth_unique_id_generator,
|
197
206
|
:secret_matches?,
|
@@ -363,9 +372,13 @@ module Rodauth
|
|
363
372
|
end
|
364
373
|
end
|
365
374
|
|
375
|
+
def oauth_application_path(id)
|
376
|
+
"#{oauth_applications_path}/#{id}"
|
377
|
+
end
|
378
|
+
|
366
379
|
# /oauth-applications routes
|
367
380
|
def oauth_applications
|
368
|
-
request.on(
|
381
|
+
request.on(oauth_applications_route) do
|
369
382
|
require_account
|
370
383
|
|
371
384
|
request.get "new" do
|
@@ -422,16 +435,20 @@ module Rodauth
|
|
422
435
|
false
|
423
436
|
when revoke_path
|
424
437
|
!json_request?
|
425
|
-
when authorize_path,
|
438
|
+
when authorize_path, oauth_applications_path
|
426
439
|
only_json? ? false : super
|
427
440
|
else
|
428
441
|
super
|
429
442
|
end
|
430
443
|
end
|
431
444
|
|
432
|
-
# Overrides
|
433
|
-
def
|
434
|
-
super ||
|
445
|
+
# Overrides session_value, so that a valid authorization token also authenticates a request
|
446
|
+
def session_value
|
447
|
+
super || begin
|
448
|
+
return unless authorization_token
|
449
|
+
|
450
|
+
authorization_token[oauth_tokens_account_id_column]
|
451
|
+
end
|
435
452
|
end
|
436
453
|
|
437
454
|
def accepts_json?
|
@@ -449,10 +466,6 @@ module Rodauth
|
|
449
466
|
end
|
450
467
|
end
|
451
468
|
|
452
|
-
def initialize(scope)
|
453
|
-
@scope = scope
|
454
|
-
end
|
455
|
-
|
456
469
|
def scopes
|
457
470
|
scope = request.params["scope"]
|
458
471
|
case scope
|
@@ -551,12 +564,11 @@ module Rodauth
|
|
551
564
|
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
552
565
|
|
553
566
|
# Check whether we can reutilize db entries for the same account / application pair
|
554
|
-
one_oauth_token_per_account =
|
555
|
-
|
556
|
-
definition[:
|
557
|
-
definition[:columns] == oauth_tokens_unique_columns
|
558
|
-
end
|
567
|
+
one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
|
568
|
+
definition[:unique] &&
|
569
|
+
definition[:columns] == oauth_tokens_unique_columns
|
559
570
|
end
|
571
|
+
|
560
572
|
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
561
573
|
end
|
562
574
|
|
@@ -1350,7 +1362,7 @@ module Rodauth
|
|
1350
1362
|
issuer: issuer,
|
1351
1363
|
authorization_endpoint: authorize_url,
|
1352
1364
|
token_endpoint: token_url,
|
1353
|
-
registration_endpoint:
|
1365
|
+
registration_endpoint: oauth_applications_url,
|
1354
1366
|
scopes_supported: oauth_application_scopes,
|
1355
1367
|
response_types_supported: responses_supported,
|
1356
1368
|
response_modes_supported: response_modes_supported,
|
@@ -3,11 +3,13 @@
|
|
3
3
|
require "rodauth/oauth/ttl_store"
|
4
4
|
|
5
5
|
module Rodauth
|
6
|
-
Feature.define(:oauth_jwt) do
|
6
|
+
Feature.define(:oauth_jwt, :OauthJwt) do
|
7
7
|
depends :oauth
|
8
8
|
|
9
9
|
JWKS = OAuth::TtlStore.new
|
10
10
|
|
11
|
+
# Recommended to have hmac_secret as well
|
12
|
+
|
11
13
|
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
12
14
|
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
13
15
|
|
@@ -38,7 +40,8 @@ module Rodauth
|
|
38
40
|
:jwt_encode,
|
39
41
|
:jwt_decode,
|
40
42
|
:jwks_set,
|
41
|
-
:last_account_login_at
|
43
|
+
:last_account_login_at,
|
44
|
+
:generate_jti
|
42
45
|
)
|
43
46
|
|
44
47
|
route(:jwks) do |r|
|
@@ -59,6 +62,15 @@ module Rodauth
|
|
59
62
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
60
63
|
end
|
61
64
|
|
65
|
+
# Overrides session_value, so that a valid authorization token also authenticates a request
|
66
|
+
def session_value
|
67
|
+
super || begin
|
68
|
+
return unless authorization_token
|
69
|
+
|
70
|
+
authorization_token["sub"]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
62
74
|
private
|
63
75
|
|
64
76
|
unless method_defined?(:last_account_login_at)
|
@@ -67,6 +79,10 @@ module Rodauth
|
|
67
79
|
end
|
68
80
|
end
|
69
81
|
|
82
|
+
def issuer
|
83
|
+
@issuer ||= oauth_jwt_token_issuer || authorization_server_url
|
84
|
+
end
|
85
|
+
|
70
86
|
def authorization_token
|
71
87
|
return @authorization_token if defined?(@authorization_token)
|
72
88
|
|
@@ -79,7 +95,7 @@ module Rodauth
|
|
79
95
|
|
80
96
|
return unless jwt_token
|
81
97
|
|
82
|
-
return if jwt_token["iss"] !=
|
98
|
+
return if jwt_token["iss"] != issuer ||
|
83
99
|
(oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
|
84
100
|
!jwt_token["sub"]
|
85
101
|
|
@@ -105,7 +121,7 @@ module Rodauth
|
|
105
121
|
redirect_response_error("invalid_request_object")
|
106
122
|
end
|
107
123
|
|
108
|
-
claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
|
124
|
+
claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
|
109
125
|
|
110
126
|
redirect_response_error("invalid_request_object") unless claims
|
111
127
|
|
@@ -118,7 +134,7 @@ module Rodauth
|
|
118
134
|
claims.delete("iss")
|
119
135
|
audience = claims.delete("aud")
|
120
136
|
|
121
|
-
redirect_response_error("invalid_request_object") if audience && audience !=
|
137
|
+
redirect_response_error("invalid_request_object") if audience && audience != issuer
|
122
138
|
|
123
139
|
claims.each do |k, v|
|
124
140
|
request.params[k.to_s] = v
|
@@ -209,7 +225,7 @@ module Rodauth
|
|
209
225
|
issued_at = Time.now.to_i
|
210
226
|
|
211
227
|
claims = {
|
212
|
-
iss:
|
228
|
+
iss: issuer, # issuer
|
213
229
|
iat: issued_at, # issued at
|
214
230
|
#
|
215
231
|
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
|
@@ -317,6 +333,23 @@ module Rodauth
|
|
317
333
|
end
|
318
334
|
end
|
319
335
|
|
336
|
+
def generate_jti(payload)
|
337
|
+
# Use the key and iat to create a unique key per request to prevent replay attacks
|
338
|
+
jti_raw = [
|
339
|
+
payload[:aud] || payload["aud"],
|
340
|
+
payload[:iat] || payload["iat"]
|
341
|
+
].join(":").to_s
|
342
|
+
Digest::SHA256.hexdigest(jti_raw)
|
343
|
+
end
|
344
|
+
|
345
|
+
def verify_jti(jti, claims)
|
346
|
+
generate_jti(claims) == jti
|
347
|
+
end
|
348
|
+
|
349
|
+
def verify_aud(aud, claims)
|
350
|
+
aud == (oauth_jwt_audience || claims["client_id"])
|
351
|
+
end
|
352
|
+
|
320
353
|
if defined?(JSON::JWT)
|
321
354
|
|
322
355
|
def jwk_import(data)
|
@@ -325,6 +358,7 @@ module Rodauth
|
|
325
358
|
|
326
359
|
# json-jwt
|
327
360
|
def jwt_encode(payload)
|
361
|
+
payload[:jti] = generate_jti(payload)
|
328
362
|
jwt = JSON::JWT.new(payload)
|
329
363
|
jwk = JSON::JWK.new(_jwt_key)
|
330
364
|
|
@@ -340,18 +374,34 @@ module Rodauth
|
|
340
374
|
jwt.to_s
|
341
375
|
end
|
342
376
|
|
343
|
-
def jwt_decode(
|
377
|
+
def jwt_decode(
|
378
|
+
token,
|
379
|
+
jws_key: oauth_jwt_public_key || _jwt_key,
|
380
|
+
verify_claims: true,
|
381
|
+
verify_jti: true,
|
382
|
+
**
|
383
|
+
)
|
344
384
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
345
385
|
|
346
|
-
if is_authorization_server?
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
386
|
+
claims = if is_authorization_server?
|
387
|
+
if oauth_jwt_legacy_public_key
|
388
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
389
|
+
elsif jws_key
|
390
|
+
JSON::JWT.decode(token, jws_key)
|
391
|
+
end
|
392
|
+
elsif (jwks = auth_server_jwks_set)
|
393
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
394
|
+
end
|
395
|
+
|
396
|
+
if verify_claims && !(claims[:iss] == issuer &&
|
397
|
+
verify_aud(claims[:aud], claims) &&
|
398
|
+
(!claims[:iat] || Time.at(claims[:iat]) > (Time.now - oauth_token_expires_in)) &&
|
399
|
+
(!claims[:exp] || Time.at(claims[:exp]) > Time.now) &&
|
400
|
+
(!verify_jti || verify_jti(claims[:jti], claims)))
|
401
|
+
return
|
354
402
|
end
|
403
|
+
|
404
|
+
claims
|
355
405
|
rescue JSON::JWT::Exception
|
356
406
|
nil
|
357
407
|
end
|
@@ -384,12 +434,8 @@ module Rodauth
|
|
384
434
|
key = jwk.keypair
|
385
435
|
end
|
386
436
|
|
387
|
-
# Use the key and iat to create a unique key per request to prevent replay attacks
|
388
|
-
jti_raw = [key, payload[:iat]].join(":").to_s
|
389
|
-
jti = Digest::SHA256.hexdigest(jti_raw)
|
390
|
-
|
391
437
|
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
392
|
-
payload[:jti] =
|
438
|
+
payload[:jti] = generate_jti(payload)
|
393
439
|
token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
|
394
440
|
|
395
441
|
if oauth_jwt_jwe_key
|
@@ -405,21 +451,54 @@ module Rodauth
|
|
405
451
|
token
|
406
452
|
end
|
407
453
|
|
408
|
-
def jwt_decode(
|
454
|
+
def jwt_decode(
|
455
|
+
token,
|
456
|
+
jws_key: oauth_jwt_public_key || _jwt_key,
|
457
|
+
jws_algorithm: oauth_jwt_algorithm,
|
458
|
+
verify_claims: true,
|
459
|
+
verify_jti: true
|
460
|
+
)
|
409
461
|
# decrypt jwe
|
410
462
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
463
|
+
|
464
|
+
# verifying the JWT implies verifying:
|
465
|
+
#
|
466
|
+
# issuer: check that server generated the token
|
467
|
+
# aud: check the audience field (client is who he says he is)
|
468
|
+
# iat: check that the token didn't expire
|
469
|
+
#
|
470
|
+
# subject can't be verified automatically without having access to the account id,
|
471
|
+
# which we don't because that's the whole point.
|
472
|
+
#
|
473
|
+
verify_claims_params = if verify_claims
|
474
|
+
{
|
475
|
+
verify_iss: true,
|
476
|
+
iss: issuer,
|
477
|
+
# can't use stock aud verification, as it's dependent on the client application id
|
478
|
+
verify_aud: false,
|
479
|
+
verify_jti: (verify_jti ? method(:verify_jti) : false),
|
480
|
+
verify_iat: true
|
481
|
+
}
|
482
|
+
else
|
483
|
+
{}
|
484
|
+
end
|
485
|
+
|
411
486
|
# decode jwt
|
412
|
-
if is_authorization_server?
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
487
|
+
claims = if is_authorization_server?
|
488
|
+
if oauth_jwt_legacy_public_key
|
489
|
+
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
490
|
+
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
|
491
|
+
elsif jws_key
|
492
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
|
493
|
+
end
|
494
|
+
elsif (jwks = auth_server_jwks_set)
|
495
|
+
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
496
|
+
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
|
497
|
+
end
|
498
|
+
|
499
|
+
return if verify_claims && !verify_aud(claims["aud"], claims)
|
500
|
+
|
501
|
+
claims
|
423
502
|
rescue JWT::DecodeError, JWT::JWKError
|
424
503
|
nil
|
425
504
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
module Rodauth
|
4
|
-
Feature.define(:oidc) do
|
4
|
+
Feature.define(:oidc, :Oidc) do
|
5
5
|
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
6
6
|
OIDC_SCOPES_MAP = {
|
7
7
|
"profile" => %i[name family_name given_name middle_name nickname preferred_username
|
@@ -14,6 +14,7 @@ module Rodauth
|
|
14
14
|
VALID_METADATA_KEYS = %i[
|
15
15
|
issuer
|
16
16
|
authorization_endpoint
|
17
|
+
end_session_endpoint
|
17
18
|
token_endpoint
|
18
19
|
userinfo_endpoint
|
19
20
|
jwks_uri
|
@@ -75,6 +76,10 @@ module Rodauth
|
|
75
76
|
auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
|
76
77
|
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
|
77
78
|
|
79
|
+
# logout
|
80
|
+
auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
|
81
|
+
auth_value_method :use_rp_initiated_logout?, false
|
82
|
+
|
78
83
|
auth_value_methods(:get_oidc_param, :get_additional_param)
|
79
84
|
|
80
85
|
# /userinfo
|
@@ -108,10 +113,81 @@ module Rodauth
|
|
108
113
|
end
|
109
114
|
end
|
110
115
|
|
111
|
-
|
116
|
+
# /oidc-logout
|
117
|
+
route(:oidc_logout) do |r|
|
118
|
+
next unless use_rp_initiated_logout?
|
119
|
+
|
120
|
+
before_oidc_logout_route
|
121
|
+
require_authorizable_account
|
122
|
+
|
123
|
+
# OpenID Providers MUST support the use of the HTTP GET and POST methods
|
124
|
+
r.on method: %i[get post] do
|
125
|
+
catch_error do
|
126
|
+
validate_oidc_logout_params
|
127
|
+
|
128
|
+
#
|
129
|
+
# why this is done:
|
130
|
+
#
|
131
|
+
# we need to decode the id token in order to get the application, because, if the
|
132
|
+
# signing key is application-specific, we don't know how to verify the signature
|
133
|
+
# beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
|
134
|
+
# the @oauth_application, and then decode-and-verify.
|
135
|
+
#
|
136
|
+
oauth_token = jwt_decode(param("id_token_hint"), verify_claims: false)
|
137
|
+
oauth_application_id = oauth_token["client_id"]
|
138
|
+
|
139
|
+
# check whether ID token belongs to currently logged-in user
|
140
|
+
redirect_response_error("invalid_request") unless oauth_token["sub"] == jwt_subject(
|
141
|
+
oauth_tokens_account_id_column => account_id,
|
142
|
+
oauth_tokens_oauth_application_id_column => oauth_application_id
|
143
|
+
)
|
144
|
+
|
145
|
+
# When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
|
146
|
+
redirect_response_error("invalid_request") unless oauth_token && oauth_token["iss"] == issuer
|
147
|
+
|
148
|
+
# now let's logout from IdP
|
149
|
+
transaction do
|
150
|
+
before_logout
|
151
|
+
logout
|
152
|
+
after_logout
|
153
|
+
end
|
154
|
+
|
155
|
+
if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
156
|
+
catch(:default_logout_redirect) do
|
157
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
|
158
|
+
|
159
|
+
throw(:default_logout_redirect) unless oauth_application
|
160
|
+
|
161
|
+
post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uri_column].split(" ")
|
162
|
+
|
163
|
+
throw(:default_logout_redirect) unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
|
164
|
+
|
165
|
+
if (state = param_or_nil("state"))
|
166
|
+
post_logout_redirect_uri = URI(post_logout_redirect_uri)
|
167
|
+
params = ["state=#{state}"]
|
168
|
+
params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
|
169
|
+
post_logout_redirect_uri.query = params.join("&")
|
170
|
+
post_logout_redirect_uri = post_logout_redirect_uri.to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
redirect(post_logout_redirect_uri)
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
# regular logout procedure
|
179
|
+
set_notice_flash(logout_notice_flash)
|
180
|
+
redirect(logout_redirect)
|
181
|
+
end
|
182
|
+
|
183
|
+
redirect_response_error("invalid_request")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def openid_configuration(alt_issuer = nil)
|
112
188
|
request.on(".well-known/openid-configuration") do
|
113
189
|
request.get do
|
114
|
-
json_response_success(openid_configuration_body(
|
190
|
+
json_response_success(openid_configuration_body(alt_issuer), cache: true)
|
115
191
|
end
|
116
192
|
end
|
117
193
|
end
|
@@ -139,6 +215,15 @@ module Rodauth
|
|
139
215
|
end
|
140
216
|
end
|
141
217
|
|
218
|
+
def check_csrf?
|
219
|
+
case request.path
|
220
|
+
when userinfo_path
|
221
|
+
false
|
222
|
+
else
|
223
|
+
super
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
142
227
|
private
|
143
228
|
|
144
229
|
def require_authorizable_account
|
@@ -342,6 +427,18 @@ module Rodauth
|
|
342
427
|
params
|
343
428
|
end
|
344
429
|
|
430
|
+
# Logout
|
431
|
+
|
432
|
+
def validate_oidc_logout_params
|
433
|
+
redirect_response_error("invalid_request") unless param_or_nil("id_token_hint")
|
434
|
+
# check if valid token hint type
|
435
|
+
return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
436
|
+
|
437
|
+
return if check_valid_uri?(redirect_uri)
|
438
|
+
|
439
|
+
redirect_response_error("invalid_request")
|
440
|
+
end
|
441
|
+
|
345
442
|
# Metadata
|
346
443
|
|
347
444
|
def openid_configuration_body(path)
|
@@ -368,6 +465,7 @@ module Rodauth
|
|
368
465
|
|
369
466
|
metadata.merge(
|
370
467
|
userinfo_endpoint: userinfo_url,
|
468
|
+
end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?),
|
371
469
|
response_types_supported: response_types_supported,
|
372
470
|
subject_types_supported: [oauth_jwt_subject_type],
|
373
471
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-oauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|
@@ -23,12 +23,12 @@ files:
|
|
23
23
|
- CHANGELOG.md
|
24
24
|
- LICENSE.txt
|
25
25
|
- README.md
|
26
|
-
- lib/generators/
|
27
|
-
- lib/generators/
|
28
|
-
- lib/generators/
|
29
|
-
- lib/generators/
|
30
|
-
- lib/generators/
|
31
|
-
- lib/generators/
|
26
|
+
- lib/generators/rodauth/oauth/install_generator.rb
|
27
|
+
- lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
|
28
|
+
- lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
|
29
|
+
- lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb
|
30
|
+
- lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb
|
31
|
+
- lib/generators/rodauth/oauth/views_generator.rb
|
32
32
|
- lib/rodauth/features/oauth.rb
|
33
33
|
- lib/rodauth/features/oauth_http_mac.rb
|
34
34
|
- lib/rodauth/features/oauth_jwt.rb
|
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
71
|
- !ruby/object:Gem::Version
|
72
72
|
version: '0'
|
73
73
|
requirements: []
|
74
|
-
rubygems_version: 3.
|
74
|
+
rubygems_version: 3.2.15
|
75
75
|
signing_key:
|
76
76
|
specification_version: 4
|
77
77
|
summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
|