rodauth-oauth 0.4.0 → 0.5.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 +59 -27
- data/README.md +7 -2
- 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 +0 -0
- data/lib/rodauth/features/oauth.rb +54 -29
- data/lib/rodauth/features/oauth_jwt.rb +110 -31
- data/lib/rodauth/features/oidc.rb +100 -2
- 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: 366fa7201a8cb26525b2abdfab0c4108a4afbef4fa8697cf57e874919eda2afd
|
4
|
+
data.tar.gz: 958b9fd8b4cd2996a85b96fac6dff6a6b35832adcdaed4d81e20842041a1299e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb496f7d438c0447ba4ef899f9ec37549cea355fb5c670818cd040970c1316c13442caa58aebc4ea66cd92f2f369d6ff7c39b502190e2d4fec2f95d5e27b4279
|
7
|
+
data.tar.gz: 73048d860285b29056ade97d9092103e2cae18fa9967df603ddce9c83c10fd5c44891e7282f3195d0b79b2d73bf12c748483a811f404d50fb29df40da26a6bf4
|
data/CHANGELOG.md
CHANGED
@@ -2,26 +2,72 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
-
### 0.
|
5
|
+
### 0.5.1 (19/03/2021)
|
6
6
|
|
7
|
-
|
7
|
+
#### Improvements
|
8
|
+
|
9
|
+
* Changing "Callback URL" to "Redirect URL" in default templates;
|
10
|
+
|
11
|
+
#### Bugfixes
|
12
|
+
|
13
|
+
* (rails integration) Fixed templates location;
|
14
|
+
* (rails integration) Fixed migration name from generator;
|
15
|
+
* (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
|
16
|
+
* `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
|
17
|
+
* (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
|
18
|
+
|
19
|
+
### 0.5.0 (08/02/2021)
|
20
|
+
|
21
|
+
#### RP-Initiated Logout
|
22
|
+
|
23
|
+
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.
|
24
|
+
|
25
|
+
#### Security
|
26
|
+
|
27
|
+
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.
|
28
|
+
|
29
|
+
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.
|
30
|
+
|
31
|
+
### 0.4.3 (09/12/2020)
|
32
|
+
|
33
|
+
* 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.
|
34
|
+
|
35
|
+
### 0.4.2 (24/11/2020)
|
36
|
+
|
37
|
+
#### Bugfixes
|
38
|
+
|
39
|
+
* database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
|
40
|
+
|
41
|
+
### 0.4.1 (24/11/2020)
|
42
|
+
|
43
|
+
#### Improvements
|
44
|
+
|
45
|
+
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.
|
46
|
+
|
47
|
+
#### Bugfixes
|
48
|
+
|
49
|
+
* 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.
|
50
|
+
|
51
|
+
### 0.4.0 (13/11/2020)
|
52
|
+
|
53
|
+
#### Features
|
8
54
|
|
9
55
|
* 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.
|
10
56
|
|
11
57
|
* 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.
|
12
58
|
|
13
59
|
|
14
|
-
|
60
|
+
#### Improvements
|
15
61
|
|
16
62
|
* 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.
|
17
63
|
|
18
|
-
|
64
|
+
#### Bugfixes
|
19
65
|
|
20
66
|
* 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;
|
21
67
|
* rails tests were silently not running in CI;
|
22
68
|
* The CI suite was revamped, so that all Oauth tests would be run under rails as well. All versions from rails equal or above 5.0 are now targeted;
|
23
69
|
|
24
|
-
### 0.3.0
|
70
|
+
### 0.3.0 (8/10/2020)
|
25
71
|
|
26
72
|
#### Features
|
27
73
|
|
@@ -50,7 +96,7 @@ Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
|
|
50
96
|
|
51
97
|
Set HTTP Cache headers for metadata responses, such as `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`, so they can be stored at the edge. The cache will be valid for 1 day (this value isn't set by an option yet).
|
52
98
|
|
53
|
-
### 0.2.0
|
99
|
+
### 0.2.0 (9/9/2020)
|
54
100
|
|
55
101
|
#### Features
|
56
102
|
|
@@ -94,9 +140,7 @@ Fixed some mishandling of HTTP headers when in in resource-server mode.
|
|
94
140
|
* 97.7% test coverage;
|
95
141
|
* `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
|
96
142
|
|
97
|
-
### 0.1.0
|
98
|
-
|
99
|
-
(31/7/2020)
|
143
|
+
### 0.1.0 (31/7/2020)
|
100
144
|
|
101
145
|
#### Features
|
102
146
|
|
@@ -142,9 +186,7 @@ URI schemes for client applications redirect URIs have to be `https`. In order t
|
|
142
186
|
* fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
|
143
187
|
|
144
188
|
|
145
|
-
### 0.0.6
|
146
|
-
|
147
|
-
(6/7/2020)
|
189
|
+
### 0.0.6 (6/7/2020)
|
148
190
|
|
149
191
|
#### Features
|
150
192
|
|
@@ -167,9 +209,7 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se
|
|
167
209
|
Removed React Javascript from example applications.
|
168
210
|
|
169
211
|
|
170
|
-
### 0.0.5
|
171
|
-
|
172
|
-
(26/6/2020)
|
212
|
+
### 0.0.5 (26/6/2020)
|
173
213
|
|
174
214
|
#### Features
|
175
215
|
|
@@ -206,9 +246,7 @@ It **requires** the authorization to implement the server metadata endpoint (`/.
|
|
206
246
|
* option `scopes_param` renamed to `scope_param`;
|
207
247
|
*
|
208
248
|
|
209
|
-
## 0.0.4
|
210
|
-
|
211
|
-
(13/6/2020)
|
249
|
+
## 0.0.4 (13/6/2020)
|
212
250
|
|
213
251
|
### Features
|
214
252
|
|
@@ -245,9 +283,7 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g
|
|
245
283
|
|
246
284
|
* Fixed scope claim of JWT ("scopes" -> "scope");
|
247
285
|
|
248
|
-
## 0.0.3
|
249
|
-
|
250
|
-
(5/6/2020)
|
286
|
+
## 0.0.3 (5/6/2020)
|
251
287
|
|
252
288
|
### Features
|
253
289
|
|
@@ -279,9 +315,7 @@ end
|
|
279
315
|
* renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
|
280
316
|
* It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
|
281
317
|
|
282
|
-
## 0.0.2
|
283
|
-
|
284
|
-
(29/5/2020)
|
318
|
+
## 0.0.2 (29/5/2020)
|
285
319
|
|
286
320
|
### Features
|
287
321
|
|
@@ -297,8 +331,6 @@ end
|
|
297
331
|
|
298
332
|
* usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
|
299
333
|
|
300
|
-
## 0.0.1
|
301
|
-
|
302
|
-
(14/5/2020)
|
334
|
+
## 0.0.1 (14/5/2020)
|
303
335
|
|
304
336
|
Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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?
|
@@ -489,13 +506,13 @@ module Rodauth
|
|
489
506
|
def fetch_access_token
|
490
507
|
value = request.env["HTTP_AUTHORIZATION"]
|
491
508
|
|
492
|
-
return unless value
|
509
|
+
return unless value && !value.empty?
|
493
510
|
|
494
511
|
scheme, token = value.split(" ", 2)
|
495
512
|
|
496
513
|
return unless scheme.downcase == oauth_token_type
|
497
514
|
|
498
|
-
return if token.empty?
|
515
|
+
return if token.nil? || token.empty?
|
499
516
|
|
500
517
|
token
|
501
518
|
end
|
@@ -508,31 +525,34 @@ module Rodauth
|
|
508
525
|
|
509
526
|
return unless bearer_token
|
510
527
|
|
511
|
-
|
512
|
-
|
513
|
-
|
528
|
+
@authorization_token = if is_authorization_server?
|
529
|
+
# check if token has not expired
|
530
|
+
# check if token has been revoked
|
531
|
+
oauth_token_by_token(bearer_token)
|
532
|
+
else
|
533
|
+
# where in resource server, NOT the authorization server.
|
534
|
+
payload = introspection_request("access_token", bearer_token)
|
535
|
+
|
536
|
+
return unless payload["active"]
|
537
|
+
|
538
|
+
payload
|
539
|
+
end
|
514
540
|
end
|
515
541
|
|
516
542
|
def require_oauth_authorization(*scopes)
|
517
|
-
|
518
|
-
authorization_required unless authorization_token
|
543
|
+
authorization_required unless authorization_token
|
519
544
|
|
520
|
-
|
545
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
521
546
|
|
547
|
+
token_scopes = if is_authorization_server?
|
522
548
|
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
523
549
|
else
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
# where in resource server, NOT the authorization server.
|
531
|
-
payload = introspection_request("access_token", bearer_token)
|
532
|
-
|
533
|
-
authorization_required unless payload["active"]
|
534
|
-
|
535
|
-
payload["scope"].split(oauth_scope_separator)
|
550
|
+
aux_scopes = authorization_token["scope"]
|
551
|
+
if aux_scopes
|
552
|
+
aux_scopes.split(oauth_scope_separator)
|
553
|
+
else
|
554
|
+
[]
|
555
|
+
end
|
536
556
|
end
|
537
557
|
|
538
558
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
@@ -540,6 +560,11 @@ module Rodauth
|
|
540
560
|
|
541
561
|
def post_configure
|
542
562
|
super
|
563
|
+
|
564
|
+
# all of the extensions below involve DB changes. Resource server mode doesn't use
|
565
|
+
# database functions for OAuth though.
|
566
|
+
return unless is_authorization_server?
|
567
|
+
|
543
568
|
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
544
569
|
|
545
570
|
# Check whether we can reutilize db entries for the same account / application pair
|
@@ -616,9 +641,9 @@ module Rodauth
|
|
616
641
|
http.use_ssl = auth_url.scheme == "https"
|
617
642
|
|
618
643
|
request = Net::HTTP::Post.new(introspect_path)
|
619
|
-
request["content-type"] =
|
644
|
+
request["content-type"] = "application/x-www-form-urlencoded"
|
620
645
|
request["accept"] = json_response_content_type
|
621
|
-
request.
|
646
|
+
request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
|
622
647
|
|
623
648
|
before_introspection_request(request)
|
624
649
|
response = http.request(request)
|
@@ -1342,7 +1367,7 @@ module Rodauth
|
|
1342
1367
|
issuer: issuer,
|
1343
1368
|
authorization_endpoint: authorize_url,
|
1344
1369
|
token_endpoint: token_url,
|
1345
|
-
registration_endpoint:
|
1370
|
+
registration_endpoint: oauth_applications_url,
|
1346
1371
|
scopes_supported: oauth_application_scopes,
|
1347
1372
|
response_types_supported: responses_supported,
|
1348
1373
|
response_modes_supported: response_modes_supported,
|
@@ -8,6 +8,8 @@ module Rodauth
|
|
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
|
@@ -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.5.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-03-19 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.3
|
75
75
|
signing_key:
|
76
76
|
specification_version: 4
|
77
77
|
summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
|