doorkeeper 5.9.9 → 6.0.0.beta1
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 +44 -22
- data/README.md +1 -0
- data/app/controllers/doorkeeper/authorizations_controller.rb +3 -33
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +0 -23
- data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
- data/app/controllers/doorkeeper/tokens_controller.rb +10 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +6 -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 +46 -0
- data/lib/doorkeeper/client_authentication/method.rb +23 -0
- data/lib/doorkeeper/client_authentication/registry.rb +47 -0
- data/lib/doorkeeper/client_authentication.rb +93 -0
- data/lib/doorkeeper/config/option.rb +1 -1
- data/lib/doorkeeper/config/validations.rb +162 -1
- data/lib/doorkeeper/config.rb +114 -15
- data/lib/doorkeeper/errors.rb +1 -0
- data/lib/doorkeeper/models/access_token_mixin.rb +40 -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/uri_builder.rb +11 -0
- data/lib/doorkeeper/oauth/authorization_code_request.rb +4 -1
- data/lib/doorkeeper/oauth/client.rb +18 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +53 -0
- data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +29 -0
- data/lib/doorkeeper/oauth/client_authentication/none.rb +32 -0
- 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 +0 -14
- data/lib/doorkeeper/oauth/metadata_response.rb +157 -0
- data/lib/doorkeeper/oauth/pre_authorization.rb +12 -32
- data/lib/doorkeeper/oauth/token.rb +2 -95
- data/lib/doorkeeper/oauth/token_introspection.rb +27 -7
- data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
- data/lib/doorkeeper/orm/active_record/redirect_uri_validator.rb +1 -5
- data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
- data/lib/doorkeeper/rails/routes.rb +6 -0
- data/lib/doorkeeper/request.rb +65 -18
- data/lib/doorkeeper/server.rb +5 -2
- data/lib/doorkeeper/version.rb +4 -4
- data/lib/doorkeeper.rb +8 -4
- data/lib/generators/doorkeeper/templates/initializer.rb +67 -18
- metadata +14 -7
- data/lib/doorkeeper/oauth/client/credentials.rb +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 454edd4f496ec3750a0be3da7db728302d93b355c075d6aded28792c8a53b9a8
|
|
4
|
+
data.tar.gz: 8be2862ad184cb71e36a5b9d2747bfc5186fe8bf9c539b6d61fbe92d05a57e5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cb1367a63c3404a1dc79dd910265e5c189da7e8745b40c48f46e06d9b326f7a215760747c0273e28a51a26ff8e6447c376faf6a169d9f93cec84aac97191022
|
|
7
|
+
data.tar.gz: 3a3dc94969468b7ff5ef364876d068bf8fde231fcaeaa5ea6e43c207f7aafb236f02b5264b32678ad100788a1deb99caee79a3701cca1b4f79fd3a7f487865f1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,29 +5,49 @@ upgrade guides.
|
|
|
5
5
|
|
|
6
6
|
User-visible changes worth mentioning.
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
- [#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
8
|
+
## main
|
|
9
|
+
|
|
10
|
+
- Please add here
|
|
11
|
+
|
|
12
|
+
## 6.0.0.beta1
|
|
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
|
+
- [#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
|
|
18
|
+
- [#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.
|
|
19
|
+
- [#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.
|
|
20
|
+
- [#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.
|
|
21
|
+
- [#1840] Introduce a pluggable client authentication registry (RFC 6749 §2.3).
|
|
22
|
+
- 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`.
|
|
23
|
+
- Custom strategies can be registered with `Doorkeeper::ClientAuthentication.register`.
|
|
24
|
+
- **[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.
|
|
25
|
+
- **[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.
|
|
26
|
+
- **[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.
|
|
27
|
+
- **Deprecated**: the `client_credentials` option — use `client_authentication` instead.
|
|
28
|
+
- **Deprecated**: the `Doorkeeper::OAuth::Client::Credentials` constant itself — it remains as an alias of `Doorkeeper::ClientAuthentication::Credentials` (the plain uid/secret struct).
|
|
29
|
+
- See the [Upgrade Guide](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) for detailed breaking changes, deprecations and migration steps.
|
|
30
|
+
- [#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).
|
|
31
|
+
- [#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.
|
|
32
|
+
- [#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.
|
|
33
|
+
- [#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.
|
|
34
|
+
- [#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.
|
|
35
|
+
- [#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.
|
|
36
|
+
- [#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.
|
|
37
|
+
- [#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).
|
|
38
|
+
- [#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.
|
|
39
|
+
- [#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.
|
|
30
40
|
- [#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.
|
|
41
|
+
- [#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"`).
|
|
42
|
+
- [#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.
|
|
43
|
+
- [#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].
|
|
44
|
+
- [#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].
|
|
45
|
+
- [#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].
|
|
46
|
+
- [#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].
|
|
47
|
+
- [#1864] Fix `custom_access_token_attributes` values being dropped when the authorization goes through the consent screen: the approve/deny forms now carry the custom attributes as hidden fields, and the pre-authorization JSON (`api_only` mode) includes them so custom consent UIs can send them back.
|
|
48
|
+
- [#1869] Improve test coverage
|
|
49
|
+
- [#1870] Fix: raise the intended `Doorkeeper::Errors::TokenGeneratorNotFound` / `UnableToGenerateToken` (instead of a confusing `NameError`) when `application_secret_generator` is misconfigured.
|
|
50
|
+
- Please add here
|
|
31
51
|
|
|
32
52
|
## 5.9.3
|
|
33
53
|
|
|
@@ -47,6 +67,7 @@ User-visible changes worth mentioning.
|
|
|
47
67
|
- [#1797] Fix `doorkeeper:db:cleanup` rake task failure on PostgreSQL
|
|
48
68
|
- [#1800] Set `@grant_type` in `ClientCredentialsRequest` and `RefreshTokenRequest` constructors so `request.grant_type` returns
|
|
49
69
|
the correct value in hooks like `before_successful_strategy_response`.
|
|
70
|
+
- [#1801] Fix `hash_application_secrets` fallback not upgrading secret on access.
|
|
50
71
|
- [#1802] Fix `filter_parameters` not applied when `Doorkeeper.configure` is called inside to_prepare.
|
|
51
72
|
- [#1804] Use `ActiveSupport.on_load(:active_record)` in ORM hooks to prevent loading ActiveRecord models too early
|
|
52
73
|
- [#1806] Fix token revocation bypass for public clients (RFC 7009)
|
|
@@ -77,6 +98,7 @@ User-visible changes worth mentioning.
|
|
|
77
98
|
- [#1779] Only lock previous access token model when creating a new token from its refresh token if revoke_previous_refresh_token_on_use is false
|
|
78
99
|
- [#1778] Ensure that token revocation is idempotent by checking that that token has not already been revoked before revoking.
|
|
79
100
|
|
|
101
|
+
|
|
80
102
|
## 5.8.2
|
|
81
103
|
|
|
82
104
|
- [#1755] Fix the error message for force_pkce
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@ 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
|
|
27
28
|
|
|
28
29
|
## Table of Contents
|
|
29
30
|
|
|
@@ -16,13 +16,7 @@ module Doorkeeper
|
|
|
16
16
|
redirect_or_render(authorize_response)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# The deny path answers a request the client sent, so it is subject to the
|
|
20
|
-
# same client and redirect URI validation as #create: without it an
|
|
21
|
-
# unregistered redirect_uri was honoured and the user-agent redirected to
|
|
22
|
-
# it, carrying the OAuth `state` to an origin of the caller's choosing.
|
|
23
19
|
def destroy
|
|
24
|
-
return if refuse_invalid_client?
|
|
25
|
-
|
|
26
20
|
redirect_or_render(authorization.deny)
|
|
27
21
|
rescue Doorkeeper::Errors::InvalidTokenStrategy => e
|
|
28
22
|
error_response = get_error_response_from_exception(e)
|
|
@@ -51,37 +45,13 @@ module Doorkeeper
|
|
|
51
45
|
|
|
52
46
|
if Doorkeeper.configuration.redirect_on_errors? && pre_auth.error_response.redirectable?
|
|
53
47
|
redirect_or_render(pre_auth.error_response)
|
|
48
|
+
elsif Doorkeeper.configuration.api_only
|
|
49
|
+
render json: pre_auth.error_response.body, status: pre_auth.error_response.status
|
|
54
50
|
else
|
|
55
|
-
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def render_error_response(error_response)
|
|
60
|
-
if Doorkeeper.configuration.api_only
|
|
61
|
-
render json: error_response.body, status: error_response.status
|
|
62
|
-
else
|
|
63
|
-
render :error, locals: { error_response: error_response }, status: error_response.status
|
|
51
|
+
render :error, locals: { error_response: pre_auth.error_response }, status: pre_auth.error_response.status
|
|
64
52
|
end
|
|
65
53
|
end
|
|
66
54
|
|
|
67
|
-
# Renders the error when the request's client_id or redirect_uri is missing
|
|
68
|
-
# or invalid, and answers whether the request was refused. Errors are
|
|
69
|
-
# rendered, never redirected, regardless of `handle_auth_errors :redirect`:
|
|
70
|
-
# no failure this check can produce leaves a redirect target worth
|
|
71
|
-
# trusting. Either the client failed first, so the redirect URI was never
|
|
72
|
-
# reached, or the redirect URI is itself the invalid one - and RFC 6749
|
|
73
|
-
# Section 3.1.2.4 forbids redirecting the user-agent to an invalid
|
|
74
|
-
# redirection URI.
|
|
75
|
-
def refuse_invalid_client?
|
|
76
|
-
return false if pre_auth.client_valid?
|
|
77
|
-
|
|
78
|
-
error_response = pre_auth.error_response
|
|
79
|
-
error_response.raise_exception! if Doorkeeper.config.raise_on_errors?
|
|
80
|
-
|
|
81
|
-
render_error_response(error_response)
|
|
82
|
-
true
|
|
83
|
-
end
|
|
84
|
-
|
|
85
55
|
def can_authorize_response?
|
|
86
56
|
Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token?
|
|
87
57
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
class AuthorizedApplicationsController < Doorkeeper::ApplicationController
|
|
5
5
|
before_action :authenticate_resource_owner!
|
|
6
|
-
before_action :validate_resource_owner
|
|
7
6
|
|
|
8
7
|
def index
|
|
9
8
|
@applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner)
|
|
@@ -30,27 +29,5 @@ module Doorkeeper
|
|
|
30
29
|
format.json { head :no_content }
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
# `authenticate_resource_owner!` hands the request to the host
|
|
37
|
-
# application's `resource_owner_authenticator` block, which is expected to
|
|
38
|
-
# halt the request itself — redirect to a sign-in page, raise, render —
|
|
39
|
-
# when nobody is signed in. A block that merely answers nil halts nothing:
|
|
40
|
-
# the library default (no block configured) logs a warning and answers
|
|
41
|
-
# nil, and so does any block written as a bare lookup such as
|
|
42
|
-
# `User.find_by(id: session[:user_id])`.
|
|
43
|
-
#
|
|
44
|
-
# Both actions here scope their work to `current_resource_owner`, and a
|
|
45
|
-
# nil owner is not "no scope" — it is the scope of the records that have
|
|
46
|
-
# no resource owner, which is what the client credentials flow issues.
|
|
47
|
-
# #index would list every application holding one and #destroy would
|
|
48
|
-
# revoke them, for a caller that never authenticated. So refuse the
|
|
49
|
-
# request rather than treat "nobody" as an owner. Only this controller is
|
|
50
|
-
# guarded; `authenticate_resource_owner!` itself is left alone because
|
|
51
|
-
# AuthorizationsController shapes its own response around the owner.
|
|
52
|
-
def validate_resource_owner
|
|
53
|
-
head :unauthorized unless current_resource_owner
|
|
54
|
-
end
|
|
55
32
|
end
|
|
56
33
|
end
|
|
@@ -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
|
|
@@ -138,6 +138,15 @@ module Doorkeeper
|
|
|
138
138
|
RevocableTokens::RevocableRefreshToken.new(token)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# Revocation (RFC 7009) and introspection (RFC 7662) deliberately share
|
|
142
|
+
# the same request shape — `token` plus an optional `token_type_hint` —
|
|
143
|
+
# so both actions resolve the presented token through #revocable_token.
|
|
144
|
+
# The wrapper type doubles as the record of which credential the client
|
|
145
|
+
# actually presented.
|
|
146
|
+
def presented_token_type
|
|
147
|
+
revocable_token.is_a?(RevocableTokens::RevocableRefreshToken) ? :refresh_token : :access_token
|
|
148
|
+
end
|
|
149
|
+
|
|
141
150
|
def access_token
|
|
142
151
|
token = Doorkeeper.config.access_token_model.by_token(params["token"])
|
|
143
152
|
return unless token
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
|
30
30
|
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
|
31
31
|
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
|
32
|
+
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
33
|
+
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
34
|
+
<% end %>
|
|
32
35
|
<%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
|
|
33
36
|
<% end %>
|
|
34
37
|
<%= form_tag oauth_authorization_path, method: :delete do %>
|
|
@@ -40,6 +43,9 @@
|
|
|
40
43
|
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
|
41
44
|
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
|
42
45
|
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
|
46
|
+
<% @pre_auth.custom_access_token_attributes.each do |attribute_name, attribute_value| %>
|
|
47
|
+
<%= hidden_field_tag attribute_name, attribute_value, id: nil %>
|
|
48
|
+
<% end %>
|
|
43
49
|
<%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
|
|
44
50
|
<% end %>
|
|
45
51
|
</div>
|
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
# Adapter that lets a legacy callable +client_credentials+ extractor
|
|
6
|
+
# (e.g. +->(request) { [client_id, client_secret] }+) keep working through
|
|
7
|
+
# the client authentication registry during the deprecation window.
|
|
8
|
+
#
|
|
9
|
+
# The callable is expected to return a +[uid, secret]+ pair (or +nil+ when
|
|
10
|
+
# it does not apply), matching the historical +Credentials.from_request+
|
|
11
|
+
# contract. It is considered to match a request whenever it yields present
|
|
12
|
+
# credentials, mirroring the previous "first extractor that returns a uid
|
|
13
|
+
# wins" behaviour.
|
|
14
|
+
class LegacyCallable
|
|
15
|
+
def initialize(callable)
|
|
16
|
+
@callable = callable
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches_request?(request)
|
|
20
|
+
credentials_for(request).present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authenticate(request)
|
|
24
|
+
credentials_for(request)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Invoke the wrapped extractor at most once per request. +matches_request?+
|
|
30
|
+
# and +authenticate+ both need the extracted credentials, and the legacy
|
|
31
|
+
# +Credentials.from_request+ contract called each extractor exactly once,
|
|
32
|
+
# so cache the result on the request itself to preserve that. The adapter
|
|
33
|
+
# is stored in the config/registry and therefore shared across requests
|
|
34
|
+
# and threads, so the cache must live on the per-request +env+ (keyed per
|
|
35
|
+
# adapter instance) rather than on the adapter, which would otherwise race.
|
|
36
|
+
def credentials_for(request)
|
|
37
|
+
request.env[cache_key] ||=
|
|
38
|
+
Doorkeeper::ClientAuthentication::Credentials.new(*@callable.call(request))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cache_key
|
|
42
|
+
@cache_key ||= "doorkeeper.client_authentication.legacy_callable.#{@callable.object_id}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
# Wraps a registered client authentication method, pairing its
|
|
6
|
+
# registration +name+ with the +strategy+ object that knows how to match
|
|
7
|
+
# and authenticate a request.
|
|
8
|
+
#
|
|
9
|
+
# NOTE: the wrapped object is exposed as +strategy+ rather than +method+ on
|
|
10
|
+
# purpose — an +attr_reader :method+ would shadow Ruby's core
|
|
11
|
+
# +Object#method+ reflection API and break +wrapper.method(:authenticate)+.
|
|
12
|
+
class Method
|
|
13
|
+
attr_reader :name, :strategy
|
|
14
|
+
|
|
15
|
+
delegate :matches_request?, :authenticate, to: :strategy
|
|
16
|
+
|
|
17
|
+
def initialize(name, strategy)
|
|
18
|
+
@name = name
|
|
19
|
+
@strategy = strategy
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doorkeeper
|
|
4
|
+
module ClientAuthentication
|
|
5
|
+
# Holds the registered client authentication methods and provides the DSL
|
|
6
|
+
# to register and look them up by name.
|
|
7
|
+
module Registry
|
|
8
|
+
mattr_accessor :registered_methods
|
|
9
|
+
self.registered_methods = {}
|
|
10
|
+
|
|
11
|
+
# Allows to register a custom OAuth client authentication method so that
|
|
12
|
+
# Doorkeeper could recognize and process it.
|
|
13
|
+
#
|
|
14
|
+
def register(name, method)
|
|
15
|
+
unless name.respond_to?(:to_sym)
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"client authentication method name must be a Symbol or String, got #{name.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
unless method.respond_to?(:matches_request?) && method.respond_to?(:authenticate)
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"client authentication method '#{name}' must respond to " \
|
|
23
|
+
"#matches_request? and #authenticate, got #{method.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
name_key = name.to_sym
|
|
27
|
+
|
|
28
|
+
if registered_methods.key?(name_key)
|
|
29
|
+
::Kernel.warn <<~WARNING
|
|
30
|
+
[DOORKEEPER] '#{name_key}' client authentication strategy is already registered and will be overridden
|
|
31
|
+
in #{caller(1..1).first}
|
|
32
|
+
WARNING
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
registered_methods[name_key] = Doorkeeper::ClientAuthentication::Method.new(name_key, method)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# [NOTE]: switch to #fetch once the deprecated client_credentials
|
|
39
|
+
# fallbacks are removed.
|
|
40
|
+
def get(name)
|
|
41
|
+
return unless name.respond_to?(:to_sym)
|
|
42
|
+
|
|
43
|
+
registered_methods[name.to_sym]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "doorkeeper/client_authentication/credentials"
|
|
4
|
+
require "doorkeeper/client_authentication/fallback_method"
|
|
5
|
+
require "doorkeeper/client_authentication/legacy_callable"
|
|
6
|
+
require "doorkeeper/client_authentication/method"
|
|
7
|
+
require "doorkeeper/client_authentication/registry"
|
|
8
|
+
|
|
9
|
+
module Doorkeeper
|
|
10
|
+
# Registry of the OAuth client authentication methods (RFC 6749 §2.3)
|
|
11
|
+
# Doorkeeper knows how to process. Each registered method is able to tell
|
|
12
|
+
# whether it +matches_request?+ and how to +authenticate+ it into a
|
|
13
|
+
# Credentials object.
|
|
14
|
+
module ClientAuthentication
|
|
15
|
+
extend Registry
|
|
16
|
+
|
|
17
|
+
# Default ordered client authentication methods (RFC 6749 §2.3) used when
|
|
18
|
+
# +client_authentication+ is not configured.
|
|
19
|
+
DEFAULT_METHODS = %i[client_secret_basic client_secret_post none].freeze
|
|
20
|
+
|
|
21
|
+
# Maps the current method names back onto the deprecated +client_credentials+
|
|
22
|
+
# names so the +client_credentials_methods+ alias can keep returning the
|
|
23
|
+
# legacy symbols that external callers (e.g. doorkeeper-openid_connect) map
|
|
24
|
+
# from. Names without a legacy equivalent (e.g. +:none+, custom methods) are
|
|
25
|
+
# returned unchanged — consumers that don't recognise them drop them, just
|
|
26
|
+
# as they did before.
|
|
27
|
+
LEGACY_NAME_FOR = {
|
|
28
|
+
client_secret_basic: :from_basic,
|
|
29
|
+
client_secret_post: :from_params,
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Converts resolved Method objects into the legacy client_credentials
|
|
33
|
+
# symbol names for the deprecated +client_credentials_methods+ alias.
|
|
34
|
+
def self.to_legacy_client_credentials_names(methods)
|
|
35
|
+
methods.map { |method| LEGACY_NAME_FOR.fetch(method.name, method.name) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
register(
|
|
39
|
+
:none,
|
|
40
|
+
Doorkeeper::OAuth::ClientAuthentication::None,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
register(
|
|
44
|
+
:client_secret_post,
|
|
45
|
+
Doorkeeper::OAuth::ClientAuthentication::ClientSecretPost,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
register(
|
|
49
|
+
:client_secret_basic,
|
|
50
|
+
Doorkeeper::OAuth::ClientAuthentication::ClientSecretBasic,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Converts a deprecated +client_credentials+ configuration into the client
|
|
54
|
+
# authentication method names / adapters understood by the registry.
|
|
55
|
+
# Unknown values are warned about and dropped; callables are wrapped in a
|
|
56
|
+
# LegacyCallable adapter. +:none+ (public client support) is appended only
|
|
57
|
+
# for +:from_params+ — the sole legacy method that accepted a bare
|
|
58
|
+
# +client_id+ without a secret — so a Basic-only configuration is not
|
|
59
|
+
# silently broadened.
|
|
60
|
+
def self.from_legacy_client_credentials(methods)
|
|
61
|
+
converted = methods.filter_map { |method| legacy_client_credential(method) }
|
|
62
|
+
converted.push(:none) if methods.include?(:from_params)
|
|
63
|
+
converted
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.legacy_client_credential(method)
|
|
67
|
+
case method
|
|
68
|
+
when :from_basic
|
|
69
|
+
:client_secret_basic
|
|
70
|
+
when :from_params
|
|
71
|
+
:client_secret_post
|
|
72
|
+
else
|
|
73
|
+
legacy_extractor_or_nil(method)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
private_class_method :legacy_client_credential
|
|
77
|
+
|
|
78
|
+
def self.legacy_extractor_or_nil(method)
|
|
79
|
+
unless method.respond_to?(:call)
|
|
80
|
+
Kernel.warn("[DOORKEEPER] Unknown client_credentials method detected: #{method}")
|
|
81
|
+
return nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Kernel.warn(
|
|
85
|
+
"[DOORKEEPER] client_credentials callable extractors are deprecated; wrapping it in a " \
|
|
86
|
+
"legacy client authentication adapter. Register it via " \
|
|
87
|
+
"Doorkeeper::ClientAuthentication.register instead.",
|
|
88
|
+
)
|
|
89
|
+
Method.new(:legacy_callable, LegacyCallable.new(method))
|
|
90
|
+
end
|
|
91
|
+
private_class_method :legacy_extractor_or_nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -74,7 +74,7 @@ module Doorkeeper
|
|
|
74
74
|
def self.extended(base)
|
|
75
75
|
return if base.respond_to?(:builder_class)
|
|
76
76
|
|
|
77
|
-
raise Doorkeeper::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \
|
|
77
|
+
raise Doorkeeper::Errors::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \
|
|
78
78
|
"for #{base} that returns your custom Builder class to use options DSL!"
|
|
79
79
|
end
|
|
80
80
|
end
|