doorkeeper 6.0.0.beta1 → 6.0.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -1
- data/README.md +203 -0
- data/app/controllers/doorkeeper/applications_controller.rb +38 -9
- data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
- data/app/controllers/doorkeeper/tokens_controller.rb +5 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +18 -0
- data/config/locales/en.yml +2 -0
- data/lib/doorkeeper/client_authentication/legacy_callable.rb +6 -0
- data/lib/doorkeeper/client_authentication/method.rb +17 -0
- data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
- data/lib/doorkeeper/client_authentication.rb +9 -0
- data/lib/doorkeeper/config.rb +47 -0
- data/lib/doorkeeper/document_cache.rb +81 -0
- data/lib/doorkeeper/errors.rb +48 -0
- data/lib/doorkeeper/http_fetcher.rb +232 -0
- data/lib/doorkeeper/models/access_grant_mixin.rb +16 -0
- data/lib/doorkeeper/models/access_token_mixin.rb +42 -1
- data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
- data/lib/doorkeeper/oauth/authorization_code_request.rb +123 -2
- data/lib/doorkeeper/oauth/base_request.rb +1 -2
- data/lib/doorkeeper/oauth/client.rb +6 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +4 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +4 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +47 -8
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/key_resolver.rb +104 -0
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb +78 -0
- data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +247 -0
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +25 -9
- data/lib/doorkeeper/oauth/client_credentials_request.rb +36 -5
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
- data/lib/doorkeeper/oauth/metadata_response.rb +8 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
- data/lib/doorkeeper/oauth/pre_authorization.rb +51 -3
- data/lib/doorkeeper/oauth/refresh_token_request.rb +51 -1
- data/lib/doorkeeper/oauth/resource_indicator_validator.rb +70 -0
- data/lib/doorkeeper/oauth/scopes.rb +25 -0
- data/lib/doorkeeper/oauth/token_introspection.rb +6 -0
- data/lib/doorkeeper/version.rb +1 -1
- data/lib/doorkeeper.rb +4 -0
- data/lib/generators/doorkeeper/grant_reuse_revocation_generator.rb +43 -0
- data/lib/generators/doorkeeper/resource_indicators_generator.rb +33 -0
- data/lib/generators/doorkeeper/templates/add_access_token_to_access_grants.rb.erb +10 -0
- data/lib/generators/doorkeeper/templates/enable_resource_indicators_migration.rb.erb +8 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +65 -0
- data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
- metadata +45 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b401160f58e48398c2e25631a27e5dabc423480e08f34a64c91c25107ffcab53
|
|
4
|
+
data.tar.gz: 6c6dc01f00118c8525fe7ec329cab28cdaad05b5c5b06402cbb7f59ceb565e1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c394405b84f4b64de4db90e9ab8ea932f2899a2f3898d78df4d06add7765c6f5ae7a457e8e3c19ed69bf7592ffec8c3d1ab5c241f1a521e0347780ead79c233d
|
|
7
|
+
data.tar.gz: 2275e90f19537ae4cda5e60ad1571ec29e0468e53b84d91bb165dea68960c2bf60d358194373ebd183b688d9043d86ef3a0e3685c0c38c7d4db8822ee7a98534
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ User-visible changes worth mentioning.
|
|
|
7
7
|
|
|
8
8
|
## main
|
|
9
9
|
|
|
10
|
+
- [#PR ID] Description of the change.
|
|
11
|
+
|
|
12
|
+
## 6.0.0.beta2
|
|
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
|
+
- [#1865] Revoke the token issued for an authorization code when the code is exchanged more than once, per RFC 6749 §4.1.2 / §10.5. Active when the `oauth_access_grants.access_token_id` column exists: new installs get it from the generated migration, existing apps can add it with `rails generate doorkeeper:grant_reuse_revocation`. Closes [#1713].
|
|
18
|
+
- [#1871] **[BREAKING]** `redirect_uri` is now compared to the registered redirect URIs with the simple string comparison required by RFC 6749 §3.1.2.3 (the RFC 8252 §7.3 loopback port exception is kept). Clients relying on the previous lenient matching must send the exact registered URI, closes [#1718].
|
|
19
|
+
- [#1874] Fix `force_pkce` requiring a `code_challenge` from response types that never issue an authorization code (e.g. `token`, or an OIDC extension's `id_token` / `id_token token`). PKCE (RFC 7636) protects the authorization code exchange, so for code-less response types there is no token-endpoint step where a `code_verifier` could ever be checked — such requests were rejected over a parameter that cannot be validated. `force_pkce` now only enforces the challenge for response types that issue a code (`code` and code-carrying hybrid types such as `code id_token`).
|
|
20
|
+
- [#1876] Fix: reject a non-string `scope` parameter (e.g. `scope[a]=b`, which Rack parses into a Hash) with `invalid_request` (RFC 6749 §3.3) instead of an unhandled 500. `Scopes.from_string` now raises `Errors::InvalidScopeParameter` for a non-string argument — turned into `invalid_request` by the token endpoint's `rescue_from`, so every grant type is covered — and the authorization endpoint rejects it up front in pre-authorization validation. The crash was reachable unauthenticated, before client authentication.
|
|
21
|
+
- [#1877] Fix: let the `none` client authentication strategy match a request whose `Authorization` header carries a Bearer token. A bearer credential authorizes access to the endpoint itself (e.g. a bearer-protected introspection endpoint per RFC 7662 §2.1, or a revocation request) rather than authenticating the client, so it must not suppress the public-client `none` strategy when the client identifies itself with a body `client_id`. Any other non-blank `Authorization` value — Basic, or a `Bearer` with no token — is still treated as header-based client authentication and continues to bypass `none`. The 6.0.0.beta1 workaround of dropping the `Authorization` header from such a request is no longer needed.
|
|
22
|
+
- [#1878] Fix: the loopback redirect URI exception (RFC 8252 §7.3) now varies by port only, not by userinfo. The port was cleared with `URI#port=`, which on Ruby >= 4.0 also drops the userinfo, so `http://attacker@127.0.0.1/cb` matched a registered `http://127.0.0.1/cb`. The match is now made component-by-component. The destination host is always the loopback interface, so this was not a cross-origin open redirect, and non-loopback hosts were never affected.
|
|
23
|
+
- [#1879] Fix: with `reuse_access_token` enabled, replaying an authorization code no longer revokes an access token that another grant still shares. The single-use revocation added in [#1865] followed the grant's `access_token_id`, which a reused token shares across grants, so a replay could collaterally revoke a token another valid session still held. The revocation now skips a token referenced by another grant and only reaches one unique to the replayed code; a token unique to the code is still revoked as before.
|
|
24
|
+
- [#1881] Fix: `Doorkeeper::ApplicationsController` no longer 500s on `create`/`update`/`destroy` in `api_only` mode, where `ActionController::API` provides no `flash`. Confining `flash` to the HTML path is not sufficient on its own, because a client that does not name JSON explicitly still negotiates its way into that path — an absent `Accept` header and a browser-like list such as `application/json, text/plain, */*` both resolve to `text/html`, and a bare `*/*` resolves to the first registered format — so `api_only` mode now pins the response format to JSON.
|
|
25
|
+
- [#1883] Internal: merge `CHANGELOG.md` with git's `union` driver, so two pull requests that each add an entry no longer conflict on the line above "Please add here".
|
|
26
|
+
- [#1884] Fix: `/oauth/introspect` and `/oauth/revoke` extend the token lookup across both token types when the lookup by `token_type_hint` finds nothing (RFC 7662 §2.1 / RFC 7009 §2.1), so a wrong hint no longer hides a token the server knows about ([#1882])
|
|
27
|
+
- [#1885] Index `oauth_access_grants.access_token_id` in the migration templates: since [#1879] the code-replay revocation filters access grants by that column, the "never used to filter queries" premise behind `index: false` no longer holds.
|
|
28
|
+
- [#1886] Add support for Resource Indicators for OAuth 2.0 (RFC 8707). Clients can include a `resource` parameter in authorization and token requests to indicate the target protected resource(s). The authorization server validates resource URIs, enforces audience restriction on tokens, and includes `aud` in introspection responses. Enable by configuring `resource_indicator_validator` with a callable. Requires new `resource` columns on access grants and tokens — run `rails generate doorkeeper:resource_indicators` to add the migration.
|
|
29
|
+
- [#1887] [test] Pin that a requested non-default scope reaches the authorization grant and the exchanged token, and that the authorization strategy shares the controller's pre-authorization — the mismatch reported in [#1576] does not reproduce. Test-only change, closes [#1576].
|
|
30
|
+
- [#1888] Document custom grant flow registration (`Doorkeeper::GrantFlow.register`) in the README with a SAML 2.0 bearer assertion (RFC 7522) walkthrough, and pin URN-shaped custom grant types with an end-to-end request spec. Docs/test-only change, closes [#764].
|
|
31
|
+
- [#1890] [test] Pin that the authorization endpoint answers `invalid_redirect_uri` for a client registered without a redirect URI (`allow_blank_redirect_uri`), whether or not the request supplies one — the behavior required by RFC 6749 §3.1.2.3. Test-only change, closes [#1682].
|
|
32
|
+
- [#1898] Fix: with `reuse_access_token` enabled, the `client_credentials` grant no longer reuses a token whose `resource` differs from the one requested (RFC 8707), so the audience restriction the client asked for is always applied. Follow-up to [#1886].
|
|
33
|
+
- [#1899] Document the client authentication methods registry (`Doorkeeper::ClientAuthentication.register`) in the README with a walkthrough for registering a custom method, and pin it with an end-to-end request spec. Docs/test-only change, closes [#1894].
|
|
34
|
+
- [#1891] Fix: an authorization request carrying a `redirect_uri` for a client registered without one (`redirect_uri` is `nil` under `allow_blank_redirect_uri`) now answers `invalid_redirect_uri` instead of crashing with an unhandled 500.
|
|
35
|
+
- [#1896] Add an opt-in `private_key_jwt` client authentication method (RFC 7523 / OIDC Core §9, requires the `jwt` gem >= 2.7) that verifies assertions against the client's published public keys — `jwks` / `jwks_uri` attributes you define on your Application model, the latter fetched with an SSRF-hardened HTTP client. The jti replay guard and the fetched-JWKS cache are process-local by default and can be replaced with shared stores via the `private_key_jwt_replay_guard` / `private_key_jwt_jwks_cache` config options. Part of [#1875].
|
|
36
|
+
- [#1901] [test] Pin the answered behaviors behind [#984], [#1554], [#1600], [#1663], [#1759] and [#1787] with regression specs — the built-in client authentication methods, the unmodified echo of long `state` values, DB persistence with custom token generators, refresh token rotation/expiry semantics and the introspection asymmetry. Test-only change, closes those issues along with [#1291], [#1756] and [#1764].
|
|
37
|
+
- [#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
|
+
- [#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
|
+
- [#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.
|
|
10
40
|
- Please add here
|
|
11
41
|
|
|
12
42
|
## 6.0.0.beta1
|
|
@@ -47,7 +77,11 @@ and changelog below before the update since this version includes breaking chang
|
|
|
47
77
|
- [#1864] Fix `custom_access_token_attributes` values being dropped when the authorization goes through the consent screen: the approve/deny forms now carry the custom attributes as hidden fields, and the pre-authorization JSON (`api_only` mode) includes them so custom consent UIs can send them back.
|
|
48
78
|
- [#1869] Improve test coverage
|
|
49
79
|
- [#1870] Fix: raise the intended `Doorkeeper::Errors::TokenGeneratorNotFound` / `UnableToGenerateToken` (instead of a confusing `NameError`) when `application_secret_generator` is misconfigured.
|
|
50
|
-
|
|
80
|
+
|
|
81
|
+
## 5.9.5
|
|
82
|
+
|
|
83
|
+
- [#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.
|
|
84
|
+
- [#1853] Fix `reuse_access_token` reusing a token that was created with `custom_access_token_attributes` values when the new request doesn't specify any custom attributes. Such requests now only match tokens without custom attributes.
|
|
51
85
|
|
|
52
86
|
## 5.9.3
|
|
53
87
|
|
data/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Supported features:
|
|
|
25
25
|
- [OAuth 2.0 for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252)
|
|
26
26
|
- [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)
|
|
27
27
|
- [OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/html/rfc9207) — opt-in by setting `issuer`; adds the `iss` parameter to authorization redirects returned to the client
|
|
28
|
+
- [Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)
|
|
28
29
|
|
|
29
30
|
## Table of Contents
|
|
30
31
|
|
|
@@ -38,6 +39,9 @@ Supported features:
|
|
|
38
39
|
- [Grape](#grape)
|
|
39
40
|
- [ORMs](#orms)
|
|
40
41
|
- [Extensions](#extensions)
|
|
42
|
+
- [Resource Indicators](#resource-indicators)
|
|
43
|
+
- [Custom Grant Flows](#custom-grant-flows)
|
|
44
|
+
- [Custom Client Authentication Methods](#custom-client-authentication-methods)
|
|
41
45
|
- [Example Applications](#example-applications)
|
|
42
46
|
- [Sponsors](#sponsors)
|
|
43
47
|
- [Development](#development)
|
|
@@ -107,6 +111,205 @@ Extensions that are not included by default and can be installed separately.
|
|
|
107
111
|
| CIBA - Client Initiated Backchannel Authentication Flow extension | [doorkeeper-ciba](https://github.com/autoseg/doorkeeper-ciba) |
|
|
108
112
|
| Device Authorization Grant | [doorkeeper-device_authorization_grant](https://github.com/exop-group/doorkeeper-device_authorization_grant) |
|
|
109
113
|
|
|
114
|
+
## Resource Indicators
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
### Setup
|
|
119
|
+
|
|
120
|
+
1. Run the generator to add the required `resource` column:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
rails generate doorkeeper:resource_indicators
|
|
124
|
+
rails db:migrate
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
2. Configure a validator in your initializer:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# config/initializers/doorkeeper.rb
|
|
131
|
+
Doorkeeper.configure do
|
|
132
|
+
resource_indicator_validator ->(resource_indicators, client) {
|
|
133
|
+
allowed = %w[https://api.example.com/ https://calendar.example.com/]
|
|
134
|
+
resource_indicators.all? { |r| allowed.include?(r) }
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The callable receives an array of resource URIs and the OAuth client. Return `true` to accept or `false` to reject with `invalid_target`.
|
|
140
|
+
|
|
141
|
+
### Behavior
|
|
142
|
+
|
|
143
|
+
- Resource URIs must be absolute and must not contain a fragment component.
|
|
144
|
+
- Resource indicators are stored on grants and tokens.
|
|
145
|
+
- Token and refresh requests enforce subset restrictions against the original grant.
|
|
146
|
+
- Token introspection responses include `aud` when resource indicators are present.
|
|
147
|
+
- Grants issued with resource indicators retain their audience restriction even if the validator is later removed from configuration.
|
|
148
|
+
|
|
149
|
+
### Multiple resources
|
|
150
|
+
|
|
151
|
+
RFC 8707 uses repeated query parameters (`?resource=…&resource=…`) for multiple values, but Rack collapses repeated keys to the last value. Clients must use the Rails bracket syntax for multiple resource indicators:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
?resource[]=https://api.example.com/&resource[]=https://calendar.example.com/
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A single `resource=…` works as-is.
|
|
158
|
+
|
|
159
|
+
## Custom Grant Flows
|
|
160
|
+
|
|
161
|
+
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).
|
|
162
|
+
|
|
163
|
+
A grant flow bundles a matcher for the `grant_type` parameter with a strategy class that processes the token request. Register it before `Doorkeeper.configure` and enable it by adding its registered name to `grant_flows`:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# config/initializers/doorkeeper.rb
|
|
167
|
+
Doorkeeper::GrantFlow.register(
|
|
168
|
+
:saml2_bearer,
|
|
169
|
+
grant_type_matches: "urn:ietf:params:oauth:grant-type:saml2-bearer",
|
|
170
|
+
grant_type_strategy: SamlBearer::Strategy,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
Doorkeeper.configure do
|
|
174
|
+
grant_flows %w[authorization_code saml2_bearer]
|
|
175
|
+
# ...
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Note that `grant_flows` lists the *registered flow name* (`saml2_bearer`), while `grant_type_matches` — a `String` or a `Regexp` — is what the request's `grant_type` parameter is matched against.
|
|
180
|
+
|
|
181
|
+
The strategy class receives the authorization server as `server` and builds the request object handling the grant:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
module SamlBearer
|
|
185
|
+
class Strategy < Doorkeeper::Request::Strategy
|
|
186
|
+
delegate :client, :parameters, to: :server
|
|
187
|
+
|
|
188
|
+
def request
|
|
189
|
+
@request ||= TokenRequest.new(Doorkeeper.config, client, parameters)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The request object validates the grant and issues the token. Subclassing `Doorkeeper::OAuth::BaseRequest` provides the response handling, scope calculation and token creation, so only the grant-specific parts remain (per RFC 7522 §2.1 the `assertion` parameter carries a single SAML assertion, base64url-encoded without padding):
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
module SamlBearer
|
|
199
|
+
class TokenRequest < Doorkeeper::OAuth::BaseRequest
|
|
200
|
+
validate :client, error: Doorkeeper::Errors::InvalidClient
|
|
201
|
+
validate :client_supports_grant_flow, error: Doorkeeper::Errors::UnauthorizedClient
|
|
202
|
+
validate :assertion, error: Doorkeeper::Errors::InvalidGrant
|
|
203
|
+
validate :scopes, error: Doorkeeper::Errors::InvalidScope
|
|
204
|
+
|
|
205
|
+
attr_reader :client, :parameters, :access_token
|
|
206
|
+
|
|
207
|
+
def initialize(server, client, parameters = {})
|
|
208
|
+
@server = server
|
|
209
|
+
@client = client
|
|
210
|
+
@parameters = parameters
|
|
211
|
+
@original_scopes = parameters[:scope]
|
|
212
|
+
@grant_type = "urn:ietf:params:oauth:grant-type:saml2-bearer"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def before_successful_response
|
|
218
|
+
find_or_create_access_token(client, resource_owner, scopes, {}, server)
|
|
219
|
+
super
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def assertion
|
|
223
|
+
# Decode and verify the SAML assertion — signature, audience, validity
|
|
224
|
+
# window, etc. — e.g. with the ruby-saml gem. Skipping verification
|
|
225
|
+
# turns the endpoint into a token vending machine for anyone.
|
|
226
|
+
@assertion ||= decode_and_verify_saml(parameters[:assertion])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def resource_owner
|
|
230
|
+
# Map the assertion's subject to a resource owner.
|
|
231
|
+
@resource_owner ||= User.find_by(email: assertion.name_id)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def validate_client
|
|
235
|
+
client.present?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def validate_client_supports_grant_flow
|
|
239
|
+
Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client&.application)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_assertion
|
|
243
|
+
assertion.present? && resource_owner.present?
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def validate_scopes
|
|
247
|
+
return true if scopes.blank?
|
|
248
|
+
|
|
249
|
+
Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
|
|
250
|
+
scope_str: scopes.to_s,
|
|
251
|
+
server_scopes: server.scopes,
|
|
252
|
+
app_scopes: client&.scopes,
|
|
253
|
+
grant_type: grant_type,
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The `client_supports_grant_flow` validation keeps the custom grant subject to the `allow_grant_flow_for_client` configuration option (per-client grant restrictions), just like the built-in flows.
|
|
261
|
+
|
|
262
|
+
Flows can also handle custom `response_type` values on the authorization endpoint via the `response_type_matches` / `response_type_strategy` options — see the built-in registrations in [`lib/doorkeeper/grant_flow.rb`](lib/doorkeeper/grant_flow.rb) for reference. An extension can also group several flows under one configuration name with `Doorkeeper::GrantFlow.register_alias` (e.g. the OpenID Connect extension registers `implicit_oidc` to expand to multiple response types).
|
|
263
|
+
|
|
264
|
+
## Custom Client Authentication Methods
|
|
265
|
+
|
|
266
|
+
Doorkeeper authenticates clients (RFC 6749 §2.3) through a registry of named methods. `client_secret_basic`, `client_secret_post` and `none` are built in, and an application or extension can register additional ones — for instance to keep accepting credentials that a partner integration sends in its own headers.
|
|
267
|
+
|
|
268
|
+
A method is any object that responds to `matches_request?` and `authenticate`. Register it before `Doorkeeper.configure` and enable it by listing its registered name in `client_authentication`:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# config/initializers/doorkeeper.rb
|
|
272
|
+
Doorkeeper::ClientAuthentication.register(
|
|
273
|
+
:partner_headers,
|
|
274
|
+
PartnerHeaders::Authentication,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
Doorkeeper.configure do
|
|
278
|
+
client_authentication %i[client_secret_basic client_secret_post partner_headers none]
|
|
279
|
+
# ...
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The order of `client_authentication` is the order the methods are tried in: the first one whose `matches_request?` returns true handles the request.
|
|
284
|
+
|
|
285
|
+
`matches_request?` decides whether the request carries this method's credentials, and `authenticate` extracts them into a `Doorkeeper::ClientAuthentication::Credentials` pair (or `nil`):
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
module PartnerHeaders
|
|
289
|
+
class Authentication
|
|
290
|
+
def self.matches_request?(request)
|
|
291
|
+
request.get_header("HTTP_X_CLIENT_ID").present? &&
|
|
292
|
+
request.get_header("HTTP_X_CLIENT_SECRET").present?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.authenticate(request)
|
|
296
|
+
Doorkeeper::ClientAuthentication::Credentials.new(
|
|
297
|
+
request.get_header("HTTP_X_CLIENT_ID"),
|
|
298
|
+
request.get_header("HTTP_X_CLIENT_SECRET"),
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Two things are worth keeping in mind when writing one.
|
|
306
|
+
|
|
307
|
+
**Keep `matches_request?` as narrow as possible.** RFC 6749 §2.3 forbids a client from using more than one authentication method in a single request, and Doorkeeper enforces that across the whole registry rather than only the enabled methods. A method that matches too broadly therefore collides with a built-in one and the request is answered with `invalid_request`.
|
|
308
|
+
|
|
309
|
+
**The returned credentials are resolved with `by_uid_and_secret`.** A blank secret resolves only a public (non-confidential) client — that is what the built-in `none` method relies on — while a confidential client is resolved only when the secret matches the registered one. A method that establishes the client's identity by some other proof, such as a client certificate or a signed assertion, therefore still has to produce the registered secret for a confidential client.
|
|
310
|
+
|
|
311
|
+
Enabled methods are advertised in the authorization server metadata, so a registered method appears in `token_endpoint_auth_methods_supported` at `/.well-known/oauth-authorization-server` once `client_authentication` lists it.
|
|
312
|
+
|
|
110
313
|
## Example Applications
|
|
111
314
|
|
|
112
315
|
These applications show how Doorkeeper works and how to integrate with it. Start with the oAuth2 server and use the clients to connect with the server.
|
|
@@ -5,6 +5,7 @@ module Doorkeeper
|
|
|
5
5
|
layout "doorkeeper/admin" unless Doorkeeper.configuration.api_only
|
|
6
6
|
|
|
7
7
|
before_action :authenticate_admin!
|
|
8
|
+
before_action :force_json_format, if: :api_only?
|
|
8
9
|
before_action :set_application, only: %i[show edit update destroy]
|
|
9
10
|
|
|
10
11
|
def index
|
|
@@ -31,11 +32,13 @@ module Doorkeeper
|
|
|
31
32
|
@application = Doorkeeper.config.application_model.new(application_params)
|
|
32
33
|
|
|
33
34
|
if @application.save
|
|
34
|
-
flash[:notice] = I18n.t(:notice, scope: %i[doorkeeper flash applications create])
|
|
35
|
-
flash[:application_secret] = @application.plaintext_secret
|
|
36
|
-
|
|
37
35
|
respond_to do |format|
|
|
38
|
-
format.html
|
|
36
|
+
format.html do
|
|
37
|
+
flash[:notice] = I18n.t(:notice, scope: %i[doorkeeper flash applications create])
|
|
38
|
+
flash[:application_secret] = @application.plaintext_secret
|
|
39
|
+
|
|
40
|
+
redirect_to oauth_application_url(@application)
|
|
41
|
+
end
|
|
39
42
|
format.json { render json: @application, as_owner: true }
|
|
40
43
|
end
|
|
41
44
|
else
|
|
@@ -54,10 +57,12 @@ module Doorkeeper
|
|
|
54
57
|
|
|
55
58
|
def update
|
|
56
59
|
if @application.update(application_params)
|
|
57
|
-
flash[:notice] = I18n.t(:notice, scope: i18n_scope(:update))
|
|
58
|
-
|
|
59
60
|
respond_to do |format|
|
|
60
|
-
format.html
|
|
61
|
+
format.html do
|
|
62
|
+
flash[:notice] = I18n.t(:notice, scope: i18n_scope(:update))
|
|
63
|
+
|
|
64
|
+
redirect_to oauth_application_url(@application)
|
|
65
|
+
end
|
|
61
66
|
format.json { render json: @application, as_owner: true }
|
|
62
67
|
end
|
|
63
68
|
else
|
|
@@ -73,10 +78,14 @@ module Doorkeeper
|
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def destroy
|
|
76
|
-
|
|
81
|
+
destroyed = @application.destroy
|
|
77
82
|
|
|
78
83
|
respond_to do |format|
|
|
79
|
-
format.html
|
|
84
|
+
format.html do
|
|
85
|
+
flash[:notice] = I18n.t(:notice, scope: i18n_scope(:destroy)) if destroyed
|
|
86
|
+
|
|
87
|
+
redirect_to oauth_applications_url
|
|
88
|
+
end
|
|
80
89
|
format.json { head :no_content }
|
|
81
90
|
end
|
|
82
91
|
end
|
|
@@ -95,5 +104,25 @@ module Doorkeeper
|
|
|
95
104
|
def i18n_scope(action)
|
|
96
105
|
%i[doorkeeper flash applications] << action
|
|
97
106
|
end
|
|
107
|
+
|
|
108
|
+
def api_only?
|
|
109
|
+
Doorkeeper.config.api_only
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# In api_only mode this controller descends from ActionController::API,
|
|
113
|
+
# which has no flash and renders no views — but Rails still negotiates a
|
|
114
|
+
# format from the request, and only a client that names JSON explicitly
|
|
115
|
+
# lands on the json branch. Measured on Rails 8.0: an absent Accept header
|
|
116
|
+
# and a browser-like list such as "application/json, text/plain, */*"
|
|
117
|
+
# (axios's default) both resolve to text/html, while a bare "*/*" (curl,
|
|
118
|
+
# Net::HTTP, python-requests) resolves to Mime::ALL, which respond_to
|
|
119
|
+
# answers with the first registered format — html in every action here.
|
|
120
|
+
# Those requests therefore ran the html branch and raised on flash, after
|
|
121
|
+
# create had already persisted the record and destroy had already deleted
|
|
122
|
+
# it. Nothing needs negotiating when only JSON can be served, so pin the
|
|
123
|
+
# format rather than leaving a branch registered that cannot run.
|
|
124
|
+
def force_json_format
|
|
125
|
+
request.format = :json
|
|
126
|
+
end
|
|
98
127
|
end
|
|
99
128
|
end
|
|
@@ -65,7 +65,12 @@ module Doorkeeper
|
|
|
65
65
|
pre_auth.client,
|
|
66
66
|
current_resource_owner,
|
|
67
67
|
pre_auth.scopes,
|
|
68
|
-
)
|
|
68
|
+
) do |token|
|
|
69
|
+
# RFC 8707: a token for a different audience must not satisfy the match.
|
|
70
|
+
Doorkeeper.config.access_token_model.resource_indicators_match?(
|
|
71
|
+
token, pre_auth.resource_indicators&.join(" ").presence,
|
|
72
|
+
)
|
|
73
|
+
end
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def redirect_or_render(auth)
|
|
@@ -101,11 +106,20 @@ module Doorkeeper
|
|
|
101
106
|
end
|
|
102
107
|
|
|
103
108
|
def pre_auth_params
|
|
104
|
-
|
|
109
|
+
# RFC 8707: `resource` may appear as a single value (?resource=…) or as
|
|
110
|
+
# a Rails-style array (?resource[]=…&resource[]=…). Both scalar and array
|
|
111
|
+
# forms are permitted through strong parameters.
|
|
112
|
+
#
|
|
113
|
+
# NOTE: The RFC wire format uses repeated keys (?resource=…&resource=…),
|
|
114
|
+
# but Rack collapses those into the last value. Clients targeting this
|
|
115
|
+
# endpoint must use the resource[] bracket syntax for multiple values.
|
|
116
|
+
params
|
|
117
|
+
.slice(*pre_auth_param_fields, :resource)
|
|
118
|
+
.permit(*pre_auth_param_fields, :resource, resource: [])
|
|
105
119
|
end
|
|
106
120
|
|
|
107
121
|
def pre_auth_param_fields
|
|
108
|
-
custom_access_token_attributes + %i[
|
|
122
|
+
@pre_auth_param_fields ||= custom_access_token_attributes + %i[
|
|
109
123
|
client_id
|
|
110
124
|
code_challenge
|
|
111
125
|
code_challenge_method
|
|
@@ -123,9 +123,13 @@ module Doorkeeper
|
|
|
123
123
|
def revocable_token
|
|
124
124
|
return @revocable_token if defined? @revocable_token
|
|
125
125
|
|
|
126
|
+
# The hint is a lookup-order optimization, not a filter: a client is
|
|
127
|
+
# explicitly allowed to guess it wrong, so when the lookup by the hinted
|
|
128
|
+
# type finds nothing, RFC 7009 §2.1 and RFC 7662 §2.1 require the search
|
|
129
|
+
# to extend across the other supported token type.
|
|
126
130
|
@revocable_token =
|
|
127
131
|
if params[:token_type_hint] == "refresh_token"
|
|
128
|
-
refresh_token
|
|
132
|
+
refresh_token || access_token
|
|
129
133
|
else
|
|
130
134
|
access_token || refresh_token
|
|
131
135
|
end
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
<% end %>
|
|
21
21
|
|
|
22
|
+
<% if Array(@pre_auth.resource_indicators).any? %>
|
|
23
|
+
<div id="oauth-resources">
|
|
24
|
+
<h3><%= t('.resources') %></h3>
|
|
25
|
+
|
|
26
|
+
<ul class="text-info">
|
|
27
|
+
<% Array(@pre_auth.resource_indicators).each do |resource| %>
|
|
28
|
+
<li><%= resource %></li>
|
|
29
|
+
<% end %>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
22
34
|
<div class="actions">
|
|
23
35
|
<%= form_tag oauth_authorization_path, method: :post do %>
|
|
24
36
|
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
|
@@ -32,6 +44,9 @@
|
|
|
32
44
|
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
33
45
|
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
34
46
|
<% end %>
|
|
47
|
+
<% Array.wrap(@pre_auth.resource_indicators).each do |resource_value| %>
|
|
48
|
+
<%= hidden_field_tag "resource[]", resource_value, id: nil %>
|
|
49
|
+
<% end %>
|
|
35
50
|
<%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
|
|
36
51
|
<% end %>
|
|
37
52
|
<%= form_tag oauth_authorization_path, method: :delete do %>
|
|
@@ -46,6 +61,9 @@
|
|
|
46
61
|
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
47
62
|
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
48
63
|
<% end %>
|
|
64
|
+
<% Array.wrap(@pre_auth.resource_indicators).each do |resource_value| %>
|
|
65
|
+
<%= hidden_field_tag "resource[]", resource_value, id: nil %>
|
|
66
|
+
<% end %>
|
|
49
67
|
<%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
|
|
50
68
|
<% end %>
|
|
51
69
|
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -70,6 +70,7 @@ en:
|
|
|
70
70
|
title: 'Authorization required'
|
|
71
71
|
prompt: 'Authorize %{client_name} to use your account?'
|
|
72
72
|
able_to: 'This application will be able to:'
|
|
73
|
+
resources: 'Resources'
|
|
73
74
|
show:
|
|
74
75
|
title: 'Authorization code:'
|
|
75
76
|
form_post:
|
|
@@ -108,6 +109,7 @@ en:
|
|
|
108
109
|
other: 'The code_challenge_method must be one of %{challenge_methods}.'
|
|
109
110
|
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
|
|
110
111
|
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
|
|
112
|
+
invalid_target: 'The requested resource is invalid, missing, unknown, or malformed.'
|
|
111
113
|
|
|
112
114
|
# Configuration error messages
|
|
113
115
|
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
|
|
@@ -16,6 +16,12 @@ module Doorkeeper
|
|
|
16
16
|
@callable = callable
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Legacy extractors pull a uid/secret pair out of the request, so they
|
|
20
|
+
# are treated as shared-secret methods.
|
|
21
|
+
def uses_shared_secret?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
def matches_request?(request)
|
|
20
26
|
credentials_for(request).present?
|
|
21
27
|
end
|
|
@@ -18,6 +18,23 @@ module Doorkeeper
|
|
|
18
18
|
@name = name
|
|
19
19
|
@strategy = strategy
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
# Whether this method authenticates clients with a shared symmetric
|
|
23
|
+
# secret. Callers that must refuse such methods — because no secret
|
|
24
|
+
# can have been established with the client — can ask the registry
|
|
25
|
+
# instead of keeping a hard-coded list of method names.
|
|
26
|
+
#
|
|
27
|
+
# Strategies may declare this themselves by defining
|
|
28
|
+
# +uses_shared_secret?+; for strategies that don't, the registration
|
|
29
|
+
# name is checked for "client_secret" as a conservative fallback, so an
|
|
30
|
+
# undeclared method errs on the side of being treated as secret-based.
|
|
31
|
+
def uses_shared_secret?
|
|
32
|
+
if strategy.respond_to?(:uses_shared_secret?)
|
|
33
|
+
strategy.uses_shared_secret?
|
|
34
|
+
else
|
|
35
|
+
name.to_s.include?("client_secret")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
21
38
|
end
|
|
22
39
|
end
|
|
23
40
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
# Credentials for a client that was already fully authenticated by its
|
|
6
|
+
# authentication method (e.g. a verified private_key_jwt assertion), so
|
|
7
|
+
# the client lookup must not run a secret comparison — there is no
|
|
8
|
+
# secret, and the proof of identity has already been checked.
|
|
9
|
+
class VerifiedCredentials < Credentials
|
|
10
|
+
def initialize(uid)
|
|
11
|
+
super(uid, nil)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pre_authenticated?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "doorkeeper/client_authentication/credentials"
|
|
4
|
+
require "doorkeeper/client_authentication/verified_credentials"
|
|
4
5
|
require "doorkeeper/client_authentication/fallback_method"
|
|
5
6
|
require "doorkeeper/client_authentication/legacy_callable"
|
|
6
7
|
require "doorkeeper/client_authentication/method"
|
|
@@ -50,6 +51,14 @@ module Doorkeeper
|
|
|
50
51
|
Doorkeeper::OAuth::ClientAuthentication::ClientSecretBasic,
|
|
51
52
|
)
|
|
52
53
|
|
|
54
|
+
# Registered but not part of DEFAULT_METHODS: servers opt in through the
|
|
55
|
+
# +client_authentication+ config option. Requires the "jwt" gem at the
|
|
56
|
+
# moment an assertion is authenticated.
|
|
57
|
+
register(
|
|
58
|
+
:private_key_jwt,
|
|
59
|
+
Doorkeeper::OAuth::ClientAuthentication::PrivateKeyJwt,
|
|
60
|
+
)
|
|
61
|
+
|
|
53
62
|
# Converts a deprecated +client_credentials+ configuration into the client
|
|
54
63
|
# authentication method names / adapters understood by the registry.
|
|
55
64
|
# Unknown values are warned about and dropped; callables are wrapped in a
|
data/lib/doorkeeper/config.rb
CHANGED
|
@@ -385,6 +385,53 @@ module Doorkeeper
|
|
|
385
385
|
# (RFC 8414). When nil, the request base URL is used instead.
|
|
386
386
|
option :issuer, default: nil
|
|
387
387
|
|
|
388
|
+
# Resource Indicators for OAuth 2.0 (RFC 8707).
|
|
389
|
+
#
|
|
390
|
+
# When set to a callable (lambda/proc), enables RFC 8707 support. The callable
|
|
391
|
+
# receives the array of resource indicator URIs and the client, and must return
|
|
392
|
+
# true if the resource(s) are acceptable, or false to reject with `invalid_target`.
|
|
393
|
+
#
|
|
394
|
+
# When nil (default), the `resource` parameter is ignored and RFC 8707 is disabled.
|
|
395
|
+
#
|
|
396
|
+
# @example
|
|
397
|
+
# resource_indicator_validator ->(resource_indicators, client) {
|
|
398
|
+
# resource_indicators.all? { |r| allowed_resources.include?(r) }
|
|
399
|
+
# }
|
|
400
|
+
#
|
|
401
|
+
# @param validator [Proc, nil] A callable that validates resource indicators
|
|
402
|
+
# @see https://datatracker.ietf.org/doc/html/rfc8707
|
|
403
|
+
#
|
|
404
|
+
option :resource_indicator_validator, default: nil
|
|
405
|
+
|
|
406
|
+
# Replay guard for `private_key_jwt` client assertions (jti single-use
|
|
407
|
+
# tracking, OIDC Core §9). The default guard remembers jti values in
|
|
408
|
+
# process-local memory, so it cannot see a replay delivered to a
|
|
409
|
+
# different server process; a multi-process deployment can supply a
|
|
410
|
+
# shared store instead — any object answering
|
|
411
|
+
# `first_use?(key, expires_at:)`, returning true when the key was never
|
|
412
|
+
# seen before and remembering it until the unix time `expires_at`.
|
|
413
|
+
#
|
|
414
|
+
# @example
|
|
415
|
+
# private_key_jwt_replay_guard RedisReplayGuard.new
|
|
416
|
+
#
|
|
417
|
+
# @param guard [#first_use?, nil] nil uses the built-in process-local guard
|
|
418
|
+
#
|
|
419
|
+
option :private_key_jwt_replay_guard, default: nil
|
|
420
|
+
|
|
421
|
+
# Cache for JWK Sets fetched from a client's `jwks_uri` during
|
|
422
|
+
# `private_key_jwt` authentication. Defaults to a process-local
|
|
423
|
+
# Doorkeeper::DocumentCache with a 60 second TTL; supply your own
|
|
424
|
+
# instance to change the TTL, or any object answering
|
|
425
|
+
# `fetch(url) { ... }` (returning the cached document or storing and
|
|
426
|
+
# returning the block's result) to share the cache across processes.
|
|
427
|
+
#
|
|
428
|
+
# @example
|
|
429
|
+
# private_key_jwt_jwks_cache Doorkeeper::DocumentCache.new(ttl: 300)
|
|
430
|
+
#
|
|
431
|
+
# @param cache [#fetch, nil] nil uses a built-in process-local cache
|
|
432
|
+
#
|
|
433
|
+
option :private_key_jwt_jwks_cache, default: nil
|
|
434
|
+
|
|
388
435
|
# Forces the usage of the HTTPS protocol in non-native redirect uris
|
|
389
436
|
# (enabled by default in non-development environments). OAuth2
|
|
390
437
|
# delegates security in communication to the HTTPS protocol so it is
|