rodauth-oauth 0.2.0 → 0.3.0
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 +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
|
[](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
|
4
|
-
[](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
|
4
|
+
[](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:
|