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 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: