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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02d69464053b6809900da774b4c9957d642b003d0a0de7aa076e57a5eb8895bc
4
- data.tar.gz: e1dd94b69aa4bdf051b1c28d684a0ec2a1435da9f99ca6bf77da9d537474f9a6
3
+ metadata.gz: 24f3563d467a065dc119cbe53c0f14c8f28654d6512de0b7fd09a99e3f4aabdb
4
+ data.tar.gz: 8abc6dc62b463885f7198cafec57fe707f99885e67754be1d7cefbbefa31a834
5
5
  SHA512:
6
- metadata.gz: cfe325a2e8daa96a72b4577566ae84772dcf114411ebbd9d0115f0f33f5b104f7c138a1b1ef7813d46f0fd2226c6560db5d974e51ec92b33ce7e393726005b2d
7
- data.tar.gz: df5866c1cd089c0d361a00d1ffd1db02df9b6551940dbf70e7390657fa8558ae0fb3c8eb2fcff6ec6fb9fd52406a99615574305d5a3bacdbd20f5fc22bacd63f
6
+ metadata.gz: 9fd172c9930f1cf88239a8f5ba7d5c93dc9c92b05a601c6f909b5ad12e4319ce0aa093831b93dad93542fdda0cdc1694b001ca78922239e80a2370472373b9a5
7
+ data.tar.gz: 26e8e9c619425213f2cd5f21e7710f9561c0b0395bd8928656584c88586f4197bf80a765d9a90baea604ef9fdaff5c27f4b8447adf3fa617463e26b7b0a08470
@@ -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
- SERVER_METADATA = OAuth::TtlStore.new
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
- DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
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.hex(32)
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 => Time.now + oauth_token_expires_in
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, dataset = db[oauth_tokens_table])
723
+ def oauth_token_by_token(token)
724
+ ds = db[oauth_tokens_table]
725
+
601
726
  ds = if oauth_tokens_token_hash_column
602
- dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
727
+ ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
603
728
  else
604
- dataset.where(oauth_tokens_token_column => token)
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, dataset = db[oauth_tokens_table])
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
- dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
746
+ ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
614
747
  else
615
- dataset.where(oauth_tokens_refresh_token_column => token)
748
+ ds.where(oauth_tokens_refresh_token_column => token)
616
749
  end
617
750
 
618
- ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
619
- .where(oauth_tokens_revoked_at_column => nil).first
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 => Time.now + oauth_grant_expires_in,
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
- redirect_response_error("invalid_grant") unless oauth_token
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 => Time.now + oauth_token_expires_in
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
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
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: "#{base_url}/#{oauth_applications_path}",
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
- JWKS = OAuth::TtlStore.new
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 => Time.now + oauth_token_expires_in
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.utc.to_i
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.utc.to_i if 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
- DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
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
- userinfo_endpoint: userinfo_url,
209
- response_types_supported: metadata[:response_types_supported] +
210
- ["none", "id_token", %w[code token], %w[code id_token], %w[id_token token], %w[code id_token token]],
211
- response_modes_supported: %w[query fragment],
212
- grant_types_supported: %w[authorization_code implicit],
213
-
214
- subject_types_supported: [oauth_jwt_subject_type],
215
-
216
- id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
217
- id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
218
- id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
219
-
220
- userinfo_signing_alg_values_supported: [],
221
- userinfo_encryption_alg_values_supported: [],
222
- userinfo_encryption_enc_values_supported: [],
223
-
224
- request_object_signing_alg_values_supported: [],
225
- request_object_encryption_alg_values_supported: [],
226
- request_object_encryption_enc_values_supported: [],
227
-
228
- # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
229
- # Values defined by this specification are normal, aggregated, and distributed.
230
- # If omitted, the implementation supports only normal Claims.
231
- claim_types_supported: %w[normal],
232
- claims_supported: %w[sub iss iat exp aud] | scope_claims
233
- })
234
- end
235
-
236
- # /userinfo
237
- route(:userinfo) do |r|
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  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,4 @@
1
+ <div class="form-group">
2
+ <label for="client_secret">#{rodauth.client_secret_label}#{rodauth.input_field_label_suffix}</label>
3
+ #{rodauth.input_field_string(rodauth.oauth_application_client_secret_param, "client_secret", :type=>"text")}
4
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <label for="description">#{rodauth.description_label}#{rodauth.input_field_label_suffix}</label>
3
+ #{rodauth.input_field_string(rodauth.oauth_application_description_param, "description", :type=>"text", :required => false)}
4
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <label for="homepage_url">#{rodauth.homepage_url_label}#{rodauth.input_field_label_suffix}</label>
3
+ #{rodauth.input_field_string(rodauth.oauth_application_homepage_url_param, "homepage_url", :type=>"text")}
4
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <label for="name">#{rodauth.name_label}#{rodauth.input_field_label_suffix}</label>
3
+ #{rodauth.input_field_string(rodauth.oauth_application_name_param, "name", :type=>"text")}
4
+ </div>
@@ -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,4 @@
1
+ <div class="form-group">
2
+ <label for="redirect_uri">#{rodauth.redirect_uri_label}#{rodauth.input_field_label_suffix}</label>
3
+ #{rodauth.input_field_string(rodauth.oauth_application_redirect_uri_param, "redirect_uri", :type=>"text")}
4
+ </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.2.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-09-09 00:00:00.000000000 Z
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
- homepage: https://gitlab.com/honeyryderchuck/roda-oauth
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/roda-oauth
46
- source_code_uri: https://gitlab.com/honeyryderchuck/roda-oauth
47
- changelog_uri: https://gitlab.com/honeyryderchuck/roda-oauth/-/blob/master/CHANGELOG.md
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: