rodauth-oauth 0.4.0 → 0.5.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 +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.
|