rodauth-oauth 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +1 -1
- data/lib/rodauth/features/oauth.rb +262 -203
- data/lib/rodauth/features/oauth_jwt.rb +15 -15
- data/lib/rodauth/features/oidc.rb +180 -59
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +33 -0
- data/templates/client_secret_field.str +4 -0
- data/templates/description_field.str +4 -0
- data/templates/homepage_url_field.str +4 -0
- data/templates/name_field.str +4 -0
- data/templates/new_oauth_application.str +10 -0
- data/templates/oauth_application.str +11 -0
- data/templates/oauth_applications.str +14 -0
- data/templates/oauth_tokens.str +49 -0
- data/templates/redirect_uri_field.str +4 -0
- data/templates/scope_field.str +10 -0
- metadata +17 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24f3563d467a065dc119cbe53c0f14c8f28654d6512de0b7fd09a99e3f4aabdb
|
4
|
+
data.tar.gz: 8abc6dc62b463885f7198cafec57fe707f99885e67754be1d7cefbbefa31a834
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fd172c9930f1cf88239a8f5ba7d5c93dc9c92b05a601c6f909b5ad12e4319ce0aa093831b93dad93542fdda0cdc1694b001ca78922239e80a2370472373b9a5
|
7
|
+
data.tar.gz: 26e8e9c619425213f2cd5f21e7710f9561c0b0395bd8928656584c88586f4197bf80a765d9a90baea604ef9fdaff5c27f4b8447adf3fa617463e26b7b0a08470
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,35 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.3.0
|
6
|
+
|
7
|
+
#### Features
|
8
|
+
|
9
|
+
* `oauth_refresh_token_protection_policy` is a new option, which can be used to set a protection policy around usage of refresh tokens. By default it's `none`, for backwards-compatibility. However, when set to `rotation`, refresh tokens will be "use-once", i.e. a token refresh request will generate a new refresh token. Also, refresh token requests doen with already-used refresh tokens will be interpreted as a security breach, i.e. all tokens linked to the compromised refresh token will be revoked.
|
10
|
+
|
11
|
+
#### Improvements
|
12
|
+
|
13
|
+
|
14
|
+
* Support for the OIDC authorize [`prompt` parameter](https://openid.net/specs/openid-connect-core-1_0.html) (sectionn 3.1.2.1). It supports the `none`, `login` and `consent` out-of-the-box, while providing support for `select-account` when paired with [rodauth-select-account, a rodauth feature to handle multiple accounts in the same session](https://gitlab.com/honeyryderchuck/rodauth-select-account).
|
15
|
+
|
16
|
+
* Refresh Tokens are now expired. The refresh token expiration period is governed by the `oauth_refresh_token_expires_in` option (default: 1 year), and is the period for which a refresh token can be used after its respective token expired.
|
17
|
+
|
18
|
+
#### Bugfixes
|
19
|
+
|
20
|
+
* Default Templates now being packaged, as a way to provide a default experience to the OAuth journeys.
|
21
|
+
|
22
|
+
* fixing metadata urls when plugin loaded with a prefix path (@ianks)
|
23
|
+
|
24
|
+
* All date/time-based calculations, such as determining an expiration date, or checking if a token has expired, are now performed using database arithmetic operations, using sequel's `date_arithmetic` plugin. This will eliminate subtle bugs, such as when the database timezone is different than the application OS timezone.
|
25
|
+
|
26
|
+
* OIDC configuration endpoint is now stricter, eliminating JSON metadata inherited from the Oauth metadata endpoint. (@ianks)
|
27
|
+
|
28
|
+
#### Chore
|
29
|
+
|
30
|
+
Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
|
31
|
+
|
32
|
+
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).
|
33
|
+
|
5
34
|
### 0.2.0
|
6
35
|
|
7
36
|
#### Features
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Rodauth::Oauth
|
2
2
|
|
3
3
|
[![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
|
4
|
-
[![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
|
4
|
+
[![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg?job=coverage)](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
|
5
5
|
|
6
6
|
This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framework](https://tools.ietf.org/html/rfc6749) for an authorization server.
|
7
7
|
|
@@ -46,6 +46,8 @@ module Rodauth
|
|
46
46
|
|
47
47
|
SCOPES = %w[profile.read].freeze
|
48
48
|
|
49
|
+
SERVER_METADATA = OAuth::TtlStore.new
|
50
|
+
|
49
51
|
before "authorize"
|
50
52
|
after "authorize"
|
51
53
|
|
@@ -75,6 +77,7 @@ module Rodauth
|
|
75
77
|
|
76
78
|
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
77
79
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
80
|
+
auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
|
78
81
|
auth_value_method :use_oauth_implicit_grant_type?, false
|
79
82
|
auth_value_method :use_oauth_pkce?, true
|
80
83
|
auth_value_method :use_oauth_access_type?, true
|
@@ -149,9 +152,11 @@ module Rodauth
|
|
149
152
|
auth_value_method :"oauth_applications_#{column}_column", column
|
150
153
|
end
|
151
154
|
|
155
|
+
# Feature options
|
152
156
|
auth_value_method :oauth_application_default_scope, SCOPES.first
|
153
157
|
auth_value_method :oauth_application_scopes, SCOPES
|
154
158
|
auth_value_method :oauth_token_type, "bearer"
|
159
|
+
auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
|
155
160
|
|
156
161
|
auth_value_method :invalid_client_message, "Invalid client"
|
157
162
|
auth_value_method :invalid_grant_type_message, "Invalid grant type"
|
@@ -200,7 +205,184 @@ module Rodauth
|
|
200
205
|
|
201
206
|
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
202
207
|
|
203
|
-
|
208
|
+
# /token
|
209
|
+
route(:token) do |r|
|
210
|
+
next unless is_authorization_server?
|
211
|
+
|
212
|
+
before_token_route
|
213
|
+
require_oauth_application
|
214
|
+
|
215
|
+
r.post do
|
216
|
+
catch_error do
|
217
|
+
validate_oauth_token_params
|
218
|
+
|
219
|
+
oauth_token = nil
|
220
|
+
transaction do
|
221
|
+
before_token
|
222
|
+
oauth_token = create_oauth_token
|
223
|
+
end
|
224
|
+
|
225
|
+
json_response_success(json_access_token_payload(oauth_token))
|
226
|
+
end
|
227
|
+
|
228
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# /introspect
|
233
|
+
route(:introspect) do |r|
|
234
|
+
next unless is_authorization_server?
|
235
|
+
|
236
|
+
before_introspect_route
|
237
|
+
|
238
|
+
r.post do
|
239
|
+
catch_error do
|
240
|
+
validate_oauth_introspect_params
|
241
|
+
|
242
|
+
before_introspect
|
243
|
+
oauth_token = case param("token_type_hint")
|
244
|
+
when "access_token"
|
245
|
+
oauth_token_by_token(param("token"))
|
246
|
+
when "refresh_token"
|
247
|
+
oauth_token_by_refresh_token(param("token"))
|
248
|
+
else
|
249
|
+
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
250
|
+
end
|
251
|
+
|
252
|
+
if oauth_application
|
253
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
254
|
+
elsif oauth_token
|
255
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
256
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
257
|
+
end
|
258
|
+
|
259
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
260
|
+
end
|
261
|
+
|
262
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# /revoke
|
267
|
+
route(:revoke) do |r|
|
268
|
+
next unless is_authorization_server?
|
269
|
+
|
270
|
+
before_revoke_route
|
271
|
+
require_oauth_application
|
272
|
+
|
273
|
+
r.post do
|
274
|
+
catch_error do
|
275
|
+
validate_oauth_revoke_params
|
276
|
+
|
277
|
+
oauth_token = nil
|
278
|
+
transaction do
|
279
|
+
before_revoke
|
280
|
+
oauth_token = revoke_oauth_token
|
281
|
+
after_revoke
|
282
|
+
end
|
283
|
+
|
284
|
+
if accepts_json?
|
285
|
+
json_response_success \
|
286
|
+
"token" => oauth_token[oauth_tokens_token_column],
|
287
|
+
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
288
|
+
"revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
|
289
|
+
else
|
290
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
291
|
+
redirect request.referer || "/"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# /authorize
|
300
|
+
route(:authorize) do |r|
|
301
|
+
next unless is_authorization_server?
|
302
|
+
|
303
|
+
before_authorize_route
|
304
|
+
require_authorizable_account
|
305
|
+
|
306
|
+
validate_oauth_grant_params
|
307
|
+
try_approval_prompt if use_oauth_access_type? && request.get?
|
308
|
+
|
309
|
+
r.get do
|
310
|
+
authorize_view
|
311
|
+
end
|
312
|
+
|
313
|
+
r.post do
|
314
|
+
redirect_url = URI.parse(redirect_uri)
|
315
|
+
|
316
|
+
transaction do
|
317
|
+
before_authorize
|
318
|
+
do_authorize(redirect_url)
|
319
|
+
end
|
320
|
+
redirect(redirect_url.to_s)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def oauth_server_metadata(issuer = nil)
|
325
|
+
request.on(".well-known") do
|
326
|
+
request.on("oauth-authorization-server") do
|
327
|
+
request.get do
|
328
|
+
json_response_success(oauth_server_metadata_body(issuer), true)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# /oauth-applications routes
|
335
|
+
def oauth_applications
|
336
|
+
request.on(oauth_applications_path) do
|
337
|
+
require_account
|
338
|
+
|
339
|
+
request.get "new" do
|
340
|
+
new_oauth_application_view
|
341
|
+
end
|
342
|
+
|
343
|
+
request.on(oauth_applications_id_pattern) do |id|
|
344
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
345
|
+
next unless oauth_application
|
346
|
+
|
347
|
+
scope.instance_variable_set(:@oauth_application, oauth_application)
|
348
|
+
|
349
|
+
request.is do
|
350
|
+
request.get do
|
351
|
+
oauth_application_view
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
request.on(oauth_tokens_path) do
|
356
|
+
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
357
|
+
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
358
|
+
request.get do
|
359
|
+
oauth_tokens_view
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
request.get do
|
365
|
+
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
|
366
|
+
oauth_applications_view
|
367
|
+
end
|
368
|
+
|
369
|
+
request.post do
|
370
|
+
catch_error do
|
371
|
+
validate_oauth_application_params
|
372
|
+
|
373
|
+
transaction do
|
374
|
+
before_create_oauth_application
|
375
|
+
id = create_oauth_application
|
376
|
+
after_create_oauth_application
|
377
|
+
set_notice_flash create_oauth_application_notice_flash
|
378
|
+
redirect "#{request.path}/#{id}"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
set_error_flash create_oauth_application_error_flash
|
382
|
+
new_oauth_application_view
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
204
386
|
|
205
387
|
def check_csrf?
|
206
388
|
case request.path
|
@@ -324,69 +506,6 @@ module Rodauth
|
|
324
506
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
325
507
|
end
|
326
508
|
|
327
|
-
# /oauth-applications routes
|
328
|
-
def oauth_applications
|
329
|
-
request.on(oauth_applications_path) do
|
330
|
-
require_account
|
331
|
-
|
332
|
-
request.get "new" do
|
333
|
-
new_oauth_application_view
|
334
|
-
end
|
335
|
-
|
336
|
-
request.on(oauth_applications_id_pattern) do |id|
|
337
|
-
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
338
|
-
next unless oauth_application
|
339
|
-
|
340
|
-
scope.instance_variable_set(:@oauth_application, oauth_application)
|
341
|
-
|
342
|
-
request.is do
|
343
|
-
request.get do
|
344
|
-
oauth_application_view
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
|
-
request.on(oauth_tokens_path) do
|
349
|
-
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
350
|
-
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
351
|
-
request.get do
|
352
|
-
oauth_tokens_view
|
353
|
-
end
|
354
|
-
end
|
355
|
-
end
|
356
|
-
|
357
|
-
request.get do
|
358
|
-
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
|
359
|
-
oauth_applications_view
|
360
|
-
end
|
361
|
-
|
362
|
-
request.post do
|
363
|
-
catch_error do
|
364
|
-
validate_oauth_application_params
|
365
|
-
|
366
|
-
transaction do
|
367
|
-
before_create_oauth_application
|
368
|
-
id = create_oauth_application
|
369
|
-
after_create_oauth_application
|
370
|
-
set_notice_flash create_oauth_application_notice_flash
|
371
|
-
redirect "#{request.path}/#{id}"
|
372
|
-
end
|
373
|
-
end
|
374
|
-
set_error_flash create_oauth_application_error_flash
|
375
|
-
new_oauth_application_view
|
376
|
-
end
|
377
|
-
end
|
378
|
-
end
|
379
|
-
|
380
|
-
def oauth_server_metadata(issuer = nil)
|
381
|
-
request.on(".well-known") do
|
382
|
-
request.on("oauth-authorization-server") do
|
383
|
-
request.get do
|
384
|
-
json_response_success(oauth_server_metadata_body(issuer))
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|
388
|
-
end
|
389
|
-
|
390
509
|
def post_configure
|
391
510
|
super
|
392
511
|
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
@@ -401,6 +520,10 @@ module Rodauth
|
|
401
520
|
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
402
521
|
end
|
403
522
|
|
523
|
+
def use_date_arithmetic?
|
524
|
+
true
|
525
|
+
end
|
526
|
+
|
404
527
|
private
|
405
528
|
|
406
529
|
def rescue_from_uniqueness_error(&block)
|
@@ -446,9 +569,9 @@ module Rodauth
|
|
446
569
|
# time-to-live
|
447
570
|
ttl = if response.key?("cache-control")
|
448
571
|
cache_control = response["cache-control"]
|
449
|
-
cache_control[/max-age=(\d+)/, 1]
|
572
|
+
cache_control[/max-age=(\d+)/, 1].to_i
|
450
573
|
elsif response.key?("expires")
|
451
|
-
|
574
|
+
Time.parse(response["expires"]).to_i - Time.now.to_i
|
452
575
|
end
|
453
576
|
|
454
577
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -516,7 +639,7 @@ module Rodauth
|
|
516
639
|
end
|
517
640
|
|
518
641
|
def oauth_unique_id_generator
|
519
|
-
SecureRandom.
|
642
|
+
SecureRandom.urlsafe_base64(32)
|
520
643
|
end
|
521
644
|
|
522
645
|
def generate_token_hash(token)
|
@@ -537,7 +660,7 @@ module Rodauth
|
|
537
660
|
|
538
661
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
539
662
|
create_params = {
|
540
|
-
oauth_grants_expires_in_column =>
|
663
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
541
664
|
}.merge(params)
|
542
665
|
|
543
666
|
rescue_from_uniqueness_error do
|
@@ -597,26 +720,37 @@ module Rodauth
|
|
597
720
|
end
|
598
721
|
end
|
599
722
|
|
600
|
-
def oauth_token_by_token(token
|
723
|
+
def oauth_token_by_token(token)
|
724
|
+
ds = db[oauth_tokens_table]
|
725
|
+
|
601
726
|
ds = if oauth_tokens_token_hash_column
|
602
|
-
|
727
|
+
ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
|
603
728
|
else
|
604
|
-
|
729
|
+
ds.where(oauth_tokens_token_column => token)
|
605
730
|
end
|
606
731
|
|
607
732
|
ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
608
733
|
.where(oauth_tokens_revoked_at_column => nil).first
|
609
734
|
end
|
610
735
|
|
611
|
-
def oauth_token_by_refresh_token(token,
|
736
|
+
def oauth_token_by_refresh_token(token, revoked: false)
|
737
|
+
ds = db[oauth_tokens_table]
|
738
|
+
#
|
739
|
+
# filter expired refresh tokens out.
|
740
|
+
# an expired refresh token is a token whose access token expired for a period longer than the
|
741
|
+
# refresh token expiration period.
|
742
|
+
#
|
743
|
+
ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
|
744
|
+
|
612
745
|
ds = if oauth_tokens_refresh_token_hash_column
|
613
|
-
|
746
|
+
ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
|
614
747
|
else
|
615
|
-
|
748
|
+
ds.where(oauth_tokens_refresh_token_column => token)
|
616
749
|
end
|
617
750
|
|
618
|
-
ds.where(
|
619
|
-
|
751
|
+
ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
|
752
|
+
|
753
|
+
ds.first
|
620
754
|
end
|
621
755
|
|
622
756
|
def json_access_token_payload(oauth_token)
|
@@ -731,7 +865,6 @@ module Rodauth
|
|
731
865
|
).count.zero?
|
732
866
|
|
733
867
|
# if there's a previous oauth grant for the params combo, it means that this user has approved before.
|
734
|
-
|
735
868
|
request.env["REQUEST_METHOD"] = "POST"
|
736
869
|
end
|
737
870
|
|
@@ -740,7 +873,7 @@ module Rodauth
|
|
740
873
|
oauth_grants_account_id_column => account_id,
|
741
874
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
742
875
|
oauth_grants_redirect_uri_column => redirect_uri,
|
743
|
-
oauth_grants_expires_in_column =>
|
876
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
|
744
877
|
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
745
878
|
)
|
746
879
|
|
@@ -846,14 +979,30 @@ module Rodauth
|
|
846
979
|
}
|
847
980
|
create_oauth_token_from_authorization_code(oauth_grant, create_params)
|
848
981
|
when "refresh_token"
|
849
|
-
# fetch oauth token
|
850
|
-
oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
|
851
|
-
|
852
|
-
|
982
|
+
# fetch potentially revoked oauth token
|
983
|
+
oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
|
984
|
+
|
985
|
+
if !oauth_token
|
986
|
+
redirect_response_error("invalid_grant")
|
987
|
+
elsif oauth_token[oauth_tokens_revoked_at_column]
|
988
|
+
if oauth_refresh_token_protection_policy == "rotation"
|
989
|
+
# https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
|
990
|
+
#
|
991
|
+
# If a refresh token is compromised and subsequently used by both the attacker and the legitimate
|
992
|
+
# client, one of them will present an invalidated refresh token, which will inform the authorization
|
993
|
+
# server of the breach. The authorization server cannot determine which party submitted the invalid
|
994
|
+
# refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
|
995
|
+
# forcing the legitimate client to obtain a fresh authorization grant.
|
996
|
+
|
997
|
+
db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
|
998
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
999
|
+
end
|
1000
|
+
redirect_response_error("invalid_grant")
|
1001
|
+
end
|
853
1002
|
|
854
1003
|
update_params = {
|
855
1004
|
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
856
|
-
oauth_tokens_expires_in_column =>
|
1005
|
+
oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
857
1006
|
}
|
858
1007
|
create_oauth_token_from_token(oauth_token, update_params)
|
859
1008
|
end
|
@@ -885,6 +1034,7 @@ module Rodauth
|
|
885
1034
|
redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
|
886
1035
|
|
887
1036
|
rescue_from_uniqueness_error do
|
1037
|
+
oauth_tokens_ds = db[oauth_tokens_table]
|
888
1038
|
token = oauth_unique_id_generator
|
889
1039
|
|
890
1040
|
if oauth_tokens_token_hash_column
|
@@ -893,9 +1043,25 @@ module Rodauth
|
|
893
1043
|
update_params[oauth_tokens_token_column] = token
|
894
1044
|
end
|
895
1045
|
|
896
|
-
|
1046
|
+
oauth_token = if oauth_refresh_token_protection_policy == "rotation"
|
1047
|
+
insert_params = {
|
1048
|
+
**update_params,
|
1049
|
+
oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
|
1050
|
+
oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
# revoke the refresh token
|
1054
|
+
oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1055
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
1056
|
+
|
1057
|
+
insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
|
1058
|
+
__insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
|
1059
|
+
else
|
1060
|
+
# includes none
|
1061
|
+
ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
1062
|
+
__update_and_return__(ds, update_params)
|
1063
|
+
end
|
897
1064
|
|
898
|
-
oauth_token = __update_and_return__(ds, update_params)
|
899
1065
|
oauth_token[oauth_tokens_token_column] = token
|
900
1066
|
oauth_token
|
901
1067
|
end
|
@@ -1000,9 +1166,17 @@ module Rodauth
|
|
1000
1166
|
end
|
1001
1167
|
end
|
1002
1168
|
|
1003
|
-
def json_response_success(body)
|
1169
|
+
def json_response_success(body, cache = false)
|
1004
1170
|
response.status = 200
|
1005
1171
|
response["Content-Type"] ||= json_response_content_type
|
1172
|
+
if cache
|
1173
|
+
# defaulting to 1-day for everyone, for now at least
|
1174
|
+
max_age = 60 * 60 * 24
|
1175
|
+
response["Cache-Control"] = "private, max-age=#{max_age}"
|
1176
|
+
else
|
1177
|
+
response["Cache-Control"] = "no-store"
|
1178
|
+
response["Pragma"] = "no-cache"
|
1179
|
+
end
|
1006
1180
|
json_payload = _json_response_body(body)
|
1007
1181
|
response.write(json_payload)
|
1008
1182
|
request.halt
|
@@ -1130,11 +1304,12 @@ module Rodauth
|
|
1130
1304
|
response_modes_supported << "fragment"
|
1131
1305
|
grant_types_supported << "implicit"
|
1132
1306
|
end
|
1307
|
+
|
1133
1308
|
{
|
1134
1309
|
issuer: issuer,
|
1135
1310
|
authorization_endpoint: authorize_url,
|
1136
1311
|
token_endpoint: token_url,
|
1137
|
-
registration_endpoint:
|
1312
|
+
registration_endpoint: route_url(oauth_applications_path),
|
1138
1313
|
scopes_supported: oauth_application_scopes,
|
1139
1314
|
response_types_supported: responses_supported,
|
1140
1315
|
response_modes_supported: response_modes_supported,
|
@@ -1151,121 +1326,5 @@ module Rodauth
|
|
1151
1326
|
code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
|
1152
1327
|
}
|
1153
1328
|
end
|
1154
|
-
|
1155
|
-
# /token
|
1156
|
-
route(:token) do |r|
|
1157
|
-
next unless is_authorization_server?
|
1158
|
-
|
1159
|
-
before_token_route
|
1160
|
-
require_oauth_application
|
1161
|
-
|
1162
|
-
r.post do
|
1163
|
-
catch_error do
|
1164
|
-
validate_oauth_token_params
|
1165
|
-
|
1166
|
-
oauth_token = nil
|
1167
|
-
transaction do
|
1168
|
-
before_token
|
1169
|
-
oauth_token = create_oauth_token
|
1170
|
-
end
|
1171
|
-
|
1172
|
-
json_response_success(json_access_token_payload(oauth_token))
|
1173
|
-
end
|
1174
|
-
|
1175
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1176
|
-
end
|
1177
|
-
end
|
1178
|
-
|
1179
|
-
# /introspect
|
1180
|
-
route(:introspect) do |r|
|
1181
|
-
next unless is_authorization_server?
|
1182
|
-
|
1183
|
-
before_introspect_route
|
1184
|
-
|
1185
|
-
r.post do
|
1186
|
-
catch_error do
|
1187
|
-
validate_oauth_introspect_params
|
1188
|
-
|
1189
|
-
before_introspect
|
1190
|
-
oauth_token = case param("token_type_hint")
|
1191
|
-
when "access_token"
|
1192
|
-
oauth_token_by_token(param("token"))
|
1193
|
-
when "refresh_token"
|
1194
|
-
oauth_token_by_refresh_token(param("token"))
|
1195
|
-
else
|
1196
|
-
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
1197
|
-
end
|
1198
|
-
|
1199
|
-
if oauth_application
|
1200
|
-
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1201
|
-
elsif oauth_token
|
1202
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
1203
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
1204
|
-
end
|
1205
|
-
|
1206
|
-
json_response_success(json_token_introspect_payload(oauth_token))
|
1207
|
-
end
|
1208
|
-
|
1209
|
-
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
1210
|
-
end
|
1211
|
-
end
|
1212
|
-
|
1213
|
-
# /revoke
|
1214
|
-
route(:revoke) do |r|
|
1215
|
-
next unless is_authorization_server?
|
1216
|
-
|
1217
|
-
before_revoke_route
|
1218
|
-
require_oauth_application
|
1219
|
-
|
1220
|
-
r.post do
|
1221
|
-
catch_error do
|
1222
|
-
validate_oauth_revoke_params
|
1223
|
-
|
1224
|
-
oauth_token = nil
|
1225
|
-
transaction do
|
1226
|
-
before_revoke
|
1227
|
-
oauth_token = revoke_oauth_token
|
1228
|
-
after_revoke
|
1229
|
-
end
|
1230
|
-
|
1231
|
-
if accepts_json?
|
1232
|
-
json_response_success \
|
1233
|
-
"token" => oauth_token[oauth_tokens_token_column],
|
1234
|
-
"refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
|
1235
|
-
"revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
|
1236
|
-
else
|
1237
|
-
set_notice_flash revoke_oauth_token_notice_flash
|
1238
|
-
redirect request.referer || "/"
|
1239
|
-
end
|
1240
|
-
end
|
1241
|
-
|
1242
|
-
redirect_response_error("invalid_request", request.referer || "/")
|
1243
|
-
end
|
1244
|
-
end
|
1245
|
-
|
1246
|
-
# /authorize
|
1247
|
-
route(:authorize) do |r|
|
1248
|
-
next unless is_authorization_server?
|
1249
|
-
|
1250
|
-
before_authorize_route
|
1251
|
-
require_authorizable_account
|
1252
|
-
|
1253
|
-
validate_oauth_grant_params
|
1254
|
-
try_approval_prompt if use_oauth_access_type? && request.get?
|
1255
|
-
|
1256
|
-
r.get do
|
1257
|
-
authorize_view
|
1258
|
-
end
|
1259
|
-
|
1260
|
-
r.post do
|
1261
|
-
redirect_url = URI.parse(redirect_uri)
|
1262
|
-
|
1263
|
-
transaction do
|
1264
|
-
before_authorize
|
1265
|
-
do_authorize(redirect_url)
|
1266
|
-
end
|
1267
|
-
redirect(redirect_url.to_s)
|
1268
|
-
end
|
1269
|
-
end
|
1270
1329
|
end
|
1271
1330
|
end
|
@@ -6,6 +6,8 @@ module Rodauth
|
|
6
6
|
Feature.define(:oauth_jwt) do
|
7
7
|
depends :oauth
|
8
8
|
|
9
|
+
JWKS = OAuth::TtlStore.new
|
10
|
+
|
9
11
|
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
10
12
|
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
11
13
|
|
@@ -39,7 +41,13 @@ module Rodauth
|
|
39
41
|
:last_account_login_at
|
40
42
|
)
|
41
43
|
|
42
|
-
|
44
|
+
route(:jwks) do |r|
|
45
|
+
next unless is_authorization_server?
|
46
|
+
|
47
|
+
r.get do
|
48
|
+
json_response_success({ keys: jwks_set }, true)
|
49
|
+
end
|
50
|
+
end
|
43
51
|
|
44
52
|
def require_oauth_authorization(*scopes)
|
45
53
|
authorization_required unless authorization_token
|
@@ -168,7 +176,7 @@ module Rodauth
|
|
168
176
|
|
169
177
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
170
178
|
create_params = {
|
171
|
-
oauth_grants_expires_in_column =>
|
179
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
172
180
|
}.merge(params)
|
173
181
|
|
174
182
|
oauth_token = rescue_from_uniqueness_error do
|
@@ -198,7 +206,7 @@ module Rodauth
|
|
198
206
|
end
|
199
207
|
|
200
208
|
def jwt_claims(oauth_token)
|
201
|
-
issued_at = Time.now.
|
209
|
+
issued_at = Time.now.to_i
|
202
210
|
|
203
211
|
claims = {
|
204
212
|
iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
|
@@ -219,7 +227,7 @@ module Rodauth
|
|
219
227
|
aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
|
220
228
|
}
|
221
229
|
|
222
|
-
claims[:auth_time] = last_account_login_at.
|
230
|
+
claims[:auth_time] = last_account_login_at.to_i if last_account_login_at
|
223
231
|
|
224
232
|
claims
|
225
233
|
end
|
@@ -237,7 +245,7 @@ module Rodauth
|
|
237
245
|
end
|
238
246
|
end
|
239
247
|
|
240
|
-
def oauth_token_by_token(token
|
248
|
+
def oauth_token_by_token(token)
|
241
249
|
jwt_decode(token)
|
242
250
|
end
|
243
251
|
|
@@ -300,9 +308,9 @@ module Rodauth
|
|
300
308
|
# time-to-live
|
301
309
|
ttl = if response.key?("cache-control")
|
302
310
|
cache_control = response["cache-control"]
|
303
|
-
cache_control[/max-age=(\d+)/, 1]
|
311
|
+
cache_control[/max-age=(\d+)/, 1].to_i
|
304
312
|
elsif response.key?("expires")
|
305
|
-
|
313
|
+
Time.parse(response["expires"]).to_i - Time.now.to_i
|
306
314
|
end
|
307
315
|
|
308
316
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -454,13 +462,5 @@ module Rodauth
|
|
454
462
|
|
455
463
|
super
|
456
464
|
end
|
457
|
-
|
458
|
-
route(:jwks) do |r|
|
459
|
-
next unless is_authorization_server?
|
460
|
-
|
461
|
-
r.get do
|
462
|
-
json_response_success({ keys: jwks_set })
|
463
|
-
end
|
464
|
-
end
|
465
465
|
end
|
466
466
|
end
|
@@ -10,6 +10,54 @@ module Rodauth
|
|
10
10
|
"phone" => %i[phone_number phone_number_verified].freeze
|
11
11
|
}.freeze
|
12
12
|
|
13
|
+
VALID_METADATA_KEYS = %i[
|
14
|
+
issuer
|
15
|
+
authorization_endpoint
|
16
|
+
token_endpoint
|
17
|
+
userinfo_endpoint
|
18
|
+
jwks_uri
|
19
|
+
registration_endpoint
|
20
|
+
scopes_supported
|
21
|
+
response_types_supported
|
22
|
+
response_modes_supported
|
23
|
+
grant_types_supported
|
24
|
+
acr_values_supported
|
25
|
+
subject_types_supported
|
26
|
+
id_token_signing_alg_values_supported
|
27
|
+
id_token_encryption_alg_values_supported
|
28
|
+
id_token_encryption_enc_values_supported
|
29
|
+
userinfo_signing_alg_values_supported
|
30
|
+
userinfo_encryption_alg_values_supported
|
31
|
+
userinfo_encryption_enc_values_supported
|
32
|
+
request_object_signing_alg_values_supported
|
33
|
+
request_object_encryption_alg_values_supported
|
34
|
+
request_object_encryption_enc_values_supported
|
35
|
+
token_endpoint_auth_methods_supported
|
36
|
+
token_endpoint_auth_signing_alg_values_supported
|
37
|
+
display_values_supported
|
38
|
+
claim_types_supported
|
39
|
+
claims_supported
|
40
|
+
service_documentation
|
41
|
+
claims_locales_supported
|
42
|
+
ui_locales_supported
|
43
|
+
claims_parameter_supported
|
44
|
+
request_parameter_supported
|
45
|
+
request_uri_parameter_supported
|
46
|
+
require_request_uri_registration
|
47
|
+
op_policy_uri
|
48
|
+
op_tos_uri
|
49
|
+
].freeze
|
50
|
+
|
51
|
+
REQUIRED_METADATA_KEYS = %i[
|
52
|
+
issuer
|
53
|
+
authorization_endpoint
|
54
|
+
token_endpoint
|
55
|
+
jwks_uri
|
56
|
+
response_types_supported
|
57
|
+
subject_types_supported
|
58
|
+
id_token_signing_alg_values_supported
|
59
|
+
].freeze
|
60
|
+
|
13
61
|
depends :oauth_jwt
|
14
62
|
|
15
63
|
auth_value_method :oauth_application_default_scope, "openid"
|
@@ -22,12 +70,47 @@ module Rodauth
|
|
22
70
|
|
23
71
|
auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
|
24
72
|
|
73
|
+
auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
|
74
|
+
auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
|
75
|
+
auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
|
76
|
+
|
25
77
|
auth_value_methods(:get_oidc_param)
|
26
78
|
|
79
|
+
# /userinfo
|
80
|
+
route(:userinfo) do |r|
|
81
|
+
next unless is_authorization_server?
|
82
|
+
|
83
|
+
r.on method: %i[get post] do
|
84
|
+
catch_error do
|
85
|
+
oauth_token = authorization_token
|
86
|
+
|
87
|
+
throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
|
88
|
+
|
89
|
+
oauth_scopes = oauth_token["scope"].split(" ")
|
90
|
+
|
91
|
+
throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
|
92
|
+
|
93
|
+
account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
|
94
|
+
|
95
|
+
throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
|
96
|
+
|
97
|
+
oauth_scopes.delete("openid")
|
98
|
+
|
99
|
+
oidc_claims = { "sub" => oauth_token["sub"] }
|
100
|
+
|
101
|
+
fill_with_account_claims(oidc_claims, account, oauth_scopes)
|
102
|
+
|
103
|
+
json_response_success(oidc_claims)
|
104
|
+
end
|
105
|
+
|
106
|
+
throw_json_response_error(authorization_required_error_status, "invalid_token")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
27
110
|
def openid_configuration(issuer = nil)
|
28
111
|
request.on(".well-known/openid-configuration") do
|
29
112
|
request.get do
|
30
|
-
json_response_success(openid_configuration_body(issuer))
|
113
|
+
json_response_success(openid_configuration_body(issuer), cache: true)
|
31
114
|
end
|
32
115
|
end
|
33
116
|
end
|
@@ -57,6 +140,68 @@ module Rodauth
|
|
57
140
|
|
58
141
|
private
|
59
142
|
|
143
|
+
def require_authorizable_account
|
144
|
+
try_prompt if param_or_nil("prompt")
|
145
|
+
super
|
146
|
+
end
|
147
|
+
|
148
|
+
# this executes before checking for a logged in account
|
149
|
+
def try_prompt
|
150
|
+
prompt = param_or_nil("prompt")
|
151
|
+
|
152
|
+
case prompt
|
153
|
+
when "none"
|
154
|
+
redirect_response_error("login_required") unless logged_in?
|
155
|
+
|
156
|
+
require_account
|
157
|
+
|
158
|
+
if db[oauth_grants_table].where(
|
159
|
+
oauth_grants_account_id_column => account_id,
|
160
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
161
|
+
oauth_grants_redirect_uri_column => redirect_uri,
|
162
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
163
|
+
oauth_grants_access_type_column => "online"
|
164
|
+
).count.zero?
|
165
|
+
redirect_response_error("consent_required")
|
166
|
+
end
|
167
|
+
|
168
|
+
request.env["REQUEST_METHOD"] = "POST"
|
169
|
+
when "login"
|
170
|
+
if logged_in? && request.cookies[oauth_prompt_login_cookie_key] == "login"
|
171
|
+
::Rack::Utils.delete_cookie_header!(response.headers, oauth_prompt_login_cookie_key, oauth_prompt_login_cookie_options)
|
172
|
+
return
|
173
|
+
end
|
174
|
+
|
175
|
+
# logging out
|
176
|
+
clear_session
|
177
|
+
set_session_value(login_redirect_session_key, request.fullpath)
|
178
|
+
|
179
|
+
login_cookie_opts = Hash[oauth_prompt_login_cookie_options]
|
180
|
+
login_cookie_opts[:value] = "login"
|
181
|
+
login_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_prompt_login_interval) # 15 minutes
|
182
|
+
::Rack::Utils.set_cookie_header!(response.headers, oauth_prompt_login_cookie_key, login_cookie_opts)
|
183
|
+
|
184
|
+
redirect require_login_redirect
|
185
|
+
when "consent"
|
186
|
+
require_account
|
187
|
+
|
188
|
+
if db[oauth_grants_table].where(
|
189
|
+
oauth_grants_account_id_column => account_id,
|
190
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
191
|
+
oauth_grants_redirect_uri_column => redirect_uri,
|
192
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
193
|
+
oauth_grants_access_type_column => "online"
|
194
|
+
).count.zero?
|
195
|
+
redirect_response_error("consent_required")
|
196
|
+
end
|
197
|
+
when "select-account"
|
198
|
+
# obly works if select_account plugin is available
|
199
|
+
require_select_account if respond_to?(:require_select_account)
|
200
|
+
else
|
201
|
+
redirect_response_error("invalid_request")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
60
205
|
def create_oauth_grant(create_params = {})
|
61
206
|
return super unless (nonce = param_or_nil("nonce"))
|
62
207
|
|
@@ -190,7 +335,9 @@ module Rodauth
|
|
190
335
|
# Metadata
|
191
336
|
|
192
337
|
def openid_configuration_body(path)
|
193
|
-
metadata = oauth_server_metadata_body(path)
|
338
|
+
metadata = oauth_server_metadata_body(path).select do |k, _|
|
339
|
+
VALID_METADATA_KEYS.include?(k)
|
340
|
+
end
|
194
341
|
|
195
342
|
scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
|
196
343
|
oidc, param = scope.split(".", 2)
|
@@ -204,63 +351,37 @@ module Rodauth
|
|
204
351
|
|
205
352
|
scope_claims.unshift("auth_time") if last_account_login_at
|
206
353
|
|
207
|
-
metadata.merge(
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
next unless is_authorization_server?
|
239
|
-
|
240
|
-
r.on method: %i[get post] do
|
241
|
-
catch_error do
|
242
|
-
oauth_token = authorization_token
|
243
|
-
|
244
|
-
throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
|
245
|
-
|
246
|
-
oauth_scopes = oauth_token["scope"].split(" ")
|
247
|
-
|
248
|
-
throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
|
249
|
-
|
250
|
-
account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
|
251
|
-
|
252
|
-
throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
|
253
|
-
|
254
|
-
oauth_scopes.delete("openid")
|
255
|
-
|
256
|
-
oidc_claims = { "sub" => oauth_token["sub"] }
|
257
|
-
|
258
|
-
fill_with_account_claims(oidc_claims, account, oauth_scopes)
|
259
|
-
|
260
|
-
json_response_success(oidc_claims)
|
261
|
-
end
|
262
|
-
|
263
|
-
throw_json_response_error(authorization_required_error_status, "invalid_token")
|
354
|
+
metadata.merge(
|
355
|
+
userinfo_endpoint: userinfo_url,
|
356
|
+
response_types_supported: metadata[:response_types_supported] +
|
357
|
+
["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"],
|
358
|
+
response_modes_supported: %w[query fragment],
|
359
|
+
grant_types_supported: %w[authorization_code implicit],
|
360
|
+
|
361
|
+
subject_types_supported: [oauth_jwt_subject_type],
|
362
|
+
|
363
|
+
id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
|
364
|
+
id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
|
365
|
+
id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
|
366
|
+
|
367
|
+
userinfo_signing_alg_values_supported: [],
|
368
|
+
userinfo_encryption_alg_values_supported: [],
|
369
|
+
userinfo_encryption_enc_values_supported: [],
|
370
|
+
|
371
|
+
request_object_signing_alg_values_supported: [],
|
372
|
+
request_object_encryption_alg_values_supported: [],
|
373
|
+
request_object_encryption_enc_values_supported: [],
|
374
|
+
|
375
|
+
# These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
|
376
|
+
# Values defined by this specification are normal, aggregated, and distributed.
|
377
|
+
# If omitted, the implementation supports only normal Claims.
|
378
|
+
claim_types_supported: %w[normal],
|
379
|
+
claims_supported: %w[sub iss iat exp aud] | scope_claims
|
380
|
+
).reject do |key, val|
|
381
|
+
# Filter null values in optional items
|
382
|
+
(!REQUIRED_METADATA_KEYS.include?(key.to_sym) && val.nil?) ||
|
383
|
+
# Claims with zero elements MUST be omitted from the response
|
384
|
+
(val.respond_to?(:empty?) && val.empty?)
|
264
385
|
end
|
265
386
|
end
|
266
387
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<form method="post" class="form-horizontal" role="form" id="authorize-form">
|
2
|
+
#{csrf_tag(rodauth.authorize_path) if respond_to?(:csrf_tag)}
|
3
|
+
<p class="lead">The application #{rodauth.oauth_application[rodauth.oauth_applications_name_column]} would like to access your data.</p>
|
4
|
+
|
5
|
+
<div class="form-group">
|
6
|
+
<h1 class="display-6">#{rodauth.scopes_label}</h1>
|
7
|
+
|
8
|
+
#{
|
9
|
+
rodauth.scopes.map do |scope|
|
10
|
+
<<-HTML
|
11
|
+
<div class="form-check">
|
12
|
+
<input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{scope}" #{"checked disabled" if scope == rodauth.oauth_application_default_scope}>
|
13
|
+
<label class="form-check-label" for="#{scope}">#{scope}</label>
|
14
|
+
</div>
|
15
|
+
HTML
|
16
|
+
end.join
|
17
|
+
}
|
18
|
+
|
19
|
+
<input type="hidden" name="client_id" value="#{rodauth.param("client_id")}"/>
|
20
|
+
|
21
|
+
#{"<input type=\"hidden\" name=\"access_type\" value=\"#{rodauth.param("access_type")}\"/>" if rodauth.param_or_nil("access_type")}
|
22
|
+
#{"<input type=\"hidden\" name=\"response_type\" value=\"#{rodauth.param("response_type")}\"/>" if rodauth.param_or_nil("response_type")}
|
23
|
+
#{"<input type=\"hidden\" name=\"state\" value=\"#{rodauth.param("state")}\"/>" if rodauth.param_or_nil("state")}
|
24
|
+
#{"<input type=\"hidden\" name=\"nonce\" value=\"#{rodauth.param("nonce")}\"/>" if rodauth.param_or_nil("nonce")}
|
25
|
+
#{"<input type=\"hidden\" name=\"redirect_uri\" value=\"#{rodauth.redirect_uri}\"/>" if rodauth.param_or_nil("redirect_uri")}
|
26
|
+
#{"<input type=\"hidden\" name=\"code_challenge\" value=\"#{rodauth.param("code_challenge")}\"/>" if rodauth.param_or_nil("code_challenge")}
|
27
|
+
#{"<input type=\"hidden\" name=\"code_challenge_method\" value=\"#{rodauth.param("code_challenge_method")}\"/>" if rodauth.param_or_nil("code_challenge_method")}
|
28
|
+
</div>
|
29
|
+
<p class="text-center">
|
30
|
+
<input type="submit" class="btn btn-outline-primary" value="#{h(rodauth.oauth_authorize_button)}"/>
|
31
|
+
<a href="#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{ "&state=#{rodauth.param("state")}" if rodauth.param_or_nil("state")}" class="btn btn-outline-danger">Cancel</a>
|
32
|
+
</p>
|
33
|
+
</form>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<form method="post" action="#{rodauth.oauth_applications_path}" class="rodauth" role="form" id="oauth-application-form">
|
2
|
+
#{rodauth.csrf_tag}
|
3
|
+
#{rodauth.render('name_field')}
|
4
|
+
#{rodauth.render('description_field')}
|
5
|
+
#{rodauth.render('homepage_url_field')}
|
6
|
+
#{rodauth.render('redirect_uri_field')}
|
7
|
+
#{rodauth.render('client_secret_field')}
|
8
|
+
#{rodauth.render('scope_field')}
|
9
|
+
#{rodauth.button(rodauth.oauth_application_button)}
|
10
|
+
</form>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div id="oauth-application">
|
2
|
+
<dl>
|
3
|
+
#{
|
4
|
+
(rodauth.oauth_application_required_params + %w[client_id] - %w[client_secret]).map do |param|
|
5
|
+
"<dt class=\"#{param}\">#{rodauth.send(:"#{param}_label")}</dt>" +
|
6
|
+
"<dd class=\"#{param}\">#{@oauth_application[rodauth.send(:"oauth_applications_#{param}_column")]}</dd>"
|
7
|
+
end.join
|
8
|
+
}
|
9
|
+
</dl>
|
10
|
+
<a href="/#{"#{rodauth.oauth_applications_path}/#{@oauth_application[:id]}/#{rodauth.oauth_tokens_path}"}" class="btn btn-outline-secondary">Oauth Tokens</a>
|
11
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div id="oauth-applications">
|
2
|
+
<a class="btn btn-outline-primary" href="/oauth-applications/new">Register new Oauth Application</a>
|
3
|
+
#{
|
4
|
+
if @oauth_applications.count.zero?
|
5
|
+
"<p>No oauth applications yet!</p>"
|
6
|
+
else
|
7
|
+
"<ul class=\"list-group\">" +
|
8
|
+
@oauth_applications.map do |application|
|
9
|
+
"<li class=\"list-group-item\"><a href=\"/oauth-applications/#{application[:id]}\">#{application[:name]}</a></li>"
|
10
|
+
end.join +
|
11
|
+
"</ul>"
|
12
|
+
end
|
13
|
+
}
|
14
|
+
</div>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
<div id="oauth-tokens">
|
2
|
+
#{
|
3
|
+
if @oauth_tokens.count.zero?
|
4
|
+
"<p>No oauth tokens yet!</p>"
|
5
|
+
else
|
6
|
+
<<-HTML
|
7
|
+
<table class="table">
|
8
|
+
<thead>
|
9
|
+
<tr>
|
10
|
+
<th scope="col">Token</th>
|
11
|
+
<th scope="col">Refresh Token</th>
|
12
|
+
<th scope="col">Expires in</th>
|
13
|
+
<th scope="col">Revoke</th>
|
14
|
+
<th scope="col"><span class="badge badge-pill badge-dark">#{@oauth_tokens.count}</span>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
#{
|
19
|
+
@oauth_tokens.map do |oauth_token|
|
20
|
+
<<-HTML
|
21
|
+
<tr>
|
22
|
+
<td>#{oauth_token[rodauth.oauth_tokens_token_column]}</td>
|
23
|
+
<td>#{oauth_token[rodauth.oauth_tokens_refresh_token_column]}</td>
|
24
|
+
<td>#{rodauth.convert_timestamp(oauth_token[rodauth.oauth_tokens_expires_in_column])}</td>
|
25
|
+
<td>#{rodauth.convert_timestamp(oauth_token[rodauth.oauth_tokens_revoked_at_column])}</td>
|
26
|
+
<td>
|
27
|
+
#{
|
28
|
+
if !oauth_token[rodauth.oauth_tokens_revoked_at_param] && !oauth_token[rodauth.oauth_tokens_token_hash_column]
|
29
|
+
<<-HTML
|
30
|
+
<form method="post" action="#{rodauth.revoke_path}" class="form-horizontal" role="form" id="revoke-form">
|
31
|
+
#{csrf_tag(rodauth.oauth_revoke_path) if respond_to?(:csrf_tag)}
|
32
|
+
#{rodauth.input_field_string("token_type_hint", "revoke-token-type-hint", :value => "access_token", :type=>"hidden")}
|
33
|
+
#{rodauth.input_field_string("token", "revoke-token", :value => oauth_token[rodauth.oauth_tokens_token_column], :type=>"hidden")}
|
34
|
+
#{rodauth.button(rodauth.oauth_token_revoke_button)}
|
35
|
+
</form>
|
36
|
+
HTML
|
37
|
+
end
|
38
|
+
}
|
39
|
+
</td>
|
40
|
+
</tr>
|
41
|
+
HTML
|
42
|
+
end.join
|
43
|
+
}
|
44
|
+
</tbody>
|
45
|
+
</table>
|
46
|
+
HTML
|
47
|
+
end
|
48
|
+
}
|
49
|
+
</div>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<fieldset class="form-group">
|
2
|
+
#{
|
3
|
+
rodauth.oauth_application_scopes.map do |scope|
|
4
|
+
"<div class=\"form-check checkbox\">" +
|
5
|
+
"<input id=\"#{scope}\" type=\"checkbox\" name=\"#{rodauth.oauth_application_scopes_param}[]\" value=\"#{scope}\">" +
|
6
|
+
"<label for=\"#{scope}\">#{scope}</label>" +
|
7
|
+
"</div>"
|
8
|
+
end.join
|
9
|
+
}
|
10
|
+
</fieldset>
|
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.3.0
|
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-
|
11
|
+
date: 2020-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|
@@ -39,12 +39,23 @@ files:
|
|
39
39
|
- lib/rodauth/oauth/railtie.rb
|
40
40
|
- lib/rodauth/oauth/ttl_store.rb
|
41
41
|
- lib/rodauth/oauth/version.rb
|
42
|
-
|
42
|
+
- templates/authorize.str
|
43
|
+
- templates/client_secret_field.str
|
44
|
+
- templates/description_field.str
|
45
|
+
- templates/homepage_url_field.str
|
46
|
+
- templates/name_field.str
|
47
|
+
- templates/new_oauth_application.str
|
48
|
+
- templates/oauth_application.str
|
49
|
+
- templates/oauth_applications.str
|
50
|
+
- templates/oauth_tokens.str
|
51
|
+
- templates/redirect_uri_field.str
|
52
|
+
- templates/scope_field.str
|
53
|
+
homepage: https://gitlab.com/honeyryderchuck/rodauth-oauth
|
43
54
|
licenses: []
|
44
55
|
metadata:
|
45
|
-
homepage_uri: https://gitlab.com/honeyryderchuck/
|
46
|
-
source_code_uri: https://gitlab.com/honeyryderchuck/
|
47
|
-
changelog_uri: https://gitlab.com/honeyryderchuck/
|
56
|
+
homepage_uri: https://gitlab.com/honeyryderchuck/rodauth-oauth
|
57
|
+
source_code_uri: https://gitlab.com/honeyryderchuck/rodauth-oauth
|
58
|
+
changelog_uri: https://gitlab.com/honeyryderchuck/rodauth-oauth/-/blob/master/CHANGELOG.md
|
48
59
|
post_install_message:
|
49
60
|
rdoc_options: []
|
50
61
|
require_paths:
|