doorkeeper 6.0.0.beta2 → 6.0.0.rc2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -2
  3. data/README.md +29 -0
  4. data/app/controllers/doorkeeper/authorizations_controller.rb +66 -3
  5. data/app/controllers/doorkeeper/authorized_applications_controller.rb +23 -0
  6. data/app/controllers/doorkeeper/token_info_controller.rb +13 -1
  7. data/config/locales/en.yml +1 -0
  8. data/lib/doorkeeper/config/validations.rb +91 -0
  9. data/lib/doorkeeper/config.rb +18 -2
  10. data/lib/doorkeeper/errors.rb +14 -0
  11. data/lib/doorkeeper/grape/helpers.rb +11 -1
  12. data/lib/doorkeeper/helpers/controller.rb +12 -0
  13. data/lib/doorkeeper/http_fetcher.rb +79 -19
  14. data/lib/doorkeeper/models/access_token_mixin.rb +120 -2
  15. data/lib/doorkeeper/models/concerns/secret_storable.rb +72 -6
  16. data/lib/doorkeeper/oauth/authorization/token.rb +87 -8
  17. data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +13 -0
  18. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +27 -20
  19. data/lib/doorkeeper/oauth/client_credentials/creator.rb +17 -3
  20. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +14 -0
  21. data/lib/doorkeeper/oauth/metadata_response.rb +4 -3
  22. data/lib/doorkeeper/oauth/pre_authorization.rb +50 -0
  23. data/lib/doorkeeper/oauth/refresh_token_request.rb +81 -24
  24. data/lib/doorkeeper/oauth/resource_indicator_validator.rb +10 -0
  25. data/lib/doorkeeper/oauth/token.rb +94 -2
  26. data/lib/doorkeeper/oauth/token_introspection.rb +14 -1
  27. data/lib/doorkeeper/orm/active_record/mixins/access_grant.rb +1 -0
  28. data/lib/doorkeeper/orm/active_record/mixins/access_token.rb +1 -0
  29. data/lib/doorkeeper/orm/active_record/mixins/application.rb +1 -0
  30. data/lib/doorkeeper/orm/active_record/mixins/secret_storable.rb +113 -0
  31. data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +5 -1
  32. data/lib/doorkeeper/orm/active_record.rb +1 -0
  33. data/lib/doorkeeper/rails/helpers.rb +25 -2
  34. data/lib/doorkeeper/request.rb +19 -7
  35. data/lib/doorkeeper/version.rb +1 -1
  36. data/lib/doorkeeper.rb +11 -0
  37. data/lib/generators/doorkeeper/refresh_token_scopes_generator.rb +43 -0
  38. data/lib/generators/doorkeeper/templates/add_refresh_token_scopes_to_access_tokens.rb.erb +10 -0
  39. data/lib/generators/doorkeeper/templates/initializer.rb +94 -15
  40. data/lib/generators/doorkeeper/templates/migration.rb.erb +9 -0
  41. metadata +12 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b401160f58e48398c2e25631a27e5dabc423480e08f34a64c91c25107ffcab53
4
- data.tar.gz: 6c6dc01f00118c8525fe7ec329cab28cdaad05b5c5b06402cbb7f59ceb565e1a
3
+ metadata.gz: c6d4350f4bd6db700eb44e19c8078618b64e440bfd1edf8ec51ef306ec1a99a9
4
+ data.tar.gz: a284bba8ee06ab1bf53ca43fd8a803320c4d5fe1b2576f24633f3119b23582b0
5
5
  SHA512:
6
- metadata.gz: c394405b84f4b64de4db90e9ab8ea932f2899a2f3898d78df4d06add7765c6f5ae7a457e8e3c19ed69bf7592ffec8c3d1ab5c241f1a521e0347780ead79c233d
7
- data.tar.gz: 2275e90f19537ae4cda5e60ad1571ec29e0468e53b84d91bb165dea68960c2bf60d358194373ebd183b688d9043d86ef3a0e3685c0c38c7d4db8822ee7a98534
6
+ metadata.gz: 33c85db50f78fbef731514c037cda2fbbe40b3b7c3c25e671563781e946c02aed6084ff0a2cc5bae0409e7e2808ab4f30d67639f3274b039ac39cc27a8144e3f
7
+ data.tar.gz: 633fbee3c0259e194a1b183f3c959ef90194d721755bcad84ccf0d198173f94d88aed428b615aed3e3c3d83d2b8c01f2b6f317d036f074aa9b0c699180a2e2a8
data/CHANGELOG.md CHANGED
@@ -9,6 +9,45 @@ User-visible changes worth mentioning.
9
9
 
10
10
  - [#PR ID] Description of the change.
11
11
 
12
+ ## 6.0.0.rc2
13
+
14
+ **Please make sure** you read the [Upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
15
+ and changelog below before the update since this version includes breaking changes.
16
+
17
+ - [#1951] Fix: the built gem no longer contains `vendor/bundle`. The gemspec globed all of `vendor/`, which swept in the bundle installed by the release workflow; 6.0.0.rc1 is a 44.4 MB download against 154 KB for 6.0.0.beta2.
18
+ - Fix: `AuthorizedApplicationsController` now answers `401 Unauthorized` instead of running with a `nil` resource owner, which listed and revoked every token that has no resource owner — the ones the client credentials flow issues. Affected host applications are those whose `resource_owner_authenticator` answers `nil` without halting the request itself; the generated initializer's example redirects and is not affected.
19
+ - Fix: `AuthorizationsController#destroy` now validates the client and redirect URI before producing the deny response, and renders — never redirects — when validation fails. Previously the deny path performed no OAuth-layer validation at all, allowing an open redirect to an attacker-controlled origin with the OAuth `state` attached. Also reject unregistered `response_type` values on the authorization endpoint rather than resolving them through the `constantize` fallback.
20
+ - Fix: refuse redirect URIs with a script scheme (`javascript`, `vbscript`, `data`) both when an application is registered and at authorization time, regardless of `forbid_redirect_uri`. Such a URI is never a legitimate redirection endpoint, and with `response_mode=form_post` it became the action of the auto-submitting form the authorization server renders on its own origin. Already stored records with such a URI are now refused with `invalid_redirect_uri` before the consent screen is shown.
21
+ - [#1932] Fix: keep the scope originally granted by the resource owner on refresh tokens (RFC 6749 §6), so a chain narrowed on one refresh can return to its granted scope. Tracked in a new `refresh_token_scopes` column; existing installations opt in with `rails generate doorkeeper:refresh_token_scopes`.
22
+ - [#1933] Warn at boot when the `implicit` or `password` grant flow is enabled: both are deprecated by RFC 9700 (OAuth 2.0 Security BCP) and removed from OAuth 2.1, and may be removed in a future Doorkeeper release.
23
+ - [#1915] Fix: fetching a client's `jwks_uri` now falls back to the other addresses returned by DNS when the first one cannot be connected to.
24
+ - [#1934] The refresh_token grant now consults `custom_access_token_expires_in` (with `Doorkeeper::OAuth::REFRESH_TOKEN` as the context grant type) for the TTL of the refreshed access token. A callable that returns `nil` for this grant, or no callable at all, keeps inheriting the TTL of the token being refreshed as before. A callable that returns a value unconditionally now applies to refreshes as well.
25
+ - [#1935] Add opt-in `public_client_access_token_expires_in` configuration option: a ceiling for the lifetime of access tokens issued to public (non-confidential) clients by any grant, refresh_token included, as OAuth 2.1 Section 2.4 requires the exposure of tokens issued to unauthenticated clients to be limited. Confidential clients are not affected.
26
+ - [#1938] Fix: a request body ActionDispatch cannot parse (malformed JSON under a JSON content type, say) no longer raises `ActionDispatch::Http::Parameters::ParseError` out of `Doorkeeper::OAuth::Token.from_request` and `doorkeeper_token`. Since 5.9.7 the RFC 6750 §2 multi-method check read the body on every request, so such a request raised even when it carried a valid Bearer header. The body is now treated as carrying no token, the same way ActionDispatch's own `#filtered_parameters` treats that error.
27
+ - [#1950] Document `hash_token_secrets` / `hash_application_secrets` `fallback:` as a migration-period setting that should be removed once every row is hashed, and warn at boot for as long as one is configured. While a `:plain` fallback is active the stored value is itself a valid credential, so those columns need protecting as carefully as plaintext ones.
28
+ - [#1953] Fix: `public_client_access_token_expires_in` now also holds under `reuse_access_token` and with String TTLs, and the refresh_token grant hands `custom_access_token_expires_in` and `resource_indicator_validator` what every other grant does.
29
+
30
+ ## 6.0.0.rc1
31
+
32
+ **Please make sure** you read the [Upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
33
+ and changelog below before the update since this version includes breaking changes.
34
+
35
+ - Require Ruby >= 3.2 in the gemspec, matching the CI matrix (3.2 / 3.3 / 3.4 / 4.0). Ruby 2.7, 3.0 and 3.1 have reached end-of-life.
36
+ - Fix: the `client_secret_basic` strategy now requires a `client_id` sent in the request body to name the same client as the `Authorization: Basic` header — the RFC 7521 §4.2 agreement check `private_key_jwt` already applies to an assertion's issuer. A request presenting Basic credentials for one client and a `client_id` for another was authenticated as the Basic client, silently discarding the other identity. A bare `client_id` is not a client authentication method of its own, so the RFC 6749 §2.3 multiple-methods check does not (and should not) count it.
37
+ - [#1906] Internal: exempt `Doorkeeper::Config` from `Metrics/ClassLength` with a directive on the class itself instead of raising the cop's global ceiling, so adding a configuration option no longer trips the limit.
38
+ - [#1907] Fix: a `resource` parameter no longer produces a 500 at the authorization endpoint when `resource_indicator_validator` is configured without the `doorkeeper:resource_indicators` migration. Such a request is now answered with `server_error`, as the token endpoint already did, and the missing migration is warned about at boot.
39
+ - [#1909] Add Rails 8.1 to CI test matrix.
40
+ - [#1910] Add opt-in `validate_client_before_resource_owner_authentication` configuration option: the authorization endpoint validates `client_id` and `redirect_uri` before authenticating the resource owner, so users are not sent through login for a request that can only fail.
41
+ - [#1916] Fix broken Coveralls coverage reporting.
42
+ - [#1918] The api_only controller specs no longer `load` the real controller sources, which detached the coverage of every other example that ran them and made the reported coverage depend on the random example order.
43
+ - [#1923] Fix: the fallback secret upgrade no longer writes the matched secret back over a value stored in the meantime, which could undo a concurrent `#renew_secret` and leave the superseded secret valid. Active Record writes the upgrade conditionally on the column still holding the value that matched; other ORMs can implement the new `write_upgraded_secret` hook. The Active Record write is a single `update_all` statement, so model callbacks and validations no longer run on this upgrade (timestamps and optimistic locking are still maintained).
44
+ - [#1925] Internal: pin the development dependency on `json` below 3.0. json 3 removed the positional options Hash from `JSON.parse` and the `quirks_mode` option from `JSON.generate`, both of which Active Support still uses, so the suite could not run on any released Rails version.
45
+ - [#1926] **[BREAKING]** Fix: `private_key_jwt` client authentication no longer accepts an audience derived from the request's `Host` header, which let a client assertion minted for another authorization server be replayed here. A server that configures neither `issuer` nor Rails' `default_url_options[:host]` now has no acceptable audience and refuses every assertion, and is warned about it at boot.
46
+ - **[BREAKING]** Refuse requests that transmit the access token by more than one method (RFC 6750 §2) with an `invalid_request` error, instead of silently authorizing with the first method that yielded a token and discarding the rest. The form-encoded body (§2.2) and the URI query (§2.3) count as two methods even though Rails and Rack merge them into a single `params` hash, and the same token repeated across two methods is refused too — §2 forbids the second method, not a disagreement between the two.
47
+ - Only the built-in extraction methods take part in the check; a custom callable in `access_token_methods` keeps the historical first-wins behavior and is never invoked more than once.
48
+ - `Doorkeeper::OAuth::Token.from_request` / `.authenticate` raise `Doorkeeper::Errors::MultipleAccessTokenMethods`. `Doorkeeper.authenticate` and every `doorkeeper_token` helper (Rails, Grape, and Doorkeeper's own controllers) keep their token-or-nil contract, so `doorkeeper_authorize!` renders the refusal through a new `doorkeeper_bad_request_render_options(error:)` hook (`head 400` unless you override it).
49
+ - **Upgrade note**: under `handle_auth_errors :raise` these requests raise `Doorkeeper::Errors::InvalidRequest`, a sibling of `Doorkeeper::Errors::InvalidToken` rather than a subclass — an existing `rescue Doorkeeper::Errors::InvalidToken` does not cover it.
50
+
12
51
  ## 6.0.0.beta2
13
52
 
14
53
  **Please make sure** you read the [Upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
@@ -37,7 +76,6 @@ and changelog below before the update since this version includes breaking chang
37
76
  - [#1902] Fix: requests that omit `scope` now compute the same default scopes at the authorization and token endpoints (`Scopes#common`, symmetric). With dynamic scopes enabled, a scope pattern in either `default_scopes` or the application's scopes grants the matching concrete scope at both endpoints, closes [#1889].
38
77
  - [#1903] Fix: `revoke_previous_client_credentials_token` no longer revokes a client's live access token issued for a different `resource` ([#1886]), so a client can keep one audience-restricted token per resource server.
39
78
  - [#1905] [test] Cover `private_key_jwt` client authentication on the `client_credentials` grant, the one flow where an assertion is the client's only credential end to end. Test-only change.
40
- - Please add here
41
79
 
42
80
  ## 6.0.0.beta1
43
81
 
@@ -78,6 +116,15 @@ and changelog below before the update since this version includes breaking chang
78
116
  - [#1869] Improve test coverage
79
117
  - [#1870] Fix: raise the intended `Doorkeeper::Errors::TokenGeneratorNotFound` / `UnableToGenerateToken` (instead of a confusing `NameError`) when `application_secret_generator` is misconfigured.
80
118
 
119
+ ## 5.9.7
120
+
121
+ - Refuse requests that transmit an access token by more than one method (RFC 6750 §2), instead of silently authorizing with the first configured `access_token_methods` entry that matched and discarding the other tokens. Such a request now fails closed as carrying no usable token (401 `invalid_token`); no calling contract changes. The form-encoded body (§2.2) and the URI query (§2.3) count as two methods even though Rails and Rack merge them into a single `params` hash. The same token repeated across two methods is refused too — §2 forbids the second method, not a disagreement between the two — and a custom callable extractor in `access_token_methods` keeps the historical first-wins behavior and is never invoked more than once. (The strict `invalid_request` (400) answer §3.1 prescribes ships with Doorkeeper 6.0.)
122
+ - [#1925] Internal: pin the development dependency on `json` below 3.0. json 3 removed the positional options Hash from `JSON.parse` and the `quirks_mode` option from `JSON.generate`, both of which Active Support still uses, so the suite could not run on any supported Rails version.
123
+
124
+ ## 5.9.6
125
+
126
+ - Reject requests that present more than one client identity (e.g. an `Authorization: Basic` header for one client and a `client_id` parameter naming another) with an `invalid_request` error, instead of authenticating the first extracted identity and silently discarding the other one. A `client_id` sent alongside another authentication method keeps working when it identifies the same client (RFC 7521 §4.2). Like the RFC 6749 §2.3 check released in 5.9.5, this validation does not apply when `client_credentials` is configured with a callable extractor, since the credentials the remaining extractors would return are never evaluated — the `client_credentials` option documents that now.
127
+
81
128
  ## 5.9.5
82
129
 
83
130
  - [#1901] Reject requests that authenticate the client with more than one method (RFC 6749 §2.3) with an `invalid_request` error, instead of silently authenticating with the first method that matched and discarding the other credentials.
@@ -132,7 +179,6 @@ and changelog below before the update since this version includes breaking chang
132
179
  - [#1779] Only lock previous access token model when creating a new token from its refresh token if revoke_previous_refresh_token_on_use is false
133
180
  - [#1778] Ensure that token revocation is idempotent by checking that that token has not already been revoked before revoking.
134
181
 
135
-
136
182
  ## 5.8.2
137
183
 
138
184
  - [#1755] Fix the error message for force_pkce
data/README.md CHANGED
@@ -39,7 +39,9 @@ Supported features:
39
39
  - [Grape](#grape)
40
40
  - [ORMs](#orms)
41
41
  - [Extensions](#extensions)
42
+ - [Database maintenance](#database-maintenance)
42
43
  - [Resource Indicators](#resource-indicators)
44
+ - [Refresh Token Scopes](#refresh-token-scopes)
43
45
  - [Custom Grant Flows](#custom-grant-flows)
44
46
  - [Custom Client Authentication Methods](#custom-client-authentication-methods)
45
47
  - [Example Applications](#example-applications)
@@ -111,6 +113,18 @@ Extensions that are not included by default and can be installed separately.
111
113
  | CIBA - Client Initiated Backchannel Authentication Flow extension | [doorkeeper-ciba](https://github.com/autoseg/doorkeeper-ciba) |
112
114
  | Device Authorization Grant | [doorkeeper-device_authorization_grant](https://github.com/exop-group/doorkeeper-device_authorization_grant) |
113
115
 
116
+ ## Database maintenance
117
+
118
+ Doorkeeper does **not** automatically remove expired or revoked tokens and grants. The `oauth_access_tokens` and `oauth_access_grants` tables grow indefinitely and can reach millions of rows if left unmanaged.
119
+
120
+ Prune them periodically with the bundled rake task:
121
+
122
+ ```bash
123
+ bundle exec rake doorkeeper:db:cleanup
124
+ ```
125
+
126
+ This deletes expired and revoked access tokens and grants. See the [Rake tasks guide](https://doorkeeper.gitbook.io/guides/internals/rake) for details.
127
+
114
128
  ## Resource Indicators
115
129
 
116
130
  Doorkeeper supports [Resource Indicators for OAuth 2.0 (RFC 8707)](https://datatracker.ietf.org/doc/html/rfc8707), allowing clients to signal which protected resource(s) they intend to access. Tokens are then audience-restricted to those resources.
@@ -156,6 +170,21 @@ RFC 8707 uses repeated query parameters (`?resource=…&resource=…`) for multi
156
170
 
157
171
  A single `resource=…` works as-is.
158
172
 
173
+ ## Refresh Token Scopes
174
+
175
+ A client may narrow the access token it gets back from the refresh token grant by sending a `scope` parameter. Per [RFC 6749 §6](https://datatracker.ietf.org/doc/html/rfc6749#section-6) the refresh token itself keeps the scope originally granted by the resource owner: a later refresh that omits `scope` returns to the granted scope, and one that asks for the granted scope again is accepted.
176
+
177
+ Doorkeeper tracks the granted scope in the `refresh_token_scopes` column of `oauth_access_tokens`. New installs get it from the install migration; existing installs add it with:
178
+
179
+ ```bash
180
+ rails generate doorkeeper:refresh_token_scopes
181
+ rails db:migrate
182
+ ```
183
+
184
+ With the column in place, [token introspection](https://datatracker.ietf.org/doc/html/rfc7662#section-2.2) of a refresh token reports the granted scope rather than the scope of the access token it was issued with, and `reuse_access_token` does not hand a grant of a narrower scope an existing token whose refresh token was granted a wider one.
185
+
186
+ Without the column, a narrowed refresh narrows the refresh token as well, so the chain can never return to the granted scope (the behavior of Doorkeeper before the column existed), and a `refresh_token_scopes` assigned to an access token is ignored. Rows created before the migration keep that behavior until they are rotated, and so do the [ORM extensions](#extensions) (Sequel, MongoDB) until they add the field.
187
+
159
188
  ## Custom Grant Flows
160
189
 
161
190
  Besides the built-in OAuth 2 flows, Doorkeeper can recognize and process any custom grant type through its grant flow registry — including grant types whose names are URNs or URIs, such as the SAML 2.0 bearer assertion grant defined by [RFC 7522](https://www.rfc-editor.org/rfc/rfc7522).
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Doorkeeper
4
4
  class AuthorizationsController < Doorkeeper::ApplicationController
5
+ before_action :validate_client,
6
+ only: :new,
7
+ if: -> { Doorkeeper.config.validate_client_before_resource_owner_authentication? }
5
8
  before_action :authenticate_resource_owner!
6
9
 
7
10
  def new
@@ -16,7 +19,14 @@ module Doorkeeper
16
19
  redirect_or_render(authorize_response)
17
20
  end
18
21
 
22
+ # The deny path answers a request the client sent, so it is subject to the
23
+ # same client and redirect URI validation as #create: without it an
24
+ # unregistered redirect_uri was honoured and the user-agent redirected to
25
+ # it, carrying the OAuth `state` (and, since RFC 9207 support was added,
26
+ # the issuer) to an origin of the caller's choosing.
19
27
  def destroy
28
+ return if refuse_invalid_client?(pre_auth)
29
+
20
30
  redirect_or_render(authorization.deny)
21
31
  rescue Doorkeeper::Errors::InvalidTokenStrategy => e
22
32
  error_response = get_error_response_from_exception(e)
@@ -45,13 +55,66 @@ module Doorkeeper
45
55
 
46
56
  if Doorkeeper.configuration.redirect_on_errors? && pre_auth.error_response.redirectable?
47
57
  redirect_or_render(pre_auth.error_response)
48
- elsif Doorkeeper.configuration.api_only
49
- render json: pre_auth.error_response.body, status: pre_auth.error_response.status
50
58
  else
51
- render :error, locals: { error_response: pre_auth.error_response }, status: pre_auth.error_response.status
59
+ render_error_response(pre_auth.error_response)
52
60
  end
53
61
  end
54
62
 
63
+ def render_error_response(error_response)
64
+ if Doorkeeper.configuration.api_only
65
+ render json: error_response.body, status: error_response.status
66
+ else
67
+ render :error, locals: { error_response: error_response }, status: error_response.status
68
+ end
69
+ end
70
+
71
+ # Refuses the request before resource owner authentication when the
72
+ # client_id or redirect_uri is missing or invalid.
73
+ #
74
+ # Host applications that need to refuse a client on their own terms (an
75
+ # allow-list, a registry lookup) can override this and call +super+ first:
76
+ #
77
+ # class AuthorizationsController < Doorkeeper::AuthorizationsController
78
+ # private
79
+ #
80
+ # def validate_client
81
+ # super
82
+ # return if performed?
83
+ # return if MyRegistry.allowed?(client_pre_auth.client.application)
84
+ #
85
+ # head :forbidden
86
+ # end
87
+ # end
88
+ def validate_client
89
+ refuse_invalid_client?(client_pre_auth)
90
+ end
91
+
92
+ # Renders the error of +pre_authorization+ when its client_id or
93
+ # redirect_uri is missing or invalid, and answers whether the request was
94
+ # refused. Errors are rendered, never redirected, regardless of
95
+ # `handle_auth_errors :redirect`: no failure this check can produce leaves
96
+ # a redirect target worth trusting. Either the client failed first, so the
97
+ # redirect URI was never reached, or the redirect URI is itself the invalid
98
+ # one — and RFC 6749 Section 3.1.2.4 forbids redirecting the user-agent to
99
+ # an invalid redirection URI.
100
+ def refuse_invalid_client?(pre_authorization)
101
+ return false if pre_authorization.client_valid?
102
+
103
+ error_response = pre_authorization.error_response
104
+ error_response.raise_exception! if Doorkeeper.config.raise_on_errors?
105
+
106
+ render_error_response(error_response)
107
+ true
108
+ end
109
+
110
+ # The pre-authentication view of the request. It carries no resource owner
111
+ # (nobody is authenticated yet), so it is kept apart from #pre_auth:
112
+ # owner-dependent validations (authorize_resource_owner_for_client) and the
113
+ # views need the one built with current_resource_owner.
114
+ def client_pre_auth
115
+ @client_pre_auth ||= OAuth::PreAuthorization.new(Doorkeeper.configuration, pre_auth_params)
116
+ end
117
+
55
118
  def can_authorize_response?
56
119
  Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token?
57
120
  end
@@ -3,6 +3,7 @@
3
3
  module Doorkeeper
4
4
  class AuthorizedApplicationsController < Doorkeeper::ApplicationController
5
5
  before_action :authenticate_resource_owner!
6
+ before_action :validate_resource_owner
6
7
 
7
8
  def index
8
9
  @applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner)
@@ -29,5 +30,27 @@ module Doorkeeper
29
30
  format.json { head :no_content }
30
31
  end
31
32
  end
33
+
34
+ private
35
+
36
+ # `authenticate_resource_owner!` hands the request to the host
37
+ # application's `resource_owner_authenticator` block, which is expected to
38
+ # halt the request itself — redirect to a sign-in page, raise, render —
39
+ # when nobody is signed in. A block that merely answers nil halts nothing:
40
+ # the library default (no block configured) logs a warning and answers
41
+ # nil, and so does any block written as a bare lookup such as
42
+ # `User.find_by(id: session[:user_id])`.
43
+ #
44
+ # Both actions here scope their work to `current_resource_owner`, and a
45
+ # nil owner is not "no scope" — it is the scope of the records that have
46
+ # no resource owner, which is what the client credentials flow issues.
47
+ # #index would list every application holding one and #destroy would
48
+ # revoke them, for a caller that never authenticated. So refuse the
49
+ # request rather than treat "nobody" as an owner. Only this controller is
50
+ # guarded; `authenticate_resource_owner!` itself is left alone because
51
+ # AuthorizationsController shapes its own response around the owner.
52
+ def validate_resource_owner
53
+ head :unauthorized unless current_resource_owner
54
+ end
32
55
  end
33
56
  end
@@ -6,7 +6,7 @@ module Doorkeeper
6
6
  if doorkeeper_token&.accessible?
7
7
  render json: doorkeeper_token_to_json, status: :ok
8
8
  else
9
- error = OAuth::InvalidTokenResponse.new
9
+ error = doorkeeper_token_error_response
10
10
  response.headers.merge!(error.headers)
11
11
  render json: error_to_json(error), status: error.status
12
12
  end
@@ -21,5 +21,17 @@ module Doorkeeper
21
21
  def error_to_json(error)
22
22
  error.body
23
23
  end
24
+
25
+ # RFC 6750 §3.1: transmitting the access token by more than one method is
26
+ # answered with invalid_request (400), not invalid_token (401). The helper
27
+ # answers nil for such a request and keeps the error, so the response is
28
+ # chosen here rather than by rescuing a raise.
29
+ def doorkeeper_token_error_response
30
+ if (error = @_doorkeeper_multiple_token_methods_error)
31
+ OAuth::InvalidRequestResponse.new(reason: error.reason)
32
+ else
33
+ OAuth::InvalidTokenResponse.new
34
+ end
35
+ end
24
36
  end
25
37
  end
@@ -99,6 +99,7 @@ en:
99
99
  request_not_authorized: 'Request needs to be authorized. Required parameter for authorizing the request is missing or invalid.'
100
100
  invalid_code_challenge: 'Code challenge is required.'
101
101
  multiple_client_auth_methods: 'The request utilizes more than one mechanism for authenticating the client.'
102
+ multiple_access_token_methods: 'The request uses more than one method for including an access token.'
102
103
  invalid_redirect_uri: "The requested redirect URI is malformed or doesn't match the client redirect URI."
103
104
  unauthorized_client: 'The client is not authorized to perform this request using this method.'
104
105
  access_denied: 'The resource owner or authorization server denied the request.'
@@ -10,12 +10,15 @@ module Doorkeeper
10
10
  def validate!
11
11
  validate_client_authentication_conflict
12
12
  validate_client_authentication_registered
13
+ validate_private_key_jwt_identity
13
14
  validate_reuse_access_token_value
14
15
  validate_token_reuse_limit
15
16
  validate_secret_strategies
17
+ validate_secret_fallback_strategies
16
18
  validate_pkce_code_challenge_methods
17
19
  validate_custom_metadata
18
20
  validate_refresh_token_flow
21
+ validate_deprecated_grant_flows
19
22
  validate_issuer_format
20
23
  validate_issuer_metadata_discoverability
21
24
  end
@@ -70,6 +73,33 @@ module Doorkeeper
70
73
  )
71
74
  end
72
75
 
76
+ # Warn when private_key_jwt client authentication is enabled on a server
77
+ # that identifies itself nowhere. An assertion's audience is what keeps
78
+ # it from being replayed at another authorization server, so it is only
79
+ # ever checked against the configured +issuer+ or Rails'
80
+ # +default_url_options+ - never against the request's Host header, which
81
+ # the client controls. With neither configured there is no acceptable
82
+ # audience and every assertion is refused, so make that loud at boot
83
+ # rather than leave it to be discovered request by request.
84
+ def validate_private_key_jwt_identity
85
+ # The deprecated client_credentials option only maps onto shared-secret
86
+ # methods, so a configuration using it alone never enables this one.
87
+ return if instance_variable_defined?(:@client_credentials_methods) &&
88
+ !instance_variable_defined?(:@client_authentication)
89
+ return unless client_authentication.map(&:to_s).include?("private_key_jwt")
90
+ return if issuer.present?
91
+
92
+ url_options = ::Rails.application&.routes&.default_url_options || {}
93
+ return if url_options[:host].present?
94
+
95
+ ::Rails.logger.error(
96
+ "[DOORKEEPER] private_key_jwt client authentication is enabled, but the server " \
97
+ "identifies itself nowhere, so no audience is acceptable and every client " \
98
+ "assertion will be refused. Configure issuer, or " \
99
+ "Rails.application.routes.default_url_options[:host].",
100
+ )
101
+ end
102
+
73
103
  # Determine whether +reuse_access_token+ and a non-restorable
74
104
  # +token_secret_strategy+ have both been activated.
75
105
  #
@@ -93,6 +123,46 @@ module Doorkeeper
93
123
  application_secret_strategy.validate_for(:application)
94
124
  end
95
125
 
126
+ # Warn for as long as a secret fallback strategy is configured. The
127
+ # fallback exists so that values written before hashing was enabled stay
128
+ # valid while they migrate, which makes it a setting for the migration
129
+ # period rather than a permanent one - but nothing expires it, and an
130
+ # installation that finished migrating long ago keeps it silently.
131
+ #
132
+ # A +:plain+ fallback is worth saying more about: it looks a value up by
133
+ # the value exactly as given, so while it is configured whatever the
134
+ # credential column holds is itself a valid credential. Against anyone
135
+ # who can read the column - a dump, a read-only injection, a logged query
136
+ # - the hashing protects nothing until the fallback is removed. A
137
+ # fallback to another hashing strategy does not have that property, since
138
+ # its stored values are still transformed, so it only gets the first part
139
+ # of the warning.
140
+ #
141
+ # The warning is unconditional rather than raised only while unhashed
142
+ # rows remain: answering that question means querying the credential
143
+ # tables at boot, and the setting itself is what needs surfacing.
144
+ def validate_secret_fallback_strategies
145
+ {
146
+ token: token_secret_fallback_strategy,
147
+ application: application_secret_fallback_strategy,
148
+ }.each do |type, strategy|
149
+ next if strategy.nil?
150
+
151
+ message =
152
+ "[DOORKEEPER] hash_#{type}_secrets is configured with a fallback strategy " \
153
+ "(#{strategy}), which is intended for the migration period only. Remove the " \
154
+ "fallback: option once every row has been migrated to the hashed format."
155
+
156
+ if strategy <= Doorkeeper::SecretStoring::Plain
157
+ message += " While it is configured, a value stored in the #{type} credential " \
158
+ "column is itself a valid credential, so that column needs to be " \
159
+ "protected as carefully as a plaintext credential."
160
+ end
161
+
162
+ ::Rails.logger.warn(message)
163
+ end
164
+ end
165
+
96
166
  def validate_token_reuse_limit
97
167
  return if !reuse_access_token ||
98
168
  (token_reuse_limit > 0 && token_reuse_limit <= 100)
@@ -157,6 +227,27 @@ module Doorkeeper
157
227
  )
158
228
  end
159
229
 
230
+ # Grant flows deprecated by RFC 9700 (OAuth 2.0 Security Best Current
231
+ # Practice, BCP 240) and removed from OAuth 2.1: the implicit grant
232
+ # (RFC 9700 §2.1.2) and the resource owner password credentials grant
233
+ # (RFC 9700 §2.4). Warn at boot when one of them is enabled so that
234
+ # applications relying on them notice ahead of the removal.
235
+ DEPRECATED_GRANT_FLOWS = %w[implicit password].freeze
236
+
237
+ def validate_deprecated_grant_flows
238
+ enabled = calculate_grant_flows.map(&:to_s)
239
+
240
+ DEPRECATED_GRANT_FLOWS.each do |flow|
241
+ next unless enabled.include?(flow)
242
+
243
+ ::Rails.logger.warn(
244
+ "[DOORKEEPER] The #{flow} grant flow is deprecated by RFC 9700 (OAuth 2.0 " \
245
+ "Security Best Current Practice) and removed from OAuth 2.1. It is enabled " \
246
+ "via grant_flows and may be removed in a future Doorkeeper release.",
247
+ )
248
+ end
249
+ end
250
+
160
251
  # Warn when a configured issuer is not RFC-compliant. RFC 8414 (the
161
252
  # metadata issuer) and RFC 9207 (the authorization response iss parameter)
162
253
  # both require an https URL with a host and no query or fragment component.
@@ -10,7 +10,7 @@ module Doorkeeper
10
10
  # that returns configuration Builder class. This exception raises when they don't
11
11
  # define it.
12
12
  #
13
- class Config
13
+ class Config # rubocop:disable Metrics/ClassLength
14
14
  # Default Doorkeeper configuration builder
15
15
  class Builder < AbstractBuilder
16
16
  # Provide support for an owner to be assigned to each registered
@@ -182,6 +182,13 @@ module Doorkeeper
182
182
  @config.instance_variable_set(:@force_pkce, true)
183
183
  end
184
184
 
185
+ # Validate the authorization request's client_id and redirect_uri before
186
+ # authenticating the resource owner, so users are not sent through login
187
+ # for a request that can only fail (disabled by default)
188
+ def validate_client_before_resource_owner_authentication
189
+ @config.instance_variable_set(:@validate_client_before_resource_owner_authentication, true)
190
+ end
191
+
185
192
  # Use an API mode for applications generated with --api argument
186
193
  # It will skip applications controller, disable forgery protection
187
194
  def api_only
@@ -309,6 +316,9 @@ module Doorkeeper
309
316
  option :skip_authorization, default: ->(_routes) {}
310
317
  option :access_token_expires_in, default: 7200
311
318
  option :custom_access_token_expires_in, default: ->(_context) { nil }
319
+ # Ceiling for the TTL of access tokens issued to public clients (OAuth 2.1 Section 2.4).
320
+ # Applies to every grant, including refresh_token. nil (the default) means no ceiling.
321
+ option :public_client_access_token_expires_in, default: nil
312
322
  option :authorization_code_expires_in, default: 600
313
323
  option :orm, default: :active_record
314
324
  option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
@@ -369,7 +379,9 @@ module Doorkeeper
369
379
  option :allow_grant_flow_for_client, default: ->(_grant_flow, _client) { true }
370
380
 
371
381
  # Allows to forbid specific Application redirect URI's by custom rules.
372
- # Doesn't forbid any URI by default.
382
+ # Doesn't forbid any URI by default. Redirect URIs with a script scheme
383
+ # (`javascript`, `vbscript`, `data`) are always rejected, regardless of
384
+ # this option.
373
385
  #
374
386
  # @param forbid_redirect_uri [Proc] Block or any object respond to #call
375
387
  #
@@ -629,6 +641,10 @@ module Doorkeeper
629
641
  option_set? :force_pkce
630
642
  end
631
643
 
644
+ def validate_client_before_resource_owner_authentication?
645
+ option_set? :validate_client_before_resource_owner_authentication
646
+ end
647
+
632
648
  def enforce_configured_scopes?
633
649
  option_set? :enforce_configured_scopes
634
650
  end
@@ -37,6 +37,20 @@ module Doorkeeper
37
37
  end
38
38
  end
39
39
 
40
+ # Raised when a request transmits an access token by more than one method,
41
+ # which RFC 6750 §2 explicitly forbids ("Clients MUST NOT use more than
42
+ # one method to transmit the token in each request"); §3.1 lists it among
43
+ # the conditions answered with invalid_request (HTTP 400).
44
+ class MultipleAccessTokenMethods < DoorkeeperError
45
+ def type
46
+ :invalid_request
47
+ end
48
+
49
+ def reason
50
+ :multiple_access_token_methods
51
+ end
52
+ end
53
+
40
54
  class MissingRequiredParameter < DoorkeeperError
41
55
  attr_reader :missing_param
42
56
 
@@ -36,11 +36,20 @@ module Doorkeeper
36
36
  env["api.endpoint"]
37
37
  end
38
38
 
39
+ # Memoized with the same defined? guard as Rails::Helpers#doorkeeper_token:
40
+ # both a request without a usable token and a refused multi-method request
41
+ # leave @doorkeeper_token nil, which ||= would re-evaluate on every call —
42
+ # re-running authentication just to reach the same verdict again.
39
43
  def doorkeeper_token
40
- @doorkeeper_token ||= OAuth::Token.authenticate(
44
+ return @doorkeeper_token if defined?(@doorkeeper_token)
45
+
46
+ @doorkeeper_token = OAuth::Token.authenticate(
41
47
  decorated_request,
42
48
  *Doorkeeper.config.access_token_methods,
43
49
  )
50
+ rescue Errors::MultipleAccessTokenMethods => e
51
+ @_doorkeeper_multiple_token_methods_error = e
52
+ @doorkeeper_token = nil
44
53
  end
45
54
 
46
55
  def decorated_request
@@ -49,6 +58,7 @@ module Doorkeeper
49
58
 
50
59
  def error_status_codes
51
60
  {
61
+ bad_request: 400,
52
62
  unauthorized: 401,
53
63
  forbidden: 403,
54
64
  }
@@ -39,10 +39,22 @@ module Doorkeeper
39
39
  end
40
40
 
41
41
  # :doc:
42
+ #
43
+ # Answers nil (not a raise) when the request transmits an access token
44
+ # by more than one method (RFC 6750 §2), the same token-or-nil contract
45
+ # Rails::Helpers#doorkeeper_token keeps. This is the doorkeeper_token a
46
+ # subclass of Doorkeeper::ApplicationController or
47
+ # Doorkeeper::ApplicationMetalController resolves to, so a raise here
48
+ # would turn doorkeeper_authorize! in such a controller into an
49
+ # unhandled 500. The error is kept so the response can still be the
50
+ # invalid_request (400) RFC 6750 §3.1 prescribes.
42
51
  def doorkeeper_token
43
52
  return @doorkeeper_token if defined?(@doorkeeper_token)
44
53
 
45
54
  @doorkeeper_token ||= OAuth::Token.authenticate(request, *config_methods)
55
+ rescue Errors::MultipleAccessTokenMethods => e
56
+ @_doorkeeper_multiple_token_methods_error = e
57
+ @doorkeeper_token = nil
46
58
  end
47
59
 
48
60
  def config_methods