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