rodauth-oauth 0.0.3 → 0.0.4
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 +37 -0
- data/README.md +83 -10
- data/lib/rodauth/features/oauth.rb +209 -99
- data/lib/rodauth/features/oauth_http_mac.rb +0 -3
- data/lib/rodauth/features/oauth_jwt.rb +127 -20
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22808f6f421ce935e5f69301b1f0edc37a286baedf0c4a4039d29b3ff678bf40
|
4
|
+
data.tar.gz: 64305ba438e3035309ca428d4820f00f7b169ca219882575262ee8242a800f50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6302e1e592bf760b9a1a1ac4adc6e9189545f58818553ec85fda0f86299531dcea1e1b59cba65aaa62d607e2cf541fb18ee2a9c2161ce96678b5b2812326a0b
|
7
|
+
data.tar.gz: 689a8e88d89c8fe74b665c65138ff8372cee05d0783e473e8e124b9a9f21d9806bb397d185edf9d0c87ef21cfcd2d81bfa16ab0aaa3da64b9b28d6e5e2e24ab3
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,43 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.0.4 (13/6/2020)
|
6
|
+
|
7
|
+
### Features
|
8
|
+
|
9
|
+
#### Token introspection
|
10
|
+
|
11
|
+
`rodauth-oauth` now ships with an introspection endpoint (`/oauth-introspect`).
|
12
|
+
|
13
|
+
#### Authorization Server Metadata
|
14
|
+
|
15
|
+
`rodauth-oauth` now allows to define an authorization metadata endpoint, which has to be defined at the route of the router:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
route do |r|
|
19
|
+
r.rodauth
|
20
|
+
rodauth.oauth_server_metadata
|
21
|
+
...
|
22
|
+
```
|
23
|
+
|
24
|
+
#### JWKs URI
|
25
|
+
|
26
|
+
the `oauth_jwt` feature now ships with an endpoint, `/oauth-jwks`, where client applications can retrieve the JWK set to verify generated tokens.
|
27
|
+
|
28
|
+
#### JWT access tokens as authorization grants
|
29
|
+
|
30
|
+
The `oauth_jwt` feature now allows the usage of access tokens to authorize the generation of new tokens, [as per the RFC](https://tools.ietf.org/html/rfc7523#section-4);
|
31
|
+
|
32
|
+
### Improvements
|
33
|
+
|
34
|
+
* using `client_secret_basic` authorization where client id/secret params were allowed (i.e. in the token and revoke endpoints, for example);
|
35
|
+
* improved JWK usage for both supported jwt libraries;
|
36
|
+
* marked `fetch_access_token` as auth_value_method, thereby allowing users to fetch the access token from other sources than the "Authorization" header (i.e. form body, query params, etc...)
|
37
|
+
|
38
|
+
### Bugfixes
|
39
|
+
|
40
|
+
* Fixed scope claim of JWT ("scopes" -> "scope");
|
41
|
+
|
5
42
|
## 0.0.3 (5/6/2020)
|
6
43
|
|
7
44
|
### Features
|
data/README.md
CHANGED
@@ -15,6 +15,8 @@ This gem implements:
|
|
15
15
|
* [Access Token refresh](https://tools.ietf.org/html/rfc6749#section-1.5);
|
16
16
|
* [Implicit grant (off by default)[https://tools.ietf.org/html/rfc6749#section-4.2];
|
17
17
|
* [Token revocation](https://tools.ietf.org/html/rfc7009);
|
18
|
+
* [Token introspection](https://tools.ietf.org/html/rfc7662);
|
19
|
+
* [Authorization Server Metadata](https://tools.ietf.org/html/rfc8414);
|
18
20
|
* [PKCE](https://tools.ietf.org/html/rfc7636);
|
19
21
|
* Access Type (Token refresh online and offline);
|
20
22
|
* [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
|
@@ -146,10 +148,9 @@ Token revocation can be done both by the idenntity owner or the application owne
|
|
146
148
|
|
147
149
|
```ruby
|
148
150
|
require "httpx"
|
149
|
-
httpx = HTTPX.plugin(:
|
150
|
-
response = httpx.
|
151
|
+
httpx = HTTPX.plugin(:basic_authorization)
|
152
|
+
response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
|
151
153
|
.post("https://auth_server/oauth-revoke",json: {
|
152
|
-
client_id: ENV["OAUTH_CLIENT_ID"],
|
153
154
|
token_type_hint: "access_token", # can also be "refresh:tokn"
|
154
155
|
token: "2r89hfef4j9f90d2j2390jf390g"
|
155
156
|
})
|
@@ -164,6 +165,55 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear
|
|
164
165
|
> curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
|
165
166
|
```
|
166
167
|
|
168
|
+
#### Token introspection
|
169
|
+
|
170
|
+
Token revocation can be used to determine the state of a token (whether active, what's the scope...) . Here's an example using server-to-server:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
require "httpx"
|
174
|
+
httpx = HTTPX.plugin(:basic_authorization)
|
175
|
+
response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
|
176
|
+
.post("https://auth_server/oauth-introspect",json: {
|
177
|
+
token_type_hint: "access_token", # can also be "refresh:tokn"
|
178
|
+
token: "2r89hfef4j9f90d2j2390jf390g"
|
179
|
+
})
|
180
|
+
response.raise_for_status
|
181
|
+
payload = JSON.parse(response.to_s)
|
182
|
+
puts payload #=> {"active" => true, "scope" => "read write" ....
|
183
|
+
```
|
184
|
+
|
185
|
+
##### cURL
|
186
|
+
|
187
|
+
```
|
188
|
+
> curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
|
189
|
+
```
|
190
|
+
|
191
|
+
### Authorization Server Metadata
|
192
|
+
|
193
|
+
The Authorization Server Metadata endpoint can be used by clients to obtain the information needed to interact with an
|
194
|
+
OAuth 2.0 authorization server, i.e. know which endpoint is used to authorize clients.
|
195
|
+
|
196
|
+
Because this endpoint **must be https://AUTHSERVER/.well-known/oauth-authorization-server**, you'll have to define it at the root-level of your app:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
plugin :rodauth do
|
200
|
+
# enable it in the plugin
|
201
|
+
enable :login, :oauth
|
202
|
+
oauth_application_default_scope %w[profile.read]
|
203
|
+
oauth_application_scopes %w[profile.read profile.write]
|
204
|
+
end
|
205
|
+
|
206
|
+
# then, inside roda
|
207
|
+
|
208
|
+
route do |r|
|
209
|
+
r.rodauth
|
210
|
+
# server metadata endpoint
|
211
|
+
rodauth.oauth_server_metadata
|
212
|
+
|
213
|
+
# now, your oauth and app code...
|
214
|
+
|
215
|
+
```
|
216
|
+
|
167
217
|
### Database migrations
|
168
218
|
|
169
219
|
You have to generate database tables for Oauth applications, grants and tokens. In order for you to hit the ground running, [here's a set of migrations (using `sequel`) to generate the needed tables](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/test/migrate) (omit the first 2 if you already have account tables).
|
@@ -427,13 +477,13 @@ Generating an access token will deliver the following fields:
|
|
427
477
|
# with httpx
|
428
478
|
require "httpx"
|
429
479
|
response = httpx.post("https://auth_server/oauth-token",json: {
|
430
|
-
client_id:
|
431
|
-
client_secret:
|
480
|
+
client_id: env["oauth_client_id"],
|
481
|
+
client_secret: env["oauth_client_secret"],
|
432
482
|
grant_type: "authorization_code",
|
433
483
|
code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"
|
434
484
|
})
|
435
485
|
response.raise_for_status
|
436
|
-
payload =
|
486
|
+
payload = json.parse(response.to_s)
|
437
487
|
puts payload #=> {
|
438
488
|
# "access_token" => ....
|
439
489
|
# "mac_key" => ....
|
@@ -469,7 +519,6 @@ This will, by default, use the OAuth application as HMAC signature and "HS256" a
|
|
469
519
|
```ruby
|
470
520
|
enable :oauth_jwt
|
471
521
|
oauth_jwt_secret "SECRET"
|
472
|
-
# or oauth_jwt_secret_path "path/to/file"
|
473
522
|
oauth_jwt_algorithm "HS512"
|
474
523
|
```
|
475
524
|
|
@@ -486,7 +535,7 @@ rsa_public = rsa_private.public_key
|
|
486
535
|
plugin :rodauth do
|
487
536
|
enable :oauth_jwt
|
488
537
|
oauth_jwt_key rsa_private
|
489
|
-
|
538
|
+
oauth_jwt_public_key rsa_public
|
490
539
|
oauth_jwt_algorithm "RS256"
|
491
540
|
end
|
492
541
|
```
|
@@ -496,10 +545,14 @@ end
|
|
496
545
|
One can further encode the JWT token using JSON Web Keys. Here's how you could enable the feature:
|
497
546
|
|
498
547
|
```ruby
|
548
|
+
rsa_private = OpenSSL::PKey::RSA.generate 2048
|
549
|
+
rsa_public = rsa_private.public_key
|
550
|
+
|
499
551
|
plugin :rodauth do
|
500
552
|
enable :oauth_jwt
|
501
|
-
oauth_jwt_jwk_key
|
502
|
-
|
553
|
+
oauth_jwt_jwk_key rsa_private
|
554
|
+
oauth_jwt_jwk_public_key rsa_public
|
555
|
+
oauth_jwt_jwk_algorithm "RS256"
|
503
556
|
end
|
504
557
|
```
|
505
558
|
|
@@ -520,6 +573,26 @@ end
|
|
520
573
|
|
521
574
|
which adds an extra layer of protection.
|
522
575
|
|
576
|
+
#### JWKS URI
|
577
|
+
|
578
|
+
A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/oauth-jwks`.
|
579
|
+
|
580
|
+
#### JWT Bearer as authorization grant
|
581
|
+
|
582
|
+
One can emit a new access token by using the bearer access token as grant. This can be done emitting a request similar to this:
|
583
|
+
|
584
|
+
```ruby
|
585
|
+
# with httpx
|
586
|
+
require "httpx"
|
587
|
+
response = httpx.post("https://auth_server/oauth-token",json: {
|
588
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
589
|
+
assertion: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6IkV4YW1wbGUiLCJpYXQiOjE1OTIwMDk1MDEsImNsaWVudF9pZCI6IkNMSUVOVF9JRCIsImV4cCI6MTU5MjAxMzEwMSwiYXVkIjpudWxsLCJzY29wZSI6InVzZXIucmVhZCB1c2VyLndyaXRlIiwianRpIjoiOGM1NTVjMjdiOWRjNDdmOTcyNWRkYzBhMjk0NzA1ZTA4NzFkY2JlN2Q5ZTNlMmVkNGE1ZTBiOGZlNTZlYzcxMSJ9.AlxKRtE3ec0mtyBSDx4VseND4eC6cH5ubtv8gfYxxsc"
|
590
|
+
})
|
591
|
+
response.raise_for_status
|
592
|
+
payload = json.parse(response.to_s)
|
593
|
+
puts payload #=> {
|
594
|
+
# "access_token" => "ey....
|
595
|
+
```
|
523
596
|
|
524
597
|
#### DB Schema
|
525
598
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "base64"
|
4
|
+
|
3
5
|
module Rodauth
|
4
6
|
Feature.define(:oauth) do
|
5
7
|
# RUBY EXTENSIONS
|
@@ -38,11 +40,12 @@ module Rodauth
|
|
38
40
|
after "authorize_failure"
|
39
41
|
|
40
42
|
before "token"
|
41
|
-
after "token"
|
42
43
|
|
43
44
|
before "revoke"
|
44
45
|
after "revoke"
|
45
46
|
|
47
|
+
before "introspect"
|
48
|
+
|
46
49
|
before "create_oauth_application"
|
47
50
|
after "create_oauth_application"
|
48
51
|
|
@@ -143,7 +146,7 @@ module Rodauth
|
|
143
146
|
|
144
147
|
auth_value_method :oauth_application_default_scope, SCOPES.first
|
145
148
|
auth_value_method :oauth_application_scopes, SCOPES
|
146
|
-
auth_value_method :oauth_token_type, "
|
149
|
+
auth_value_method :oauth_token_type, "bearer"
|
147
150
|
|
148
151
|
auth_value_method :invalid_request, "Request is missing a required parameter"
|
149
152
|
auth_value_method :invalid_client, "Invalid client"
|
@@ -164,7 +167,14 @@ module Rodauth
|
|
164
167
|
auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
|
165
168
|
auth_value_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
|
166
169
|
|
170
|
+
# METADATA
|
171
|
+
auth_value_method :oauth_metadata_service_documentation, nil
|
172
|
+
auth_value_method :oauth_metadata_ui_locales_supported, nil
|
173
|
+
auth_value_method :oauth_metadata_op_policy_uri, nil
|
174
|
+
auth_value_method :oauth_metadata_op_tos_uri, nil
|
175
|
+
|
167
176
|
auth_value_methods(
|
177
|
+
:fetch_access_token,
|
168
178
|
:oauth_unique_id_generator,
|
169
179
|
:secret_matches?,
|
170
180
|
:secret_hash
|
@@ -190,7 +200,7 @@ module Rodauth
|
|
190
200
|
|
191
201
|
def check_csrf?
|
192
202
|
case request.path
|
193
|
-
when oauth_token_path
|
203
|
+
when oauth_token_path, oauth_introspect_path
|
194
204
|
false
|
195
205
|
when oauth_revoke_path
|
196
206
|
!json_request?
|
@@ -221,8 +231,6 @@ module Rodauth
|
|
221
231
|
end
|
222
232
|
end
|
223
233
|
|
224
|
-
attr_reader :oauth_application
|
225
|
-
|
226
234
|
def initialize(scope)
|
227
235
|
@scope = scope
|
228
236
|
end
|
@@ -267,23 +275,25 @@ module Rodauth
|
|
267
275
|
end
|
268
276
|
end
|
269
277
|
|
270
|
-
def
|
271
|
-
|
278
|
+
def fetch_access_token
|
279
|
+
value = request.env["HTTP_AUTHORIZATION"]
|
272
280
|
|
273
|
-
|
274
|
-
value = request.get_header("HTTP_AUTHORIZATION").to_s
|
281
|
+
return unless value
|
275
282
|
|
276
|
-
|
283
|
+
scheme, token = value.split(" ", 2)
|
277
284
|
|
278
|
-
|
285
|
+
return unless scheme.downcase == oauth_token_type
|
279
286
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
+
token
|
288
|
+
end
|
289
|
+
|
290
|
+
def authorization_token
|
291
|
+
return @authorization_token if defined?(@authorization_token)
|
292
|
+
|
293
|
+
# check if there is a token
|
294
|
+
# check if token has not expired
|
295
|
+
# check if token has been revoked
|
296
|
+
@authorization_token = oauth_token_by_token(fetch_access_token)
|
287
297
|
end
|
288
298
|
|
289
299
|
def require_oauth_authorization(*scopes)
|
@@ -344,8 +354,44 @@ module Rodauth
|
|
344
354
|
end
|
345
355
|
end
|
346
356
|
|
357
|
+
def oauth_server_metadata(issuer = nil)
|
358
|
+
request.on(".well-known") do
|
359
|
+
request.on("oauth-authorization-server") do
|
360
|
+
request.get do
|
361
|
+
json_response_success(oauth_server_metadata_body(issuer))
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
347
367
|
private
|
348
368
|
|
369
|
+
# to be used internally. Same semantics as require account, must:
|
370
|
+
# fetch an authorization basic header
|
371
|
+
# parse client id and secret
|
372
|
+
#
|
373
|
+
def require_oauth_application
|
374
|
+
# get client credenntials
|
375
|
+
client_id = client_secret = nil
|
376
|
+
|
377
|
+
# client_secret_basic
|
378
|
+
if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
|
379
|
+
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
|
380
|
+
else
|
381
|
+
client_id = param_or_nil(client_id_param)
|
382
|
+
client_secret = param_or_nil(client_secret_param)
|
383
|
+
end
|
384
|
+
|
385
|
+
authorization_required unless client_id
|
386
|
+
|
387
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
388
|
+
|
389
|
+
# skip if using pkce
|
390
|
+
return if @oauth_application && use_oauth_pkce? && param_or_nil(code_verifier_param)
|
391
|
+
|
392
|
+
authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
|
393
|
+
end
|
394
|
+
|
349
395
|
def secret_matches?(oauth_application, secret)
|
350
396
|
BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
|
351
397
|
end
|
@@ -362,6 +408,10 @@ module Rodauth
|
|
362
408
|
Base64.urlsafe_encode64(Digest::SHA256.digest(token))
|
363
409
|
end
|
364
410
|
|
411
|
+
def token_from_application?(oauth_token, oauth_application)
|
412
|
+
oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
|
413
|
+
end
|
414
|
+
|
365
415
|
unless method_defined?(:password_hash)
|
366
416
|
# From login_requirements_base feature
|
367
417
|
if ENV["RACK_ENV"] == "test"
|
@@ -426,31 +476,35 @@ module Rodauth
|
|
426
476
|
end
|
427
477
|
end
|
428
478
|
|
429
|
-
def oauth_token_by_token(token)
|
430
|
-
if oauth_tokens_token_hash_column
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
479
|
+
def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
|
480
|
+
ds = if oauth_tokens_token_hash_column
|
481
|
+
dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
482
|
+
else
|
483
|
+
dataset.where(oauth_tokens_token_column => token)
|
484
|
+
end
|
485
|
+
|
486
|
+
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
487
|
+
.where(oauth_tokens_revoked_at_column => nil).first
|
435
488
|
end
|
436
489
|
|
437
|
-
def oauth_token_by_refresh_token(token)
|
438
|
-
if oauth_tokens_refresh_token_hash_column
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
490
|
+
def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
|
491
|
+
ds = if oauth_tokens_refresh_token_hash_column
|
492
|
+
dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
493
|
+
else
|
494
|
+
dataset.where(oauth_tokens_refresh_token_column => token)
|
495
|
+
end
|
496
|
+
|
497
|
+
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
498
|
+
.where(oauth_tokens_revoked_at_column => nil).first
|
443
499
|
end
|
444
500
|
|
445
501
|
def json_access_token_payload(oauth_token)
|
446
502
|
payload = {
|
447
503
|
"access_token" => oauth_token[oauth_tokens_token_column],
|
448
|
-
"token_type" => oauth_token_type
|
504
|
+
"token_type" => oauth_token_type,
|
449
505
|
"expires_in" => oauth_token_expires_in
|
450
506
|
}
|
451
|
-
if oauth_token[oauth_tokens_refresh_token_column]
|
452
|
-
payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column]
|
453
|
-
end
|
507
|
+
payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
|
454
508
|
payload
|
455
509
|
end
|
456
510
|
|
@@ -472,9 +526,7 @@ module Rodauth
|
|
472
526
|
if key == oauth_application_homepage_url_param ||
|
473
527
|
key == oauth_application_redirect_uri_param
|
474
528
|
|
475
|
-
unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
|
476
|
-
set_field_error(key, invalid_url_message)
|
477
|
-
end
|
529
|
+
set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
|
478
530
|
|
479
531
|
elsif key == oauth_application_scopes_param
|
480
532
|
|
@@ -617,13 +669,11 @@ module Rodauth
|
|
617
669
|
|
618
670
|
# Access Tokens
|
619
671
|
|
620
|
-
def
|
621
|
-
|
622
|
-
|
623
|
-
unless param_or_nil(client_secret_param)
|
624
|
-
redirect_response_error("invalid_request") unless param_or_nil(code_verifier_param)
|
625
|
-
end
|
672
|
+
def before_token
|
673
|
+
require_oauth_application
|
674
|
+
end
|
626
675
|
|
676
|
+
def validate_oauth_token_params
|
627
677
|
unless (grant_type = param_or_nil(grant_type_param))
|
628
678
|
redirect_response_error("invalid_request")
|
629
679
|
end
|
@@ -640,16 +690,6 @@ module Rodauth
|
|
640
690
|
end
|
641
691
|
|
642
692
|
def create_oauth_token
|
643
|
-
oauth_application = db[oauth_applications_table].where(
|
644
|
-
oauth_applications_client_id_column => param(client_id_param)
|
645
|
-
).first
|
646
|
-
|
647
|
-
redirect_response_error("invalid_request") unless oauth_application
|
648
|
-
|
649
|
-
if (client_secret = param_or_nil(client_secret_param))
|
650
|
-
redirect_response_error("invalid_request") unless secret_matches?(oauth_application, client_secret)
|
651
|
-
end
|
652
|
-
|
653
693
|
case param(grant_type_param)
|
654
694
|
when "authorization_code"
|
655
695
|
create_oauth_token_from_authorization_code(oauth_application)
|
@@ -678,9 +718,7 @@ module Rodauth
|
|
678
718
|
if oauth_grant[oauth_grants_code_challenge_column]
|
679
719
|
code_verifier = param_or_nil(code_verifier_param)
|
680
720
|
|
681
|
-
unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
682
|
-
redirect_response_error("invalid_request")
|
683
|
-
end
|
721
|
+
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
684
722
|
elsif oauth_require_pkce
|
685
723
|
redirect_response_error("code_challenge_required")
|
686
724
|
end
|
@@ -705,11 +743,9 @@ module Rodauth
|
|
705
743
|
|
706
744
|
def create_oauth_token_from_token(oauth_application)
|
707
745
|
# fetch oauth token
|
708
|
-
oauth_token = oauth_token_by_refresh_token(param(refresh_token_param))
|
709
|
-
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
|
710
|
-
).where(oauth_grants_revoked_at_column => nil).for_update.first
|
746
|
+
oauth_token = oauth_token_by_refresh_token(param(refresh_token_param))
|
711
747
|
|
712
|
-
redirect_response_error("invalid_grant") unless oauth_token
|
748
|
+
redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
|
713
749
|
|
714
750
|
token = oauth_unique_id_generator
|
715
751
|
|
@@ -741,42 +777,57 @@ module Rodauth
|
|
741
777
|
oauth_token
|
742
778
|
end
|
743
779
|
|
780
|
+
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
781
|
+
|
782
|
+
# Token introspect
|
783
|
+
|
784
|
+
def validate_oauth_introspect_params
|
785
|
+
# check if valid token hint type
|
786
|
+
if token_type_hint
|
787
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
788
|
+
end
|
789
|
+
|
790
|
+
redirect_response_error("invalid_request") unless param_or_nil(token_param)
|
791
|
+
end
|
792
|
+
|
793
|
+
def json_token_introspect_payload(token)
|
794
|
+
return { active: false } unless token
|
795
|
+
|
796
|
+
{
|
797
|
+
active: true,
|
798
|
+
scope: token[oauth_tokens_scopes_column].gsub(",", " "),
|
799
|
+
client_id: oauth_application[oauth_applications_client_id_column],
|
800
|
+
# username
|
801
|
+
token_type: oauth_token_type
|
802
|
+
}
|
803
|
+
end
|
804
|
+
|
805
|
+
def before_introspect
|
806
|
+
require_oauth_application
|
807
|
+
end
|
808
|
+
|
744
809
|
# Token revocation
|
745
810
|
|
746
811
|
def before_revoke
|
747
|
-
|
812
|
+
require_oauth_application
|
748
813
|
end
|
749
814
|
|
750
|
-
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
751
|
-
|
752
815
|
def validate_oauth_revoke_params
|
753
816
|
# check if valid token hint type
|
754
817
|
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
755
818
|
|
756
|
-
redirect_response_error("invalid_request") unless
|
819
|
+
redirect_response_error("invalid_request") unless param_or_nil(token_param)
|
757
820
|
end
|
758
821
|
|
759
822
|
def revoke_oauth_token
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
oauth_token = ds.where(oauth_tokens_revoked_at_column => nil)
|
769
|
-
.where(
|
770
|
-
Sequel.or(
|
771
|
-
oauth_tokens_account_id_column => account_id,
|
772
|
-
oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
|
773
|
-
oauth_applications_client_id_column => param(client_id_param),
|
774
|
-
oauth_applications_account_id_column => account_id
|
775
|
-
).select(oauth_applications_id_column)
|
776
|
-
)
|
777
|
-
).for_update.first
|
778
|
-
|
779
|
-
redirect_response_error("invalid_request") unless oauth_token
|
823
|
+
oauth_token = case token_type_hint
|
824
|
+
when "access_token"
|
825
|
+
oauth_token_by_token(token)
|
826
|
+
when "refresh_token"
|
827
|
+
oauth_token_by_refresh_token(token)
|
828
|
+
end
|
829
|
+
|
830
|
+
redirect_response_error("invalid_request") unless oauth_token && token_from_application?(oauth_token, oauth_application)
|
780
831
|
|
781
832
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
782
833
|
|
@@ -827,6 +878,14 @@ module Rodauth
|
|
827
878
|
end
|
828
879
|
end
|
829
880
|
|
881
|
+
def json_response_success(body)
|
882
|
+
response.status = 200
|
883
|
+
response["Content-Type"] ||= json_response_content_type
|
884
|
+
json_payload = _json_response_body(body)
|
885
|
+
response.write(json_payload)
|
886
|
+
request.halt
|
887
|
+
end
|
888
|
+
|
830
889
|
def throw_json_response_error(status, error_code)
|
831
890
|
set_response_error_status(status)
|
832
891
|
code = if respond_to?(:"#{error_code}_error_code")
|
@@ -838,7 +897,7 @@ module Rodauth
|
|
838
897
|
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
839
898
|
json_payload = _json_response_body(payload)
|
840
899
|
response["Content-Type"] ||= json_response_content_type
|
841
|
-
response["WWW-Authenticate"] = oauth_token_type if status == 401
|
900
|
+
response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
|
842
901
|
response.write(json_payload)
|
843
902
|
request.halt
|
844
903
|
end
|
@@ -930,6 +989,43 @@ module Rodauth
|
|
930
989
|
end
|
931
990
|
end
|
932
991
|
|
992
|
+
# Server metadata
|
993
|
+
|
994
|
+
def oauth_server_metadata_body(path)
|
995
|
+
issuer = base_url
|
996
|
+
issuer += "/#{path}" if issuer
|
997
|
+
|
998
|
+
responses_supported = %w[code]
|
999
|
+
response_modes_supported = %w[query]
|
1000
|
+
grant_types_supported = %w[authorization_code]
|
1001
|
+
|
1002
|
+
if use_oauth_implicit_grant_type?
|
1003
|
+
responses_supported << "token"
|
1004
|
+
response_modes_supported << "fragment"
|
1005
|
+
grant_types_supported << "implicit"
|
1006
|
+
end
|
1007
|
+
{
|
1008
|
+
issuer: issuer,
|
1009
|
+
authorization_endpoint: oauth_authorize_url,
|
1010
|
+
token_endpoint: oauth_token_url,
|
1011
|
+
registration_endpoint: "#{base_url}/#{oauth_applications_path}",
|
1012
|
+
scopes_supported: oauth_application_scopes,
|
1013
|
+
response_types_supported: responses_supported,
|
1014
|
+
response_modes_supported: response_modes_supported,
|
1015
|
+
grant_types_supported: grant_types_supported,
|
1016
|
+
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
|
1017
|
+
service_documentation: oauth_metadata_service_documentation,
|
1018
|
+
ui_locales_supported: oauth_metadata_ui_locales_supported,
|
1019
|
+
op_policy_uri: oauth_metadata_op_policy_uri,
|
1020
|
+
op_tos_uri: oauth_metadata_op_tos_uri,
|
1021
|
+
revocation_endpoint: oauth_revoke_url,
|
1022
|
+
revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
|
1023
|
+
introspection_endpoint: oauth_introspect_url,
|
1024
|
+
introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
|
1025
|
+
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1026
|
+
}
|
1027
|
+
end
|
1028
|
+
|
933
1029
|
# /oauth-token
|
934
1030
|
route(:oauth_token) do |r|
|
935
1031
|
before_token
|
@@ -941,14 +1037,35 @@ module Rodauth
|
|
941
1037
|
oauth_token = nil
|
942
1038
|
transaction do
|
943
1039
|
oauth_token = create_oauth_token
|
944
|
-
after_token
|
945
1040
|
end
|
946
1041
|
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
1042
|
+
json_response_success(json_access_token_payload(oauth_token))
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# /oauth-introspect
|
1050
|
+
route(:oauth_introspect) do |r|
|
1051
|
+
before_introspect
|
1052
|
+
|
1053
|
+
r.post do
|
1054
|
+
catch_error do
|
1055
|
+
validate_oauth_introspect_params
|
1056
|
+
|
1057
|
+
oauth_token = case param(token_type_hint_param)
|
1058
|
+
when "access_token"
|
1059
|
+
oauth_token_by_token(param(token_param))
|
1060
|
+
when "refresh_token"
|
1061
|
+
oauth_token_by_refresh_token(param(token_param))
|
1062
|
+
else
|
1063
|
+
oauth_token_by_token(param(token_param)) || oauth_token_by_refresh_token(param(token_param))
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1067
|
+
|
1068
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
952
1069
|
end
|
953
1070
|
|
954
1071
|
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
@@ -957,7 +1074,6 @@ module Rodauth
|
|
957
1074
|
|
958
1075
|
# /oauth-revoke
|
959
1076
|
route(:oauth_revoke) do |r|
|
960
|
-
require_account
|
961
1077
|
before_revoke
|
962
1078
|
|
963
1079
|
# access-token
|
@@ -972,16 +1088,10 @@ module Rodauth
|
|
972
1088
|
end
|
973
1089
|
|
974
1090
|
if accepts_json?
|
975
|
-
|
976
|
-
response["Content-Type"] ||= json_response_content_type
|
977
|
-
json_response = {
|
1091
|
+
json_response_success \
|
978
1092
|
"token" => oauth_token[oauth_tokens_token_column],
|
979
1093
|
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
980
1094
|
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
981
|
-
}
|
982
|
-
json_payload = _json_response_body(json_response)
|
983
|
-
response.write(json_payload)
|
984
|
-
request.halt
|
985
1095
|
else
|
986
1096
|
set_notice_flash revoke_oauth_token_notice_flash
|
987
1097
|
redirect request.referer || "/"
|
@@ -47,9 +47,6 @@ module Rodauth
|
|
47
47
|
mac_attributes = parse_mac_authorization_header_props(token)
|
48
48
|
|
49
49
|
oauth_token = oauth_token_by_token(mac_attributes["id"])
|
50
|
-
.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
51
|
-
.where(oauth_tokens_revoked_at_column => nil)
|
52
|
-
.first
|
53
50
|
|
54
51
|
return unless oauth_token && mac_signature_matches?(oauth_token, mac_attributes)
|
55
52
|
|
@@ -4,6 +4,9 @@ module Rodauth
|
|
4
4
|
Feature.define(:oauth_jwt) do
|
5
5
|
depends :oauth
|
6
6
|
|
7
|
+
auth_value_method :grant_type_param, "grant_type"
|
8
|
+
auth_value_method :assertion_param, "assertion"
|
9
|
+
|
7
10
|
auth_value_method :oauth_jwt_token_issuer, "Example"
|
8
11
|
|
9
12
|
auth_value_method :oauth_jwt_key, nil
|
@@ -23,9 +26,9 @@ module Rodauth
|
|
23
26
|
auth_value_method :oauth_jwt_audience, nil
|
24
27
|
|
25
28
|
auth_value_methods(
|
26
|
-
:generate_jti,
|
27
29
|
:jwt_encode,
|
28
|
-
:jwt_decode
|
30
|
+
:jwt_decode,
|
31
|
+
:jwks_set
|
29
32
|
)
|
30
33
|
|
31
34
|
def require_oauth_authorization(*scopes)
|
@@ -33,7 +36,7 @@ module Rodauth
|
|
33
36
|
|
34
37
|
scopes << oauth_application_default_scope if scopes.empty?
|
35
38
|
|
36
|
-
token_scopes = authorization_token["
|
39
|
+
token_scopes = authorization_token["scope"].split(" ")
|
37
40
|
|
38
41
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
39
42
|
end
|
@@ -43,17 +46,54 @@ module Rodauth
|
|
43
46
|
def authorization_token
|
44
47
|
return @authorization_token if defined?(@authorization_token)
|
45
48
|
|
46
|
-
@authorization_token =
|
47
|
-
|
49
|
+
@authorization_token = jwt_decode(fetch_access_token)
|
50
|
+
end
|
51
|
+
|
52
|
+
# /token
|
48
53
|
|
49
|
-
|
54
|
+
def before_token
|
55
|
+
# requset authentication optional for assertions
|
56
|
+
return if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
50
57
|
|
51
|
-
|
58
|
+
super
|
59
|
+
end
|
52
60
|
|
53
|
-
|
61
|
+
def validate_oauth_token_params
|
62
|
+
if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
63
|
+
redirect_response_error("invalid_client") unless param_or_nil(assertion_param)
|
64
|
+
else
|
65
|
+
super
|
54
66
|
end
|
55
67
|
end
|
56
68
|
|
69
|
+
def create_oauth_token
|
70
|
+
if param(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
71
|
+
create_oauth_token_from_assertion
|
72
|
+
else
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_oauth_token_from_assertion
|
78
|
+
claims = jwt_decode(param(assertion_param))
|
79
|
+
|
80
|
+
redirect_response_error("invalid_grant") unless claims
|
81
|
+
|
82
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
83
|
+
|
84
|
+
account = account_ds(claims["sub"]).first
|
85
|
+
|
86
|
+
redirect_response_error("invalid_client") unless oauth_application && account
|
87
|
+
|
88
|
+
create_params = {
|
89
|
+
oauth_tokens_account_id_column => claims["sub"],
|
90
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
91
|
+
oauth_tokens_scopes_column => claims["scope"]
|
92
|
+
}
|
93
|
+
|
94
|
+
generate_oauth_token(create_params, false)
|
95
|
+
end
|
96
|
+
|
57
97
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
58
98
|
create_params = {
|
59
99
|
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
@@ -86,14 +126,14 @@ module Rodauth
|
|
86
126
|
# owner is involved, such as the client credentials grant, the value
|
87
127
|
# of "sub" SHOULD correspond to an identifier the authorization
|
88
128
|
# server uses to indicate the client application.
|
89
|
-
client_id:
|
129
|
+
client_id: oauth_application[oauth_applications_client_id_column],
|
90
130
|
|
91
131
|
exp: issued_at + oauth_token_expires_in,
|
92
132
|
aud: oauth_jwt_audience,
|
93
133
|
|
94
134
|
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
95
135
|
# token data.
|
96
|
-
|
136
|
+
scope: oauth_token[oauth_tokens_scopes_column].gsub(",", " ")
|
97
137
|
}
|
98
138
|
|
99
139
|
token = jwt_encode(payload)
|
@@ -102,6 +142,45 @@ module Rodauth
|
|
102
142
|
oauth_token
|
103
143
|
end
|
104
144
|
|
145
|
+
def oauth_token_by_token(token, *)
|
146
|
+
jwt_decode(token)
|
147
|
+
end
|
148
|
+
|
149
|
+
def json_token_introspect_payload(oauth_token)
|
150
|
+
return { active: false } unless oauth_token
|
151
|
+
|
152
|
+
return super unless oauth_token["sub"] # naive check on whether it's a jwt token
|
153
|
+
|
154
|
+
{
|
155
|
+
active: true,
|
156
|
+
scope: oauth_token["scope"],
|
157
|
+
client_id: oauth_token["client_id"],
|
158
|
+
# username
|
159
|
+
token_type: "access_token",
|
160
|
+
exp: oauth_token["exp"],
|
161
|
+
iat: oauth_token["iat"],
|
162
|
+
nbf: oauth_token["nbf"],
|
163
|
+
sub: oauth_token["sub"],
|
164
|
+
aud: oauth_token["aud"],
|
165
|
+
iss: oauth_token["iss"],
|
166
|
+
jti: oauth_token["jti"]
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
def oauth_server_metadata_body(path)
|
171
|
+
metadata = super
|
172
|
+
metadata.merge! \
|
173
|
+
jwks_uri: oauth_jwks_url,
|
174
|
+
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
|
175
|
+
metadata
|
176
|
+
end
|
177
|
+
|
178
|
+
def token_from_application?(oauth_token, oauth_application)
|
179
|
+
return super unless oauth_token["sub"] # naive check on whether it's a jwt token
|
180
|
+
|
181
|
+
oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
|
182
|
+
end
|
183
|
+
|
105
184
|
def _jwt_key
|
106
185
|
@_jwt_key ||= oauth_jwt_key || oauth_application[oauth_applications_client_secret_column]
|
107
186
|
end
|
@@ -130,16 +209,26 @@ module Rodauth
|
|
130
209
|
end
|
131
210
|
|
132
211
|
def jwt_decode(token)
|
212
|
+
return @jwt_token if defined?(@jwt_token)
|
213
|
+
|
133
214
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
215
|
+
|
216
|
+
@jwt_token = if oauth_jwt_jwk_key
|
217
|
+
jwk = JSON::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
|
218
|
+
JSON::JWT.decode(token, jwk)
|
219
|
+
else
|
220
|
+
JSON::JWT.decode(token, oauth_jwt_public_key || _jwt_key)
|
221
|
+
end
|
140
222
|
rescue JSON::JWT::Exception
|
141
223
|
nil
|
142
224
|
end
|
225
|
+
|
226
|
+
def jwks_set
|
227
|
+
[
|
228
|
+
(JSON::JWK.new(oauth_jwt_jwk_public_key).merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
|
229
|
+
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
230
|
+
].compact
|
231
|
+
end
|
143
232
|
# :nocov:
|
144
233
|
elsif defined?(JWT)
|
145
234
|
|
@@ -163,7 +252,7 @@ module Rodauth
|
|
163
252
|
|
164
253
|
# Use the key and iat to create a unique key per request to prevent replay attacks
|
165
254
|
jti_raw = [key, payload[:iat]].join(":").to_s
|
166
|
-
jti = Digest::
|
255
|
+
jti = Digest::SHA256.hexdigest(jti_raw)
|
167
256
|
|
168
257
|
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
169
258
|
payload[:jti] = jti
|
@@ -183,6 +272,8 @@ module Rodauth
|
|
183
272
|
end
|
184
273
|
|
185
274
|
def jwt_decode(token)
|
275
|
+
return @jwt_token if defined?(@jwt_token)
|
276
|
+
|
186
277
|
# decrypt jwe
|
187
278
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
188
279
|
|
@@ -190,7 +281,7 @@ module Rodauth
|
|
190
281
|
headers = { algorithms: [oauth_jwt_algorithm] }
|
191
282
|
|
192
283
|
key = if oauth_jwt_jwk_key
|
193
|
-
jwk_key = JWT::JWK.new(oauth_jwt_jwk_key)
|
284
|
+
jwk_key = JWT::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
|
194
285
|
# JWK
|
195
286
|
# The jwk loader would fetch the set of JWKs from a trusted source
|
196
287
|
jwk_loader = lambda do |options|
|
@@ -207,12 +298,18 @@ module Rodauth
|
|
207
298
|
# worst case scenario, the key is the application key
|
208
299
|
oauth_jwt_public_key || _jwt_key
|
209
300
|
end
|
210
|
-
|
211
|
-
|
301
|
+
@jwt_token, = JWT.decode(token, key, true, headers)
|
302
|
+
@jwt_token
|
212
303
|
rescue JWT::DecodeError
|
213
304
|
nil
|
214
305
|
end
|
215
306
|
|
307
|
+
def jwks_set
|
308
|
+
[
|
309
|
+
(JWT::JWK.new(oauth_jwt_jwk_public_key).export.merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
|
310
|
+
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
311
|
+
].compact
|
312
|
+
end
|
216
313
|
else
|
217
314
|
# :nocov:
|
218
315
|
def jwt_encode(_token)
|
@@ -222,7 +319,17 @@ module Rodauth
|
|
222
319
|
def jwt_decode(_token)
|
223
320
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
224
321
|
end
|
322
|
+
|
323
|
+
def jwks_set
|
324
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
325
|
+
end
|
225
326
|
# :nocov:
|
226
327
|
end
|
328
|
+
|
329
|
+
route(:oauth_jwks) do |r|
|
330
|
+
r.get do
|
331
|
+
json_response_success(jwks_set)
|
332
|
+
end
|
333
|
+
end
|
227
334
|
end
|
228
335
|
end
|
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.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-13 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:
|