rodauth-oauth 0.1.0 → 0.4.2

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: 3eac600d006a2c78509f608575db062b7ba6d67356b890c7d38414b9b82875f9
4
- data.tar.gz: c37fc18c093f546023481a88cc526c5a0b721b1a3bfeac827c21184e0583071b
3
+ metadata.gz: 38ad6d8c4d03dac86f78e9c883f11426d805f77f82248098192ed4696a9f20e8
4
+ data.tar.gz: 07d4a10bae7e031033f0660347022e42139adb7be8834daaba307f1530838ce0
5
5
  SHA512:
6
- metadata.gz: 0a04fdb5ab370ed5736208cbd4ccb1e6da801af52cd68004625a21008c4a10a04bc143d99c9e1a71bccb9fad882fc3cff27d9c0900689dbd5cf6c0616e4d43a0
7
- data.tar.gz: e6d5cb6e8ff31d64eb588fa39ad6a1e7bb1ae9416adac64e5f9a21bf451ffd4115a8c2d3bb8759f1a32192a88a4a401336e1baca7621a33934dc1b2a873c8402
6
+ metadata.gz: 6331ba7a98a83f27d5ab3accb21dcf0676d43b21b76b98bf2349b688eeec5eaa1842e3c7e340cbc8a4109f40186ddabfc9d6daf679860b6438f1bb0520c6b383
7
+ data.tar.gz: 3b3b5f1c512a55fbcd6308cebf1e638bc9fe0a1e720f2acbde3b2cf52ca76431e668f74ae7070bb1f031c9e5b63fa307deab4b525c033caa00aa898d8505bb34
@@ -2,6 +2,114 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ### 0.4.2
6
+
7
+ ### Bugfixes
8
+
9
+ * database entensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
10
+
11
+ ### 0.4.1
12
+
13
+ ### Improvements
14
+
15
+ When in "Resource Server" mode, calling `rodauth.authorization_token` will now return an hash of the JSON payload that the Authorization Server responds, and which was already previously used to authorize access to protected resources.
16
+
17
+ ### Bugfixes
18
+
19
+ * An error ocurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
20
+
21
+ ### 0.4.0
22
+
23
+ ### Features
24
+
25
+ * A new method, `get_additional_param(account, claim)`, is now exposed; this method will be called whenever non-OIDC scopes are requested in the emission of the ID token.
26
+
27
+ * The `form_post` response is now supported, either by passing the `response_mode=form_post` request param in the authorization URL, or by setting `oauth_response_mode "form_post"` option. This improves the overall security of an Authorization server even more, as authorization codes are sent to client applications via a POST request to the redirect URI.
28
+
29
+
30
+ ### Improvements
31
+
32
+ * For the OIDC `address` scope, proper claims are now emitted as per the standard, i.e. the "formatted", "street_address", "locality", "region", "postal_code", "country". These will be the ones referenced in the `get_oidc_param` method.
33
+
34
+ ### Bugfixes
35
+
36
+ * The rails templates were missing declarations from a few params, which made some of the flows (the PKCE for example) not work out-of-the box;
37
+ * rails tests were silently not running in CI;
38
+ * The CI suite was revamped, so that all Oauth tests would be run under rails as well. All versions from rails equal or above 5.0 are now targeted;
39
+
40
+ ### 0.3.0
41
+
42
+ #### Features
43
+
44
+ * `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 performed 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.
45
+
46
+ #### Improvements
47
+
48
+
49
+ * 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).
50
+
51
+ * Refresh Tokens are now expirable. 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 access token expired.
52
+
53
+ #### Bugfixes
54
+
55
+ * Default Templates now being packaged, as a way to provide a default experience to the OAuth journeys.
56
+
57
+ * fixing metadata urls when plugin loaded with a prefix path (@ianks)
58
+
59
+ * 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.
60
+
61
+ * OIDC configuration endpoint is now stricter, eliminating JSON metadata inherited from the Oauth metadata endpoint. (@ianks)
62
+
63
+ #### Chore
64
+
65
+ Use `rodauth.convert_timestamp` in the templates, whenever dates are displayed.
66
+
67
+ 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).
68
+
69
+ ### 0.2.0
70
+
71
+ #### Features
72
+
73
+ ##### SAML Assertion Grant Type
74
+
75
+ `rodauth-auth` now supports using a SAML Assertion to request for an Access token.In order to enable, you have to:
76
+
77
+ ```ruby
78
+ plugin :rodauth do
79
+ enable :oauth_saml
80
+ end
81
+ ```
82
+
83
+ For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/SAML-Assertion-Access-Tokens).
84
+
85
+ ##### Supporting rotating keys
86
+
87
+ At some point, you'll want to replace the pkeys and algorithm used to generate and verify the JWT access tokens, but you want to keep validating previously-distributed JWT tokens, at least until they expire. Now you can, via two new options, `oauth_jwt_legacy_public_key` and `oauth_jwt_legacy_algorithm`, which will be declared in the JWKs URI and used to verify access tokens.
88
+
89
+
90
+ ##### Reuse access tokens
91
+
92
+ If the `oauth_reuse_access_token` is set, if there's already an existing valid access token, any new grant for the same application / account / scope will keep the same access token. This can be helpful in scenarios where one wants the same access token distributed across devices.
93
+
94
+ ##### require_authorizable_account
95
+
96
+ The method used to verify access to the authorize flow is called `require_authorizable_account`. By default, it checks if a user is logged in by using rodauth's own `require_account`. This is the method you'd want to redefine in order to augment these requirements, i.e. request 2fa authentication.
97
+
98
+ #### Improvements
99
+
100
+ Expired and revoked access tokens end up generating a lot of garbage, which will have to be periodically cleaned up. You can mitigate this now by setting a uniqueness index for a group of columns, i.e. if you set a uniqueness index for the `oauth_application_id/account_id/scopes` column, `rodauth-oauth` will transparently reuse the same db entry to store the new access token. If setting some other type of uniqueness index, make sure to update the option `oauth_tokens_unique_columns` (the array of columns from the uniqueness index).
101
+
102
+ #### Bugfixes
103
+
104
+ Calling `before_*_route` callbacks appropriately.
105
+
106
+ Fixed some mishandling of HTTP headers when in in resource-server mode.
107
+
108
+ #### Chore
109
+
110
+ * 97.7% test coverage;
111
+ * `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
112
+
5
113
  ### 0.1.0
6
114
 
7
115
  (31/7/2020)
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
 
@@ -21,6 +21,7 @@ This gem implements the following RFCs and features of OAuth:
21
21
  * Access Type (Token refresh online and offline);
22
22
  * [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
23
23
  * [JWT Acess Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
24
+ * [SAML 2.0 Assertion Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-03);
24
25
  * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
25
26
  * OAuth application and token management dashboards;
26
27
 
@@ -43,14 +43,14 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
43
43
  t.foreign_key :oauth_tokens, column: :oauth_token_id
44
44
  t.integer :oauth_application_id
45
45
  t.foreign_key :oauth_applications, column: :oauth_application_id
46
- t.string :token, null: false, token: true
46
+ t.string :token, null: false, token: true, unique: true
47
47
  # uncomment if setting oauth_tokens_token_hash_column
48
48
  # and delete the token column
49
- # t.string :token_hash, token: true
50
- t.string :refresh_token
49
+ # t.string :token_hash, token: true, unique: true
50
+ t.string :refresh_token, unique: true
51
51
  # uncomment if setting oauth_tokens_refresh_token_hash_column
52
52
  # and delete the refresh_token column
53
- # t.string :refresh_token_hash, token: true
53
+ # t.string :refresh_token_hash, token: true, unique: true
54
54
  t.datetime :expires_in, null: false
55
55
  t.datetime :revoked_at
56
56
  t.string :scopes, null: false
@@ -1,16 +1,21 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "time"
3
4
  require "base64"
4
5
  require "securerandom"
5
6
  require "net/http"
6
7
 
7
8
  require "rodauth/oauth/ttl_store"
9
+ require "rodauth/oauth/database_extensions"
8
10
 
9
11
  module Rodauth
10
12
  Feature.define(:oauth) do
11
13
  # RUBY EXTENSIONS
12
- # :nocov:
13
14
  unless Regexp.method_defined?(:match?)
15
+ # If you wonder why this is there: the oauth feature uses a refinement to enhance the
16
+ # Regexp class locally with #match? , but this is never tested, because ActiveSupport
17
+ # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
18
+ # :nocov:
14
19
  module RegexpExtensions
15
20
  refine(Regexp) do
16
21
  def match?(*args)
@@ -19,6 +24,7 @@ module Rodauth
19
24
  end
20
25
  end
21
26
  using(RegexpExtensions)
27
+ # :nocov:
22
28
  end
23
29
 
24
30
  unless String.method_defined?(:delete_suffix!)
@@ -37,10 +43,11 @@ module Rodauth
37
43
  end
38
44
  using(SuffixExtensions)
39
45
  end
40
- # :nocov:
41
46
 
42
47
  SCOPES = %w[profile.read].freeze
43
48
 
49
+ SERVER_METADATA = OAuth::TtlStore.new
50
+
44
51
  before "authorize"
45
52
  after "authorize"
46
53
 
@@ -70,12 +77,14 @@ module Rodauth
70
77
 
71
78
  auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
72
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
73
81
  auth_value_method :use_oauth_implicit_grant_type?, false
74
82
  auth_value_method :use_oauth_pkce?, true
75
83
  auth_value_method :use_oauth_access_type?, true
76
84
 
77
85
  auth_value_method :oauth_require_pkce, false
78
86
  auth_value_method :oauth_pkce_challenge_method, "S256"
87
+ auth_value_method :oauth_response_mode, "query"
79
88
 
80
89
  auth_value_method :oauth_valid_uri_schemes, %w[https]
81
90
 
@@ -92,6 +101,7 @@ module Rodauth
92
101
  button "Register", "oauth_application"
93
102
  button "Authorize", "oauth_authorize"
94
103
  button "Revoke", "oauth_token_revoke"
104
+ button "Back to Client Application", "oauth_authorize_post"
95
105
 
96
106
  # OAuth Token
97
107
  auth_value_method :oauth_tokens_path, "oauth-tokens"
@@ -110,6 +120,8 @@ module Rodauth
110
120
  auth_value_method :oauth_tokens_token_hash_column, nil
111
121
  auth_value_method :oauth_tokens_refresh_token_hash_column, nil
112
122
 
123
+ # Access Token reuse
124
+ auth_value_method :oauth_reuse_access_token, false
113
125
  # OAuth Grants
114
126
  auth_value_method :oauth_grants_table, :oauth_grants
115
127
  auth_value_method :oauth_grants_id_column, :id
@@ -124,6 +136,7 @@ module Rodauth
124
136
 
125
137
  auth_value_method :authorization_required_error_status, 401
126
138
  auth_value_method :invalid_oauth_response_status, 400
139
+ auth_value_method :already_in_use_response_status, 409
127
140
 
128
141
  # OAuth Applications
129
142
  auth_value_method :oauth_applications_path, "oauth-applications"
@@ -141,9 +154,11 @@ module Rodauth
141
154
  auth_value_method :"oauth_applications_#{column}_column", column
142
155
  end
143
156
 
157
+ # Feature options
144
158
  auth_value_method :oauth_application_default_scope, SCOPES.first
145
159
  auth_value_method :oauth_application_scopes, SCOPES
146
160
  auth_value_method :oauth_token_type, "bearer"
161
+ auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
147
162
 
148
163
  auth_value_method :invalid_client_message, "Invalid client"
149
164
  auth_value_method :invalid_grant_type_message, "Invalid grant type"
@@ -155,6 +170,8 @@ module Rodauth
155
170
 
156
171
  auth_value_method :unique_error_message, "is already in use"
157
172
  auth_value_method :null_error_message, "is not filled"
173
+ auth_value_method :already_in_use_message, "error generating unique token"
174
+ auth_value_method :already_in_use_error_code, "invalid_request"
158
175
 
159
176
  # PKCE
160
177
  auth_value_method :code_challenge_required_error_code, "invalid_request"
@@ -172,6 +189,8 @@ module Rodauth
172
189
  # Only required to use if the plugin is to be used in a resource server
173
190
  auth_value_method :is_authorization_server?, true
174
191
 
192
+ auth_value_method :oauth_unique_id_generation_retries, 3
193
+
175
194
  auth_value_methods(
176
195
  :fetch_access_token,
177
196
  :oauth_unique_id_generator,
@@ -179,14 +198,223 @@ module Rodauth
179
198
  :secret_hash,
180
199
  :generate_token_hash,
181
200
  :authorization_server_url,
182
- :before_introspection_request
201
+ :before_introspection_request,
202
+ :require_authorizable_account,
203
+ :oauth_tokens_unique_columns
183
204
  )
184
205
 
185
206
  auth_value_methods(:only_json?)
186
207
 
187
208
  auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
188
209
 
189
- SERVER_METADATA = OAuth::TtlStore.new
210
+ # /token
211
+ route(:token) do |r|
212
+ next unless is_authorization_server?
213
+
214
+ before_token_route
215
+ require_oauth_application
216
+
217
+ r.post do
218
+ catch_error do
219
+ validate_oauth_token_params
220
+
221
+ oauth_token = nil
222
+ transaction do
223
+ before_token
224
+ oauth_token = create_oauth_token
225
+ end
226
+
227
+ json_response_success(json_access_token_payload(oauth_token))
228
+ end
229
+
230
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
231
+ end
232
+ end
233
+
234
+ # /introspect
235
+ route(:introspect) do |r|
236
+ next unless is_authorization_server?
237
+
238
+ before_introspect_route
239
+
240
+ r.post do
241
+ catch_error do
242
+ validate_oauth_introspect_params
243
+
244
+ before_introspect
245
+ oauth_token = case param("token_type_hint")
246
+ when "access_token"
247
+ oauth_token_by_token(param("token"))
248
+ when "refresh_token"
249
+ oauth_token_by_refresh_token(param("token"))
250
+ else
251
+ oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
252
+ end
253
+
254
+ if oauth_application
255
+ redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
256
+ elsif oauth_token
257
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
258
+ oauth_token[oauth_tokens_oauth_application_id_column]).first
259
+ end
260
+
261
+ json_response_success(json_token_introspect_payload(oauth_token))
262
+ end
263
+
264
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
265
+ end
266
+ end
267
+
268
+ # /revoke
269
+ route(:revoke) do |r|
270
+ next unless is_authorization_server?
271
+
272
+ before_revoke_route
273
+ require_oauth_application
274
+
275
+ r.post do
276
+ catch_error do
277
+ validate_oauth_revoke_params
278
+
279
+ oauth_token = nil
280
+ transaction do
281
+ before_revoke
282
+ oauth_token = revoke_oauth_token
283
+ after_revoke
284
+ end
285
+
286
+ if accepts_json?
287
+ json_response_success \
288
+ "token" => oauth_token[oauth_tokens_token_column],
289
+ "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
290
+ "revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
291
+ else
292
+ set_notice_flash revoke_oauth_token_notice_flash
293
+ redirect request.referer || "/"
294
+ end
295
+ end
296
+
297
+ redirect_response_error("invalid_request", request.referer || "/")
298
+ end
299
+ end
300
+
301
+ # /authorize
302
+ route(:authorize) do |r|
303
+ next unless is_authorization_server?
304
+
305
+ before_authorize_route
306
+ require_authorizable_account
307
+
308
+ validate_oauth_grant_params
309
+ try_approval_prompt if use_oauth_access_type? && request.get?
310
+
311
+ r.get do
312
+ authorize_view
313
+ end
314
+
315
+ r.post do
316
+ redirect_url = URI.parse(redirect_uri)
317
+
318
+ params, mode = transaction do
319
+ before_authorize
320
+ do_authorize
321
+ end
322
+
323
+ case mode
324
+ when "query"
325
+ params = params.map { |k, v| "#{k}=#{v}" }
326
+ params << redirect_url.query if redirect_url.query
327
+ redirect_url.query = params.join("&")
328
+ redirect(redirect_url.to_s)
329
+ when "fragment"
330
+ params = params.map { |k, v| "#{k}=#{v}" }
331
+ params << redirect_url.query if redirect_url.query
332
+ redirect_url.fragment = params.join("&")
333
+ redirect(redirect_url.to_s)
334
+ when "form_post"
335
+ scope.view layout: false, inline: <<-FORM
336
+ <html>
337
+ <head><title>Authorized</title></head>
338
+ <body onload="javascript:document.forms[0].submit()">
339
+ <form method="post" action="#{redirect_uri}">
340
+ #{
341
+ params.map do |name, value|
342
+ "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
343
+ end.join
344
+ }
345
+ <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
346
+ </form>
347
+ </body>
348
+ </html>
349
+ FORM
350
+ when "none"
351
+ redirect(redirect_url.to_s)
352
+ end
353
+ end
354
+ end
355
+
356
+ def oauth_server_metadata(issuer = nil)
357
+ request.on(".well-known") do
358
+ request.on("oauth-authorization-server") do
359
+ request.get do
360
+ json_response_success(oauth_server_metadata_body(issuer), true)
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ # /oauth-applications routes
367
+ def oauth_applications
368
+ request.on(oauth_applications_path) do
369
+ require_account
370
+
371
+ request.get "new" do
372
+ new_oauth_application_view
373
+ end
374
+
375
+ request.on(oauth_applications_id_pattern) do |id|
376
+ oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
377
+ next unless oauth_application
378
+
379
+ scope.instance_variable_set(:@oauth_application, oauth_application)
380
+
381
+ request.is do
382
+ request.get do
383
+ oauth_application_view
384
+ end
385
+ end
386
+
387
+ request.on(oauth_tokens_path) do
388
+ oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
389
+ scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
390
+ request.get do
391
+ oauth_tokens_view
392
+ end
393
+ end
394
+ end
395
+
396
+ request.get do
397
+ scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
398
+ oauth_applications_view
399
+ end
400
+
401
+ request.post do
402
+ catch_error do
403
+ validate_oauth_application_params
404
+
405
+ transaction do
406
+ before_create_oauth_application
407
+ id = create_oauth_application
408
+ after_create_oauth_application
409
+ set_notice_flash create_oauth_application_notice_flash
410
+ redirect "#{request.path}/#{id}"
411
+ end
412
+ end
413
+ set_error_flash create_oauth_application_error_flash
414
+ new_oauth_application_view
415
+ end
416
+ end
417
+ end
190
418
 
191
419
  def check_csrf?
192
420
  case request.path
@@ -213,14 +441,12 @@ module Rodauth
213
441
  end
214
442
 
215
443
  unless method_defined?(:json_request?)
216
- # :nocov:
217
444
  # copied from the jwt feature
218
445
  def json_request?
219
446
  return @json_request if defined?(@json_request)
220
447
 
221
448
  @json_request = request.content_type =~ json_request_regexp
222
449
  end
223
- # :nocov:
224
450
  end
225
451
 
226
452
  def initialize(scope)
@@ -263,13 +489,13 @@ module Rodauth
263
489
  def fetch_access_token
264
490
  value = request.env["HTTP_AUTHORIZATION"]
265
491
 
266
- return unless value
492
+ return unless value && !value.empty?
267
493
 
268
494
  scheme, token = value.split(" ", 2)
269
495
 
270
496
  return unless scheme.downcase == oauth_token_type
271
497
 
272
- return if token.empty?
498
+ return if token.nil? || token.empty?
273
499
 
274
500
  token
275
501
  end
@@ -282,101 +508,84 @@ module Rodauth
282
508
 
283
509
  return unless bearer_token
284
510
 
285
- # check if token has not expired
286
- # check if token has been revoked
287
- @authorization_token = oauth_token_by_token(bearer_token)
511
+ @authorization_token = if is_authorization_server?
512
+ # check if token has not expired
513
+ # check if token has been revoked
514
+ oauth_token_by_token(bearer_token)
515
+ else
516
+ # where in resource server, NOT the authorization server.
517
+ payload = introspection_request("access_token", bearer_token)
518
+
519
+ return unless payload["active"]
520
+
521
+ payload
522
+ end
288
523
  end
289
524
 
290
525
  def require_oauth_authorization(*scopes)
291
- token_scopes = if is_authorization_server?
292
- authorization_required unless authorization_token
526
+ authorization_required unless authorization_token
293
527
 
294
- scopes << oauth_application_default_scope if scopes.empty?
528
+ scopes << oauth_application_default_scope if scopes.empty?
295
529
 
530
+ token_scopes = if is_authorization_server?
296
531
  authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
297
532
  else
298
- bearer_token = fetch_access_token
299
-
300
- authorization_required unless bearer_token
301
-
302
- scopes << oauth_application_default_scope if scopes.empty?
303
-
304
- # where in resource server, NOT the authorization server.
305
- payload = introspection_request("access_token", bearer_token)
306
-
307
- authorization_required unless payload["active"]
308
-
309
- payload["scope"].split(oauth_scope_separator)
533
+ aux_scopes = authorization_token["scope"]
534
+ if aux_scopes
535
+ aux_scopes.split(oauth_scope_separator)
536
+ else
537
+ []
538
+ end
310
539
  end
311
540
 
312
541
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
313
542
  end
314
543
 
315
- # /oauth-applications routes
316
- def oauth_applications
317
- request.on(oauth_applications_path) do
318
- require_account
319
-
320
- request.get "new" do
321
- new_oauth_application_view
322
- end
544
+ def post_configure
545
+ super
323
546
 
324
- request.on(oauth_applications_id_pattern) do |id|
325
- oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
326
- next unless oauth_application
327
-
328
- scope.instance_variable_set(:@oauth_application, oauth_application)
547
+ # all of the extensions below involve DB changes. Resource server mode doesn't use
548
+ # database functions for OAuth though.
549
+ return unless is_authorization_server?
329
550
 
330
- request.is do
331
- request.get do
332
- oauth_application_view
333
- end
334
- end
551
+ self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
335
552
 
336
- request.on(oauth_tokens_path) do
337
- oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
338
- scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
339
- request.get do
340
- oauth_tokens_view
341
- end
342
- end
553
+ # Check whether we can reutilize db entries for the same account / application pair
554
+ one_oauth_token_per_account = begin
555
+ db.indexes(oauth_tokens_table).values.any? do |definition|
556
+ definition[:unique] &&
557
+ definition[:columns] == oauth_tokens_unique_columns
343
558
  end
559
+ end
560
+ self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
561
+ end
344
562
 
345
- request.get do
346
- scope.instance_variable_set(:@oauth_applications, db[:oauth_applications])
347
- oauth_applications_view
348
- end
563
+ def use_date_arithmetic?
564
+ true
565
+ end
349
566
 
350
- request.post do
351
- catch_error do
352
- validate_oauth_application_params
567
+ private
353
568
 
354
- transaction do
355
- before_create_oauth_application
356
- id = create_oauth_application
357
- after_create_oauth_application
358
- set_notice_flash create_oauth_application_notice_flash
359
- redirect "#{request.path}/#{id}"
360
- end
361
- end
362
- set_error_flash create_oauth_application_error_flash
363
- new_oauth_application_view
364
- end
569
+ def rescue_from_uniqueness_error(&block)
570
+ retries = oauth_unique_id_generation_retries
571
+ begin
572
+ transaction(savepoint: :only, &block)
573
+ rescue Sequel::UniqueConstraintViolation
574
+ redirect_response_error("already_in_use") if retries.zero?
575
+ retries -= 1
576
+ retry
365
577
  end
366
578
  end
367
579
 
368
- def oauth_server_metadata(issuer = nil)
369
- request.on(".well-known") do
370
- request.on("oauth-authorization-server") do
371
- request.get do
372
- json_response_success(oauth_server_metadata_body(issuer))
373
- end
374
- end
375
- end
580
+ # OAuth Token Unique/Reuse
581
+ def oauth_tokens_unique_columns
582
+ [
583
+ oauth_tokens_oauth_application_id_column,
584
+ oauth_tokens_account_id_column,
585
+ oauth_tokens_scopes_column
586
+ ]
376
587
  end
377
588
 
378
- private
379
-
380
589
  def authorization_server_url
381
590
  base_url
382
591
  end
@@ -399,10 +608,10 @@ module Rodauth
399
608
 
400
609
  # time-to-live
401
610
  ttl = if response.key?("cache-control")
402
- cache_control = response["cache_control"]
403
- cache_control[/max-age=(\d+)/, 1]
611
+ cache_control = response["cache-control"]
612
+ cache_control[/max-age=(\d+)/, 1].to_i
404
613
  elsif response.key?("expires")
405
- Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
614
+ Time.parse(response["expires"]).to_i - Time.now.to_i
406
615
  end
407
616
 
408
617
  [JSON.parse(response.body, symbolize_names: true), ttl]
@@ -470,7 +679,7 @@ module Rodauth
470
679
  end
471
680
 
472
681
  def oauth_unique_id_generator
473
- SecureRandom.hex(32)
682
+ SecureRandom.urlsafe_base64(32)
474
683
  end
475
684
 
476
685
  def generate_token_hash(token)
@@ -482,89 +691,106 @@ module Rodauth
482
691
  end
483
692
 
484
693
  unless method_defined?(:password_hash)
485
- # :nocov:
486
694
  # From login_requirements_base feature
487
- if ENV["RACK_ENV"] == "test"
488
- def password_hash_cost
489
- BCrypt::Engine::MIN_COST
490
- end
491
- else
492
- def password_hash_cost
493
- BCrypt::Engine::DEFAULT_COST
494
- end
495
- end
496
695
 
497
696
  def password_hash(password)
498
- BCrypt::Password.create(password, cost: password_hash_cost)
697
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
499
698
  end
500
- # :nocov:
501
699
  end
502
700
 
503
701
  def generate_oauth_token(params = {}, should_generate_refresh_token = true)
504
702
  create_params = {
505
- oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
703
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
506
704
  }.merge(params)
507
705
 
508
- token = oauth_unique_id_generator
509
- refresh_token = nil
706
+ rescue_from_uniqueness_error do
707
+ token = oauth_unique_id_generator
510
708
 
511
- if oauth_tokens_token_hash_column
512
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
513
- else
514
- create_params[oauth_tokens_token_column] = token
515
- end
709
+ if oauth_tokens_token_hash_column
710
+ create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
711
+ else
712
+ create_params[oauth_tokens_token_column] = token
713
+ end
516
714
 
517
- if should_generate_refresh_token
518
- refresh_token = oauth_unique_id_generator
715
+ refresh_token = nil
716
+ if should_generate_refresh_token
717
+ refresh_token = oauth_unique_id_generator
519
718
 
520
- if oauth_tokens_refresh_token_hash_column
521
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
522
- else
523
- create_params[oauth_tokens_refresh_token_column] = refresh_token
719
+ if oauth_tokens_refresh_token_hash_column
720
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
721
+ else
722
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
723
+ end
524
724
  end
725
+ oauth_token = _generate_oauth_token(create_params)
726
+ oauth_token[oauth_tokens_token_column] = token
727
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
728
+ oauth_token
525
729
  end
526
- oauth_token = _generate_oauth_token(create_params)
527
-
528
- oauth_token[oauth_tokens_token_column] = token
529
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
530
- oauth_token
531
730
  end
532
731
 
533
732
  def _generate_oauth_token(params = {})
534
733
  ds = db[oauth_tokens_table]
535
734
 
536
- begin
537
- if ds.supports_returning?(:insert)
538
- ds.returning.insert(params).first
539
- else
540
- id = ds.insert(params)
541
- ds.where(oauth_tokens_id_column => id).first
735
+ if __one_oauth_token_per_account
736
+
737
+ token = __insert_or_update_and_return__(
738
+ ds,
739
+ oauth_tokens_id_column,
740
+ oauth_tokens_unique_columns,
741
+ params,
742
+ Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
743
+ ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
744
+ )
745
+
746
+ # if the previous operation didn't return a row, it means that the conditions
747
+ # invalidated the update, and the existing token is still valid.
748
+ token || ds.where(
749
+ oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
750
+ oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
751
+ ).first
752
+ else
753
+ if oauth_reuse_access_token
754
+ unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
755
+ valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
756
+ .where(unique_conds).first
757
+ return valid_token if valid_token
542
758
  end
543
- rescue Sequel::UniqueConstraintViolation
544
- retry
759
+ __insert_and_return__(ds, oauth_tokens_id_column, params)
545
760
  end
546
761
  end
547
762
 
548
- def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
763
+ def oauth_token_by_token(token)
764
+ ds = db[oauth_tokens_table]
765
+
549
766
  ds = if oauth_tokens_token_hash_column
550
- dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
767
+ ds.where(oauth_tokens_token_hash_column => generate_token_hash(token))
551
768
  else
552
- dataset.where(oauth_tokens_token_column => token)
769
+ ds.where(oauth_tokens_token_column => token)
553
770
  end
554
771
 
555
772
  ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
556
773
  .where(oauth_tokens_revoked_at_column => nil).first
557
774
  end
558
775
 
559
- def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
776
+ def oauth_token_by_refresh_token(token, revoked: false)
777
+ ds = db[oauth_tokens_table]
778
+ #
779
+ # filter expired refresh tokens out.
780
+ # an expired refresh token is a token whose access token expired for a period longer than the
781
+ # refresh token expiration period.
782
+ #
783
+ ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
784
+
560
785
  ds = if oauth_tokens_refresh_token_hash_column
561
- dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
786
+ ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
562
787
  else
563
- dataset.where(oauth_tokens_refresh_token_column => token)
788
+ ds.where(oauth_tokens_refresh_token_column => token)
564
789
  end
565
790
 
566
- ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
567
- .where(oauth_tokens_revoked_at_column => nil).first
791
+ ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
792
+
793
+ ds.first
568
794
  end
569
795
 
570
796
  def json_access_token_payload(oauth_token)
@@ -633,7 +859,6 @@ module Rodauth
633
859
  # set client ID/secret pairs
634
860
 
635
861
  create_params.merge! \
636
- oauth_applications_client_id_column => oauth_unique_id_generator,
637
862
  oauth_applications_client_secret_column => \
638
863
  secret_hash(oauth_application_params[oauth_application_client_secret_param])
639
864
 
@@ -643,29 +868,14 @@ module Rodauth
643
868
  oauth_application_default_scope
644
869
  end
645
870
 
646
- id = nil
647
- raised = begin
648
- id = db[oauth_applications_table].insert(create_params)
649
- false
650
- rescue Sequel::ConstraintViolation => e
651
- e
652
- end
653
-
654
- if raised
655
- field = raised.message[/\.(.*)$/, 1]
656
- case raised
657
- when Sequel::UniqueConstraintViolation
658
- throw_error(field, unique_error_message)
659
- when Sequel::NotNullConstraintViolation
660
- throw_error(field, null_error_message)
661
- end
871
+ rescue_from_uniqueness_error do
872
+ create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
873
+ db[oauth_applications_table].insert(create_params)
662
874
  end
663
-
664
- !raised && id
665
875
  end
666
876
 
667
877
  # Authorize
668
- def before_authorize
878
+ def require_authorizable_account
669
879
  require_account
670
880
  end
671
881
 
@@ -678,6 +888,9 @@ module Rodauth
678
888
  end
679
889
  redirect_response_error("invalid_scope") unless check_valid_scopes?
680
890
 
891
+ if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
892
+ redirect_response_error("invalid_request")
893
+ end
681
894
  validate_pkce_challenge_params if use_oauth_pkce?
682
895
  end
683
896
 
@@ -695,7 +908,6 @@ module Rodauth
695
908
  ).count.zero?
696
909
 
697
910
  # if there's a previous oauth grant for the params combo, it means that this user has approved before.
698
-
699
911
  request.env["REQUEST_METHOD"] = "POST"
700
912
  end
701
913
 
@@ -704,64 +916,52 @@ module Rodauth
704
916
  oauth_grants_account_id_column => account_id,
705
917
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
706
918
  oauth_grants_redirect_uri_column => redirect_uri,
707
- oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
919
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
708
920
  oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
709
921
  )
710
922
 
711
923
  # Access Type flow
712
- if use_oauth_access_type?
713
- if (access_type = param_or_nil("access_type"))
714
- create_params[oauth_grants_access_type_column] = access_type
715
- end
924
+ if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
925
+ create_params[oauth_grants_access_type_column] = access_type
716
926
  end
717
927
 
718
928
  # PKCE flow
719
- if use_oauth_pkce?
929
+ if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
930
+ code_challenge_method = param_or_nil("code_challenge_method")
720
931
 
721
- if (code_challenge = param_or_nil("code_challenge"))
722
- code_challenge_method = param_or_nil("code_challenge_method")
723
-
724
- create_params[oauth_grants_code_challenge_column] = code_challenge
725
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
726
- elsif oauth_require_pkce
727
- redirect_response_error("code_challenge_required")
728
- end
932
+ create_params[oauth_grants_code_challenge_column] = code_challenge
933
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
729
934
  end
730
935
 
731
936
  ds = db[oauth_grants_table]
732
937
 
733
- begin
734
- authorization_code = oauth_unique_id_generator
735
- create_params[oauth_grants_code_column] = authorization_code
736
- ds.insert(create_params)
737
- authorization_code
738
- rescue Sequel::UniqueConstraintViolation
739
- retry
938
+ rescue_from_uniqueness_error do
939
+ create_params[oauth_grants_code_column] = oauth_unique_id_generator
940
+ __insert_and_return__(ds, oauth_grants_id_column, create_params)
740
941
  end
942
+ create_params[oauth_grants_code_column]
741
943
  end
742
944
 
743
- def do_authorize(redirect_url, query_params = [], fragment_params = [])
945
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
744
946
  case param("response_type")
745
947
  when "token"
746
948
  redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
747
949
 
748
- fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
749
- when "code", "", nil
750
- query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
950
+ response_mode ||= "fragment"
951
+ response_params.replace(_do_authorize_token)
952
+ when "code"
953
+ response_mode ||= "query"
954
+ response_params.replace(_do_authorize_code)
955
+ when "none"
956
+ response_mode ||= "none"
957
+ when "", nil
958
+ response_mode ||= oauth_response_mode
959
+ response_params.replace(_do_authorize_code)
751
960
  end
752
961
 
753
- if param_or_nil("state")
754
- if !fragment_params.empty?
755
- fragment_params << "state=#{param('state')}"
756
- else
757
- query_params << "state=#{param('state')}"
758
- end
759
- end
962
+ response_params["state"] = param("state") if param_or_nil("state")
760
963
 
761
- query_params << redirect_url.query if redirect_url.query
762
-
763
- redirect_url.query = query_params.join("&") unless query_params.empty?
764
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
964
+ [response_params, response_mode]
765
965
  end
766
966
 
767
967
  def _do_authorize_code
@@ -781,10 +981,6 @@ module Rodauth
781
981
 
782
982
  # Access Tokens
783
983
 
784
- def before_token
785
- require_oauth_application
786
- end
787
-
788
984
  def validate_oauth_token_params
789
985
  unless (grant_type = param_or_nil("grant_type"))
790
986
  redirect_response_error("invalid_request")
@@ -824,18 +1020,32 @@ module Rodauth
824
1020
  }
825
1021
  create_oauth_token_from_authorization_code(oauth_grant, create_params)
826
1022
  when "refresh_token"
827
- # fetch oauth token
828
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
829
-
830
- redirect_response_error("invalid_grant") unless oauth_token
1023
+ # fetch potentially revoked oauth token
1024
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
1025
+
1026
+ if !oauth_token
1027
+ redirect_response_error("invalid_grant")
1028
+ elsif oauth_token[oauth_tokens_revoked_at_column]
1029
+ if oauth_refresh_token_protection_policy == "rotation"
1030
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
1031
+ #
1032
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
1033
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
1034
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
1035
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
1036
+ # forcing the legitimate client to obtain a fresh authorization grant.
1037
+
1038
+ db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
1039
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1040
+ end
1041
+ redirect_response_error("invalid_grant")
1042
+ end
831
1043
 
832
1044
  update_params = {
833
1045
  oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
834
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
1046
+ oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
835
1047
  }
836
1048
  create_oauth_token_from_token(oauth_token, update_params)
837
- else
838
- redirect_response_error("invalid_grant")
839
1049
  end
840
1050
  end
841
1051
 
@@ -864,29 +1074,38 @@ module Rodauth
864
1074
  def create_oauth_token_from_token(oauth_token, update_params)
865
1075
  redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
866
1076
 
867
- token = oauth_unique_id_generator
1077
+ rescue_from_uniqueness_error do
1078
+ oauth_tokens_ds = db[oauth_tokens_table]
1079
+ token = oauth_unique_id_generator
868
1080
 
869
- if oauth_tokens_token_hash_column
870
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
871
- else
872
- update_params[oauth_tokens_token_column] = token
873
- end
874
-
875
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
876
-
877
- oauth_token = begin
878
- if ds.supports_returning?(:update)
879
- ds.returning.update(update_params).first
1081
+ if oauth_tokens_token_hash_column
1082
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
880
1083
  else
881
- ds.update(update_params)
882
- ds.first
1084
+ update_params[oauth_tokens_token_column] = token
883
1085
  end
884
- rescue Sequel::UniqueConstraintViolation
885
- retry
886
- end
887
1086
 
888
- oauth_token[oauth_tokens_token_column] = token
889
- oauth_token
1087
+ oauth_token = if oauth_refresh_token_protection_policy == "rotation"
1088
+ insert_params = {
1089
+ **update_params,
1090
+ oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
1091
+ oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
1092
+ }
1093
+
1094
+ # revoke the refresh token
1095
+ oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1096
+ .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
1097
+
1098
+ insert_params[oauth_tokens_oauth_token_id_column] = oauth_token[oauth_tokens_id_column]
1099
+ __insert_and_return__(oauth_tokens_ds, oauth_tokens_id_column, insert_params)
1100
+ else
1101
+ # includes none
1102
+ ds = oauth_tokens_ds.where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
1103
+ __update_and_return__(ds, update_params)
1104
+ end
1105
+
1106
+ oauth_token[oauth_tokens_token_column] = token
1107
+ oauth_token
1108
+ end
890
1109
  end
891
1110
 
892
1111
  TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
@@ -895,8 +1114,8 @@ module Rodauth
895
1114
 
896
1115
  def validate_oauth_introspect_params
897
1116
  # check if valid token hint type
898
- if param_or_nil("token_type_hint")
899
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1117
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1118
+ redirect_response_error("unsupported_token_type")
900
1119
  end
901
1120
 
902
1121
  redirect_response_error("invalid_request") unless param_or_nil("token")
@@ -914,18 +1133,12 @@ module Rodauth
914
1133
  }
915
1134
  end
916
1135
 
917
- def before_introspect; end
918
-
919
1136
  # Token revocation
920
1137
 
921
- def before_revoke
922
- require_oauth_application
923
- end
924
-
925
1138
  def validate_oauth_revoke_params
926
1139
  # check if valid token hint type
927
- if param_or_nil("token_type_hint")
928
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1140
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
1141
+ redirect_response_error("unsupported_token_type")
929
1142
  end
930
1143
 
931
1144
  redirect_response_error("invalid_request") unless param_or_nil("token")
@@ -942,23 +1155,13 @@ module Rodauth
942
1155
 
943
1156
  redirect_response_error("invalid_request") unless oauth_token
944
1157
 
945
- if oauth_application
946
- redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
947
- else
948
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
949
- oauth_token[oauth_tokens_oauth_application_id_column]).first
950
- end
1158
+ redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
951
1159
 
952
1160
  update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
953
1161
 
954
1162
  ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
955
1163
 
956
- oauth_token = if ds.supports_returning?(:update)
957
- ds.returning.update(update_params).first
958
- else
959
- ds.update(update_params)
960
- ds.first
961
- end
1164
+ oauth_token = __update_and_return__(ds, update_params)
962
1165
 
963
1166
  oauth_token[oauth_tokens_token_column] = token
964
1167
  oauth_token
@@ -976,7 +1179,13 @@ module Rodauth
976
1179
 
977
1180
  def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
978
1181
  if accepts_json?
979
- throw_json_response_error(invalid_oauth_response_status, error_code)
1182
+ status_code = if respond_to?(:"#{error_code}_response_status")
1183
+ send(:"#{error_code}_response_status")
1184
+ else
1185
+ invalid_oauth_response_status
1186
+ end
1187
+
1188
+ throw_json_response_error(status_code, error_code)
980
1189
  else
981
1190
  redirect_url = URI.parse(redirect_url)
982
1191
  query_params = []
@@ -998,9 +1207,17 @@ module Rodauth
998
1207
  end
999
1208
  end
1000
1209
 
1001
- def json_response_success(body)
1210
+ def json_response_success(body, cache = false)
1002
1211
  response.status = 200
1003
1212
  response["Content-Type"] ||= json_response_content_type
1213
+ if cache
1214
+ # defaulting to 1-day for everyone, for now at least
1215
+ max_age = 60 * 60 * 24
1216
+ response["Cache-Control"] = "private, max-age=#{max_age}"
1217
+ else
1218
+ response["Cache-Control"] = "no-store"
1219
+ response["Pragma"] = "no-cache"
1220
+ end
1004
1221
  json_payload = _json_response_body(body)
1005
1222
  response.write(json_payload)
1006
1223
  request.halt
@@ -1023,7 +1240,6 @@ module Rodauth
1023
1240
  end
1024
1241
 
1025
1242
  unless method_defined?(:_json_response_body)
1026
- # :nocov:
1027
1243
  def _json_response_body(hash)
1028
1244
  if request.respond_to?(:convert_to_json)
1029
1245
  request.send(:convert_to_json, hash)
@@ -1031,7 +1247,6 @@ module Rodauth
1031
1247
  JSON.dump(hash)
1032
1248
  end
1033
1249
  end
1034
- # :nocov:
1035
1250
  end
1036
1251
 
1037
1252
  def authorization_required
@@ -1122,7 +1337,7 @@ module Rodauth
1122
1337
  issuer += "/#{path}" if path
1123
1338
 
1124
1339
  responses_supported = %w[code]
1125
- response_modes_supported = %w[query]
1340
+ response_modes_supported = %w[query form_post]
1126
1341
  grant_types_supported = %w[authorization_code]
1127
1342
 
1128
1343
  if use_oauth_implicit_grant_type?
@@ -1130,11 +1345,12 @@ module Rodauth
1130
1345
  response_modes_supported << "fragment"
1131
1346
  grant_types_supported << "implicit"
1132
1347
  end
1348
+
1133
1349
  {
1134
1350
  issuer: issuer,
1135
1351
  authorization_endpoint: authorize_url,
1136
1352
  token_endpoint: token_url,
1137
- registration_endpoint: "#{base_url}/#{oauth_applications_path}",
1353
+ registration_endpoint: route_url(oauth_applications_path),
1138
1354
  scopes_supported: oauth_application_scopes,
1139
1355
  response_types_supported: responses_supported,
1140
1356
  response_modes_supported: response_modes_supported,
@@ -1151,115 +1367,5 @@ module Rodauth
1151
1367
  code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1152
1368
  }
1153
1369
  end
1154
-
1155
- # /token
1156
- route(:token) do |r|
1157
- next unless is_authorization_server?
1158
-
1159
- before_token
1160
-
1161
- r.post do
1162
- catch_error do
1163
- validate_oauth_token_params
1164
-
1165
- oauth_token = nil
1166
- transaction do
1167
- oauth_token = create_oauth_token
1168
- end
1169
-
1170
- json_response_success(json_access_token_payload(oauth_token))
1171
- end
1172
-
1173
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1174
- end
1175
- end
1176
-
1177
- # /introspect
1178
- route(:introspect) do |r|
1179
- next unless is_authorization_server?
1180
-
1181
- before_introspect
1182
-
1183
- r.post do
1184
- catch_error do
1185
- validate_oauth_introspect_params
1186
-
1187
- oauth_token = case param("token_type_hint")
1188
- when "access_token"
1189
- oauth_token_by_token(param("token"))
1190
- when "refresh_token"
1191
- oauth_token_by_refresh_token(param("token"))
1192
- else
1193
- oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
1194
- end
1195
-
1196
- if oauth_application
1197
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
1198
- elsif oauth_token
1199
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
1200
- oauth_token[oauth_tokens_oauth_application_id_column]).first
1201
- end
1202
-
1203
- json_response_success(json_token_introspect_payload(oauth_token))
1204
- end
1205
-
1206
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1207
- end
1208
- end
1209
-
1210
- # /revoke
1211
- route(:revoke) do |r|
1212
- next unless is_authorization_server?
1213
-
1214
- before_revoke
1215
-
1216
- r.post do
1217
- catch_error do
1218
- validate_oauth_revoke_params
1219
-
1220
- oauth_token = nil
1221
- transaction do
1222
- oauth_token = revoke_oauth_token
1223
- after_revoke
1224
- end
1225
-
1226
- if accepts_json?
1227
- json_response_success \
1228
- "token" => oauth_token[oauth_tokens_token_column],
1229
- "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
1230
- "revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
1231
- else
1232
- set_notice_flash revoke_oauth_token_notice_flash
1233
- redirect request.referer || "/"
1234
- end
1235
- end
1236
-
1237
- redirect_response_error("invalid_request", request.referer || "/")
1238
- end
1239
- end
1240
-
1241
- # /authorize
1242
- route(:authorize) do |r|
1243
- next unless is_authorization_server?
1244
-
1245
- require_account
1246
- validate_oauth_grant_params
1247
- try_approval_prompt if use_oauth_access_type? && request.get?
1248
-
1249
- before_authorize
1250
-
1251
- r.get do
1252
- authorize_view
1253
- end
1254
-
1255
- r.post do
1256
- redirect_url = URI.parse(redirect_uri)
1257
-
1258
- transaction do
1259
- do_authorize(redirect_url)
1260
- end
1261
- redirect(redirect_url.to_s)
1262
- end
1263
- end
1264
1370
  end
1265
1371
  end