rodauth-oauth 0.4.3 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|