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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -3
  3. data/README.md +204 -0
  4. data/app/controllers/doorkeeper/applications_controller.rb +38 -9
  5. data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
  6. data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
  7. data/app/controllers/doorkeeper/tokens_controller.rb +15 -2
  8. data/app/views/doorkeeper/authorizations/new.html.erb +24 -0
  9. data/config/locales/en.yml +2 -0
  10. data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
  11. data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
  12. data/lib/doorkeeper/client_authentication/legacy_callable.rb +52 -0
  13. data/lib/doorkeeper/client_authentication/method.rb +40 -0
  14. data/lib/doorkeeper/client_authentication/registry.rb +47 -0
  15. data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
  16. data/lib/doorkeeper/client_authentication.rb +102 -0
  17. data/lib/doorkeeper/config/option.rb +1 -1
  18. data/lib/doorkeeper/config/validations.rb +162 -1
  19. data/lib/doorkeeper/config.rb +161 -9
  20. data/lib/doorkeeper/document_cache.rb +81 -0
  21. data/lib/doorkeeper/errors.rb +49 -0
  22. data/lib/doorkeeper/http_fetcher.rb +232 -0
  23. data/lib/doorkeeper/models/access_grant_mixin.rb +16 -0
  24. data/lib/doorkeeper/models/access_token_mixin.rb +81 -5
  25. data/lib/doorkeeper/models/application_mixin.rb +14 -6
  26. data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
  27. data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
  28. data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
  29. data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
  30. data/lib/doorkeeper/oauth/authorization_code_request.rb +127 -3
  31. data/lib/doorkeeper/oauth/base_request.rb +1 -2
  32. data/lib/doorkeeper/oauth/client.rb +24 -0
  33. data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +57 -0
  34. data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +33 -0
  35. data/lib/doorkeeper/oauth/client_authentication/none.rb +71 -0
  36. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/key_resolver.rb +104 -0
  37. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb +78 -0
  38. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +247 -0
  39. data/lib/doorkeeper/oauth/client_credentials/creator.rb +25 -9
  40. data/lib/doorkeeper/oauth/client_credentials_request.rb +36 -5
  41. data/lib/doorkeeper/oauth/code_response.rb +16 -2
  42. data/lib/doorkeeper/oauth/error_response.rb +11 -2
  43. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
  44. data/lib/doorkeeper/oauth/metadata_response.rb +165 -0
  45. data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
  46. data/lib/doorkeeper/oauth/pre_authorization.rb +63 -8
  47. data/lib/doorkeeper/oauth/refresh_token_request.rb +51 -1
  48. data/lib/doorkeeper/oauth/resource_indicator_validator.rb +70 -0
  49. data/lib/doorkeeper/oauth/scopes.rb +25 -0
  50. data/lib/doorkeeper/oauth/token_introspection.rb +33 -7
  51. data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
  52. data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
  53. data/lib/doorkeeper/rails/routes.rb +6 -0
  54. data/lib/doorkeeper/request.rb +59 -0
  55. data/lib/doorkeeper/server.rb +5 -2
  56. data/lib/doorkeeper/version.rb +4 -4
  57. data/lib/doorkeeper.rb +12 -4
  58. data/lib/generators/doorkeeper/grant_reuse_revocation_generator.rb +43 -0
  59. data/lib/generators/doorkeeper/resource_indicators_generator.rb +33 -0
  60. data/lib/generators/doorkeeper/templates/add_access_token_to_access_grants.rb.erb +10 -0
  61. data/lib/generators/doorkeeper/templates/enable_resource_indicators_migration.rb.erb +8 -0
  62. data/lib/generators/doorkeeper/templates/initializer.rb +131 -6
  63. data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
  64. metadata +52 -4
  65. data/lib/doorkeeper/oauth/client/credentials.rb +0 -71
  66. data/lib/doorkeeper/oauth/stateless_token.rb +0 -139
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3e4ee342094b7729f29bb64d9045cfd9736d99388c5bb15d68568c6d59a8a54
4
- data.tar.gz: e1549e74f00d26b85ed9afedbfaba17adce5a4e8a62c5ee01a8c4ff70c71bbcf
3
+ metadata.gz: b401160f58e48398c2e25631a27e5dabc423480e08f34a64c91c25107ffcab53
4
+ data.tar.gz: 6c6dc01f00118c8525fe7ec329cab28cdaad05b5c5b06402cbb7f59ceb565e1a
5
5
  SHA512:
6
- metadata.gz: d91e3f678a7a3d53e94c607b5b88d5a6e695e166a52fd9f5527c8988bd717c60debc292798c159da7356966baa4d26ad22d56a9b922582040e758c37ef26ea96
7
- data.tar.gz: e335f698b2a3fe29b139e826fafada70b94fd239a0c2e0f2be762075c27bb73c8dab4c93a9581089214b07f9016af893cd0c70daf0fdc2db9c8b72f7ae848850
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 { redirect_to oauth_application_url(@application) }
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 { redirect_to oauth_application_url(@application) }
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
- flash[:notice] = I18n.t(:notice, scope: i18n_scope(:destroy)) if @application.destroy
81
+ destroyed = @application.destroy
77
82
 
78
83
  respond_to do |format|
79
- format.html { redirect_to oauth_applications_url }
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
- params.slice(*pre_auth_param_fields).permit(*pre_auth_param_fields)
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>
@@ -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