doorkeeper 5.9.5 → 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 +70 -3
- data/README.md +204 -0
- data/app/controllers/doorkeeper/applications_controller.rb +38 -9
- data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
- data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
- data/app/controllers/doorkeeper/tokens_controller.rb +15 -2
- data/app/views/doorkeeper/authorizations/new.html.erb +24 -0
- data/config/locales/en.yml +2 -0
- data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
- data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
- data/lib/doorkeeper/client_authentication/legacy_callable.rb +52 -0
- data/lib/doorkeeper/client_authentication/method.rb +40 -0
- data/lib/doorkeeper/client_authentication/registry.rb +47 -0
- data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
- data/lib/doorkeeper/client_authentication.rb +102 -0
- data/lib/doorkeeper/config/option.rb +1 -1
- data/lib/doorkeeper/config/validations.rb +162 -1
- data/lib/doorkeeper/config.rb +161 -9
- data/lib/doorkeeper/document_cache.rb +81 -0
- data/lib/doorkeeper/errors.rb +49 -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 +81 -5
- data/lib/doorkeeper/models/application_mixin.rb +14 -6
- data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
- data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
- data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
- data/lib/doorkeeper/oauth/authorization_code_request.rb +127 -3
- data/lib/doorkeeper/oauth/base_request.rb +1 -2
- data/lib/doorkeeper/oauth/client.rb +24 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +57 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +33 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +71 -0
- 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/code_response.rb +16 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -2
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
- data/lib/doorkeeper/oauth/metadata_response.rb +165 -0
- data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
- data/lib/doorkeeper/oauth/pre_authorization.rb +63 -8
- 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 +33 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +59 -0
- data/lib/doorkeeper/server.rb +5 -2
- data/lib/doorkeeper/version.rb +4 -4
- data/lib/doorkeeper.rb +12 -4
- 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 +131 -6
- data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
- metadata +52 -4
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -71
- data/lib/doorkeeper/oauth/stateless_token.rb +0 -139
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,14 +7,80 @@ 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
|
|
|
42
|
+
## 6.0.0.beta1
|
|
43
|
+
|
|
44
|
+
**Please make sure** you read the [Upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
|
|
45
|
+
and changelog below before the update since this version includes breaking changes.
|
|
46
|
+
|
|
47
|
+
- [#1816] Fix: redirect `unauthorized_client` errors per RFC 6749 Section 4.1.2.1; validate redirect_uri before `client_supports_grant_flow` to prevent open redirect
|
|
48
|
+
- [#1867] Fix: use `access_denied` instead of `invalid_client` for `resource_owner_authorize_for_client` validation per RFC 6749 Section 4.1.2.1. `invalid_client` is a token endpoint error (Section 5.2), not an authorization endpoint error.
|
|
49
|
+
- [#1838] Add OAuth 2.0 Authorization Server Metadata endpoint (RFC 8414) served at `/.well-known/oauth-authorization-server`. The response is built from your Doorkeeper configuration and advertises the authorization, token, revocation and (when token introspection is enabled) introspection endpoints, supported scopes, response/grant types and PKCE code challenge methods. Two new config options are available: `issuer` (defaults to the request base URL) and `custom_metadata` (a Hash merged into the response, e.g. to advertise an OIDC `userinfo_endpoint`). The controller/response use the RFC 8414 "Metadata" naming so they don't collide with a future OpenID Connect Discovery (`.well-known/openid-configuration`) implementation. Endpoints disabled through `skip_controllers` are omitted from the response instead of raising a route-generation error.
|
|
50
|
+
- [#1839] Send client credentials in the request body (not the query string) in the token endpoint specs, per RFC 6749 §2.3.1. Test-only change: the `*_endpoint_url` helpers now return a path plus a matching `*_endpoint_params` builder so flow specs post credentials through the body.
|
|
51
|
+
- [#1840] Introduce a pluggable client authentication registry (RFC 6749 §2.3).
|
|
52
|
+
- New `client_authentication` config option declares which methods are accepted and in which order. Built-in strategies: `client_secret_basic`, `client_secret_post` and `none`.
|
|
53
|
+
- Custom strategies can be registered with `Doorkeeper::ClientAuthentication.register`.
|
|
54
|
+
- **[BREAKING]** Client credentials are no longer read from the query string — send them in the request body or via HTTP Basic. This applies to every endpoint that authenticates clients: token, revocation and introspection.
|
|
55
|
+
- **[BREAKING]** The `none` strategy rejects requests that carry a non-blank `Authorization` header. A common casualty is a public client POSTing to `/oauth/revoke` with a body `client_id` while still sending its `Authorization: Bearer <token>` header — drop the header from that request.
|
|
56
|
+
- **[BREAKING]** The `Doorkeeper::OAuth::Client::Credentials` class methods `.from_request`, `.from_basic` and `.from_params` are removed — extensions extracting credentials from a request should register a client authentication method instead.
|
|
57
|
+
- **Deprecated**: the `client_credentials` option — use `client_authentication` instead.
|
|
58
|
+
- **Deprecated**: the `Doorkeeper::OAuth::Client::Credentials` constant itself — it remains as an alias of `Doorkeeper::ClientAuthentication::Credentials` (the plain uid/secret struct).
|
|
59
|
+
- See the [Upgrade Guide](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) for detailed breaking changes, deprecations and migration steps.
|
|
60
|
+
- [#1841] Reject requests that use more than one client authentication method (RFC 6749 §2.3: "The client MUST NOT use more than one authentication method in each request") with an `invalid_request` error instead of silently using the first match. The request payload is validated against every registered method before the configured one is selected, so a client sending e.g. both Basic and body credentials is rejected even when only one of those methods is enabled. This applies to every endpoint that authenticates clients — token, revocation and introspection. Only real authentication mechanisms count: a bare `client_id` (the `none` method) is not a mechanism of its own, and deprecated `client_credentials` callable extractors never count towards the limit (they keep the historical first-extractor-wins selection).
|
|
61
|
+
- [#1842] **[BREAKING]** `force_pkce` now requires PKCE for all clients, including confidential ones, in line with the OAuth 2.0 Security BCP (RFC 9700) and OAuth 2.1. Previously confidential clients were exempt. If you enable `force_pkce` and have confidential clients that do not yet send a `code_challenge`/`code_verifier`, their authorization requests will start to be rejected.
|
|
62
|
+
- [#1845] Fix `NameError` when the config option DSL (`Doorkeeper::Config::Option`) is extended into a class that does not define `self.builder_class`. The guard raised `Doorkeeper::MissingConfigurationBuilderClass`, a constant that was never defined, so callers saw `uninitialized constant` instead of the intended message. The error is now defined as `Doorkeeper::Errors::MissingConfigurationBuilderClass` (a `DoorkeeperError`) and referenced correctly.
|
|
63
|
+
- [#1846] Document and pin with regression specs that a `scope` parameter sent to the token endpoint is ignored for the `authorization_code` grant (RFC 6749 §4.1.3 does not define one): the access token always inherits the scopes of the authorization grant, and the response reports the actual granted `scope`. No behavior change.
|
|
64
|
+
- [#1847] Fix `Doorkeeper.config.enabled_grant_flows` (and `calculate_grant_flows`) not listing the `refresh_token` grant flow when `use_refresh_token` is configured, so consumers (e.g. RFC 8414 metadata) no longer need to append it manually — the built-in metadata endpoint now relies on this too. The flow is no longer duplicated in `token_grant_flows` when `refresh_token` is also listed in `grant_flows` explicitly, a configuration-time warning is logged when `grant_flows` enables `refresh_token` without `use_refresh_token` (no refresh tokens would ever be issued), and the initializer template documents the flow.
|
|
65
|
+
- [#1848] Derive `token_endpoint_auth_methods_supported` in the RFC 8414 metadata response from the effective client authentication configuration (including a deprecated `client_credentials`-only setup) instead of hardcoding the default methods. Servers that customize `client_authentication` (including extension-registered methods like `private_key_jwt`) now see their metadata reflect what the server actually accepts; unregistered names are not advertised.
|
|
66
|
+
- [#1849] Support RFC 9207 (Authorization Server Issuer Identification): when `issuer` is configured, the `iss` parameter is added to the authorization responses redirected back to the client (successful and error responses alike) and `authorization_response_iss_parameter_supported` is advertised in the server metadata. Clients that parse the authorization redirect will start seeing the new `iss` parameter. A configured `issuer` that is not RFC-compliant (not an https URL, or containing a query/fragment) now logs a warning at boot, as does a path-bearing issuer, which RFC 8414 clients would not discover through Doorkeeper's root-only well-known metadata route.
|
|
67
|
+
- [#1850] Fix `reuse_access_token` returning a refresh token that doesn't match the request: token reuse now requires the candidate's refresh token presence to match what the request asks for, in both directions. A request that expects a refresh token (e.g. `use_refresh_token` enabled) no longer reuses a token issued without one (previously the refresh token was silently omitted), and a request that does not expect one no longer reuses a token that carries one (previously an unrequested refresh token was returned, reachable via a per-request `refresh_token_enabled` callable). The requirement participates in the token matching itself, so an older matching token that satisfies it is still reused; a fresh token is created only when none does. **API change for ORM extensions**: `matching_token_for` and `find_matching_token` now take an optional block (`&filter`) that a token must satisfy to count as a match — extensions overriding either method must accept the block and honor it (accept and yield).
|
|
68
|
+
- [#1851] Fix duplicate query parameter in the authorization callback when a client's registered `redirect_uri` already contains a parameter with the same name as a response parameter (e.g. `state`). The redirect query was merged with string keys on one side and symbol keys on the other, so a collision emitted the parameter twice (`?state=fixed&code=...&state=user`); the response parameter now overrides the registered one and appears exactly once. A blank response parameter (e.g. no `state` sent with the request) leaves the registered parameter untouched, per RFC 6749 §3.1.2.
|
|
69
|
+
- [#1852] Fix the `pkce_code_challenge_methods` config validator using line anchors (`^`/`$`) instead of string anchors (`\A`/`\z`), so a multi-line value such as `"plain\ngarbage"` passed validation and was retained as a (never-matching) challenge method instead of being rejected and reset to the default.
|
|
70
|
+
- [#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.
|
|
71
|
+
- [#1854] Fix the RFC 8414 metadata endpoint raising `ActionController::UrlGenerationError` (HTTP 500) when `use_doorkeeper` configures a custom controller whose namespace depth differs from `doorkeeper/metadata` (e.g. `controllers tokens: "custom_tokens"`).
|
|
72
|
+
- [#1855] Perform the fallback secret upgrade-on-access write (plain → hashed token or application secret) through the primary database role, so `enable_multiple_database_roles` setups no longer attempt the write on a read replica when the lookup happens in a request routed to the reading role.
|
|
73
|
+
- [#1857] Pin with regression specs that a `+` between scopes in a form-encoded token request is decoded as a space (so `scope=public+write` refreshes fine), while a percent-encoded literal `+` (`%2B`) names a single scope and is rejected when unknown, per RFC 6749 §3.3. Test-only change, closes [#1686].
|
|
74
|
+
- [#1859] Pin with regression specs that a refresh token bound to an expired access token can be revoked (fixed by [#1744]) and that the revoked refresh token is rejected at the token endpoint afterwards. Test-only change, closes [#1671].
|
|
75
|
+
- [#1860] Fix introspection of refresh tokens (RFC 7662): a refresh token bound to an expired access token now introspects as `active: true`, matching the token endpoint which still accepts it. The introspection response for a presented refresh token no longer includes the paired access token's `token_type` and `exp`. Fixes [#1858].
|
|
76
|
+
- [#1862] Document that with `reuse_access_token` enabled token matching considers only the application, resource owner, scopes and custom token attributes — separate authorization grants for the same combination intentionally share one access token — and pin the behavior with a regression spec. Docs/test-only change, closes [#1693].
|
|
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.
|
|
78
|
+
- [#1869] Improve test coverage
|
|
79
|
+
- [#1870] Fix: raise the intended `Doorkeeper::Errors::TokenGeneratorNotFound` / `UnableToGenerateToken` (instead of a confusing `NameError`) when `application_secret_generator` is misconfigured.
|
|
80
|
+
|
|
12
81
|
## 5.9.5
|
|
13
82
|
|
|
14
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.
|
|
15
|
-
|
|
16
|
-
## 5.9.4
|
|
17
|
-
|
|
18
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.
|
|
19
85
|
|
|
20
86
|
## 5.9.3
|
|
@@ -35,6 +101,7 @@ User-visible changes worth mentioning.
|
|
|
35
101
|
- [#1797] Fix `doorkeeper:db:cleanup` rake task failure on PostgreSQL
|
|
36
102
|
- [#1800] Set `@grant_type` in `ClientCredentialsRequest` and `RefreshTokenRequest` constructors so `request.grant_type` returns
|
|
37
103
|
the correct value in hooks like `before_successful_strategy_response`.
|
|
104
|
+
- [#1801] Fix `hash_application_secrets` fallback not upgrading secret on access.
|
|
38
105
|
- [#1802] Fix `filter_parameters` not applied when `Doorkeeper.configure` is called inside to_prepare.
|
|
39
106
|
- [#1804] Use `ActiveSupport.on_load(:active_record)` in ORM hooks to prevent loading ActiveRecord models too early
|
|
40
107
|
- [#1806] Fix token revocation bypass for public clients (RFC 7009)
|
data/README.md
CHANGED
|
@@ -24,6 +24,8 @@ Supported features:
|
|
|
24
24
|
- [OAuth 2.0 Threat Model and Security Considerations](https://datatracker.ietf.org/doc/html/rfc6819)
|
|
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
|
+
- [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)
|
|
27
29
|
|
|
28
30
|
## Table of Contents
|
|
29
31
|
|
|
@@ -37,6 +39,9 @@ Supported features:
|
|
|
37
39
|
- [Grape](#grape)
|
|
38
40
|
- [ORMs](#orms)
|
|
39
41
|
- [Extensions](#extensions)
|
|
42
|
+
- [Resource Indicators](#resource-indicators)
|
|
43
|
+
- [Custom Grant Flows](#custom-grant-flows)
|
|
44
|
+
- [Custom Client Authentication Methods](#custom-client-authentication-methods)
|
|
40
45
|
- [Example Applications](#example-applications)
|
|
41
46
|
- [Sponsors](#sponsors)
|
|
42
47
|
- [Development](#development)
|
|
@@ -106,6 +111,205 @@ Extensions that are not included by default and can be installed separately.
|
|
|
106
111
|
| CIBA - Client Initiated Backchannel Authentication Flow extension | [doorkeeper-ciba](https://github.com/autoseg/doorkeeper-ciba) |
|
|
107
112
|
| Device Authorization Grant | [doorkeeper-device_authorization_grant](https://github.com/exop-group/doorkeeper-device_authorization_grant) |
|
|
108
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
|
+
|
|
109
313
|
## Example Applications
|
|
110
314
|
|
|
111
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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
class MetadataController < Doorkeeper::ApplicationMetalController
|
|
5
|
+
def show
|
|
6
|
+
headers.merge!(metadata_response.headers)
|
|
7
|
+
render json: metadata_response.body,
|
|
8
|
+
status: metadata_response.status
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def metadata_response
|
|
14
|
+
@metadata_response ||= Doorkeeper::OAuth::MetadataResponse.new(
|
|
15
|
+
request.base_url,
|
|
16
|
+
->(**args) { url_for(**args) },
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -34,7 +34,7 @@ module Doorkeeper
|
|
|
34
34
|
|
|
35
35
|
# OAuth 2.0 Token Introspection - https://datatracker.ietf.org/doc/html/rfc7662
|
|
36
36
|
def introspect
|
|
37
|
-
introspection = OAuth::TokenIntrospection.new(server, token)
|
|
37
|
+
introspection = OAuth::TokenIntrospection.new(server, token, token_type: presented_token_type)
|
|
38
38
|
|
|
39
39
|
if introspection.authorized?
|
|
40
40
|
render json: introspection.to_json, status: 200
|
|
@@ -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
|
|
@@ -138,6 +142,15 @@ module Doorkeeper
|
|
|
138
142
|
RevocableTokens::RevocableRefreshToken.new(token)
|
|
139
143
|
end
|
|
140
144
|
|
|
145
|
+
# Revocation (RFC 7009) and introspection (RFC 7662) deliberately share
|
|
146
|
+
# the same request shape — `token` plus an optional `token_type_hint` —
|
|
147
|
+
# so both actions resolve the presented token through #revocable_token.
|
|
148
|
+
# The wrapper type doubles as the record of which credential the client
|
|
149
|
+
# actually presented.
|
|
150
|
+
def presented_token_type
|
|
151
|
+
revocable_token.is_a?(RevocableTokens::RevocableRefreshToken) ? :refresh_token : :access_token
|
|
152
|
+
end
|
|
153
|
+
|
|
141
154
|
def access_token
|
|
142
155
|
token = Doorkeeper.config.access_token_model.by_token(params["token"])
|
|
143
156
|
return unless token
|
|
@@ -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 %>
|
|
@@ -29,6 +41,12 @@
|
|
|
29
41
|
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
|
30
42
|
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
|
31
43
|
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
|
44
|
+
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
45
|
+
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% Array.wrap(@pre_auth.resource_indicators).each do |resource_value| %>
|
|
48
|
+
<%= hidden_field_tag "resource[]", resource_value, id: nil %>
|
|
49
|
+
<% end %>
|
|
32
50
|
<%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
|
|
33
51
|
<% end %>
|
|
34
52
|
<%= form_tag oauth_authorization_path, method: :delete do %>
|
|
@@ -40,6 +58,12 @@
|
|
|
40
58
|
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
|
41
59
|
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
|
42
60
|
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
|
61
|
+
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
62
|
+
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% Array.wrap(@pre_auth.resource_indicators).each do |resource_value| %>
|
|
65
|
+
<%= hidden_field_tag "resource[]", resource_value, id: nil %>
|
|
66
|
+
<% end %>
|
|
43
67
|
<%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
|
|
44
68
|
<% end %>
|
|
45
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.'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
Credentials = Struct.new(:uid, :secret) do
|
|
6
|
+
# Public clients may have their secret blank, but "credentials" are
|
|
7
|
+
# still present as long as the uid is present.
|
|
8
|
+
delegate :blank?, to: :uid
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
# Used when no registered client authentication method matches the
|
|
6
|
+
# request. It matches everything and authenticates to nothing, mirroring
|
|
7
|
+
# the previous behaviour where an unauthenticated request simply yielded
|
|
8
|
+
# no credentials.
|
|
9
|
+
class FallbackMethod
|
|
10
|
+
def self.matches_request?(_request)
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.authenticate(_request)
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|