doorkeeper-openid_connect 1.9.0 → 1.10.0
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 +40 -0
- data/README.md +212 -9
- data/app/controllers/concerns/doorkeeper/openid_connect/authorizations_extension.rb +2 -1
- data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +11 -26
- data/app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb +54 -17
- data/config/locales/en.yml +2 -0
- data/lib/doorkeeper/oauth/id_token_response.rb +1 -1
- data/lib/doorkeeper/oauth/id_token_token_response.rb +2 -1
- data/lib/doorkeeper/openid_connect/claims/claim.rb +21 -9
- data/lib/doorkeeper/openid_connect/claims_builder.rb +3 -2
- data/lib/doorkeeper/openid_connect/config.rb +20 -9
- data/lib/doorkeeper/openid_connect/engine.rb +1 -1
- data/lib/doorkeeper/openid_connect/errors.rb +3 -1
- data/{app/controllers/concerns → lib}/doorkeeper/openid_connect/grant_types_supported_mixin.rb +1 -1
- data/lib/doorkeeper/openid_connect/helpers/controller.rb +160 -31
- data/lib/doorkeeper/openid_connect/id_token.rb +15 -11
- data/lib/doorkeeper/openid_connect/id_token_token.rb +11 -4
- data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +5 -5
- data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +108 -0
- data/lib/doorkeeper/openid_connect/oauth/password_access_token_request.rb +5 -3
- data/lib/doorkeeper/openid_connect/oauth/pre_authorization.rb +2 -2
- data/lib/doorkeeper/openid_connect/oauth/token_response.rb +1 -1
- data/lib/doorkeeper/openid_connect/orm/active_record/access_grant.rb +1 -1
- data/lib/doorkeeper/openid_connect/orm/active_record/mixins/openid_request.rb +2 -2
- data/lib/doorkeeper/openid_connect/orm/active_record/request.rb +1 -1
- data/lib/doorkeeper/openid_connect/orm/active_record.rb +22 -14
- data/lib/doorkeeper/openid_connect/rails/routes/mapping.rb +3 -3
- data/lib/doorkeeper/openid_connect/rails/routes.rb +11 -11
- data/lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb +18 -0
- data/lib/doorkeeper/openid_connect/user_info.rb +6 -3
- data/lib/doorkeeper/openid_connect/version.rb +1 -1
- data/lib/doorkeeper/openid_connect.rb +128 -48
- data/lib/doorkeeper/request/id_token.rb +1 -1
- data/lib/doorkeeper/request/id_token_token.rb +1 -1
- data/lib/generators/doorkeeper/openid_connect/install_generator.rb +6 -5
- data/lib/generators/doorkeeper/openid_connect/migration_generator.rb +8 -8
- data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +36 -2
- metadata +17 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b09f0d036559583b3db8dd63e6e35452e5f9c42c90ce098ab263c1817016994
|
|
4
|
+
data.tar.gz: f9c9584568d90c9d351a347d435ba471d8e798aa42c9ac28edebefcd59d33c1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5a7f986bd19adcc2f3b4a03467d6052e32722f06162446b5ba944a8b905a111aa27528ccfc827e493382e95ab9dc89834a4ee483895951883291bda1387db81
|
|
7
|
+
data.tar.gz: c9fc3444e391e9b971bd193b18f388dafa47c5f5d6188d055fad2f524f9c8d6ceb67929a6b2ab5179ab50ff3c854e8d4883c9f6a43b9cd650cceba97a2e4dee2
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
- Please add here
|
|
4
4
|
|
|
5
|
+
## v1.10.0 (2026-06-01)
|
|
6
|
+
|
|
7
|
+
- [#241] Fix NameError on doorkeeper master by deferring AR model loading in run_hooks (see [Doorkeeper PR](https://github.com/doorkeeper-gem/doorkeeper/pull/1804))
|
|
8
|
+
- [#242] Fix `NoMethodError` for openid_request in testing environments.
|
|
9
|
+
- [#246] Fix `at_hash` to use correct hash algorithm based on `signing_algorithm`
|
|
10
|
+
- [#250] Return configured `issuer` instead of `root_url` in WebFinger response (thanks to @sato11 for the original work in #172)
|
|
11
|
+
- [#248] Fix `max_age` always triggering reauthentication when `auth_time_from_resource_owner` returns Integer
|
|
12
|
+
- [#254] **Breaking:** Omit `expires_in` from the `response_type=id_token` response (OIDC Core §3.2.2.5 — `expires_in` represents the Access Token lifetime; it is still returned for `response_type=id_token token`)
|
|
13
|
+
- [#252] Treat `auth_time_from_resource_owner` as optional in `IdToken` — omit `auth_time` claim when unconfigured instead of raising `InvalidConfiguration`
|
|
14
|
+
- [#256] Accept non-callable values (symbol / string) for the `protocol` config option, matching the pattern used by `issuer` / `signing_algorithm` / `signing_key` / `expiration`
|
|
15
|
+
- [#258] Skip `IdToken` construction on password grants without the `openid` scope
|
|
16
|
+
- [#259] Skip `IdToken` construction on authorization code grants without the `openid` scope
|
|
17
|
+
- [#261] Fix obsolete RuboCop configuration (`require:` → `plugins:`, `RSpec/FilePath` split, remove `Capybara/FeatureMethods`)
|
|
18
|
+
- [#263] **Security/Breaking:** Determine dynamically registered client's `confidential` flag from `token_endpoint_auth_method` per RFC 7591 — previously every dynamically registered client was created as public (`confidential: false`), which let callers authenticate with only `client_id` (`by_uid_and_secret(uid, nil)` bypass). Default is now `client_secret_basic` (confidential); `none` produces a public client; unsupported values (e.g. `private_key_jwt`) are rejected with `invalid_client_metadata`. Also derive `token_endpoint_auth_methods_supported` in the response from `Doorkeeper.configuration.client_credentials_methods` instead of a hardcoded list, matching #236
|
|
19
|
+
- [#264] Apply safe RuboCop autocorrections and fix resulting artifacts
|
|
20
|
+
- [#265] Add Dynamic Client Registration section to README
|
|
21
|
+
- [#266] Validate `application_type`, `response_types`, and `grant_types` parameters in dynamic client registration per RFC 7591 — reject unsupported values with `invalid_client_metadata` and echo the requested values back in the registration response, instead of silently ignoring them and returning the server's global configuration
|
|
22
|
+
- [#267] Add `authorize_dynamic_client_registration` config option to gate the dynamic client registration endpoint per RFC 7591 §3.1 — when set to a callable, the block is evaluated in the controller scope (with access to `request`, `params`, `request.headers`, etc.) and falsy return values reject the request with `401 invalid_token`. Default is `nil` so the endpoint remains open for backward compatibility; consumers should configure this to validate an Initial Access Token (or any other authorization scheme) before allowing client registration
|
|
23
|
+
- [#268] Update Dynamic Client Registration README for validated metadata parameters
|
|
24
|
+
- [#269] Document `authorize_dynamic_client_registration` in README
|
|
25
|
+
- [#270] Document the unified issuer block signature in README
|
|
26
|
+
- [#278] Test against Ruby 4.0.
|
|
27
|
+
- [#271] **Security:** Add `auth_time_from_session` config for per-session `max_age` enforcement. The legacy `auth_time_from_resource_owner` cannot distinguish between concurrent sessions and is now deprecated for `max_age` use (see [#150](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/150))
|
|
28
|
+
- [#272] Document `auth_time_from_session` in README (follow-up to [#271](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/pull/271))
|
|
29
|
+
- [#273] **Security/Hardening:** Merge framework-controlled registered claims last — `iss`/`sub`/`aud`/`exp`/`iat`/`nonce`/`auth_time` for the ID Token and `sub` for UserInfo — so a custom claim block can no longer override security-critical values. No legitimate configuration relied on this; custom claims that intentionally shadowed a registered claim name will now be ignored for that key (OIDC Core §2 / §3.1.3.7 / §5.3.2).
|
|
30
|
+
- [#276] Get RuboCop to zero offenses: fix `Lint/MissingSuper` in `IdTokenResponse`, replace `puts` with `warn` for deprecation notices, and modernise spec style
|
|
31
|
+
- [#277] Fix README inaccuracies (`signing_algorithm` description and link, `discovery_url_options` endpoint list, `oauth-authorization-server` route) and use constant-time comparison in the DCR authorization example to prevent timing attacks on the Initial Access Token
|
|
32
|
+
- [#279] Return `account_selection_required` when a `prompt=select_account` handler does not generate a response, per [OIDC Core 1.0 §3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError) — previously the authorization silently continued without account selection. Adds the missing `Errors::AccountSelectionRequired` class, mirroring the existing `login_required` backstop for `reauthenticate_resource_owner`
|
|
33
|
+
- [#275] Return `login_required` for `max_age` reauthentication when `prompt=none`, instead of triggering the interactive `reauthenticate_resource_owner` flow (OIDC Core §3.1.2.1)
|
|
34
|
+
- [#284] Document `acr` / `amr` claims in README — show how to expose Authentication Context Class Reference and Authentication Methods References via the `claim` DSL, with callouts for the `response:` and `scope:` defaults that silently bite
|
|
35
|
+
- [#288] Document `offline_access` scope recipe in README — show how to wire `use_refresh_token` with scope-based filtering for OIDC offline access
|
|
36
|
+
- [#281] Fix `NoMethodError` / `DoubleRenderError` when `resource_owner_authenticator` redirects with a truthy non-model value (e.g. `current_user || redirect_to(login_url)`). Normalize the leaked value to `nil` when `performed?` and add missing `if owner` guard on `select_account`.
|
|
37
|
+
- [#285] Document custom `jwks_uri` path pattern in README — show how to advertise a non-default path in the discovery document using Rails' `direct` URL helper
|
|
38
|
+
- [#283] Support multiple signing keys in the JWKS response — `signing_key` now also accepts an array (and callables returning an array). The first entry is the active key used to sign new ID tokens; the remaining entries are published in the JWKS so clients can still validate tokens signed with a retired key during a rotation window. Single-value and callable forms continue to work unchanged
|
|
39
|
+
- [#286] Allow claims to be assigned to multiple scopes via `scope: [:profile, :all_data]` — the claim is returned whenever the access token grants any of the listed scopes. **Note:** the previously implicit `Claim#scope=` writer (from `attr_accessor :scope`) is no longer provided; rebuild the claim instead of mutating it
|
|
40
|
+
- [#287] Add `apply_prompt_to_non_oidc_requests` option to honor the `prompt` parameter on plain OAuth requests that do not include the `openid` scope
|
|
41
|
+
- [#282] Allow `prompt=none` reauthorization with a narrower subset of previously-granted scopes (issue #63). Per RFC 6749 §1.5, narrower-or-equal scopes do not require fresh user consent; previously these requests returned `consent_required`.
|
|
42
|
+
- [#290] Freeze `Claim#scopes` and `Claim#response` arrays at construction so callers can't accidentally mutate the claim's internal state from outside
|
|
43
|
+
- [#297] Fix the generated initializer's `issuer` example referencing an undefined `request` local (the block parameter is `_request`), which raised `NameError` when copied verbatim
|
|
44
|
+
|
|
5
45
|
## v1.9.0 (2026-03-16)
|
|
6
46
|
|
|
7
47
|
- [#229] Allow to application manage signing key and algorithm
|
data/README.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
[](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-openid_connect)
|
|
5
5
|
[](https://rubygems.org/gems/doorkeeper-openid_connect)
|
|
6
6
|
|
|
7
|
-
#### :warning: **This project is looking for maintainers, see [this issue](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/89).**
|
|
8
|
-
|
|
9
7
|
This library implements an [OpenID Connect](http://openid.net/connect/) authentication provider for Rails applications on top of the [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) OAuth 2.0 framework.
|
|
10
8
|
|
|
11
9
|
OpenID Connect is a single-sign-on and identity layer with a [growing list of server and client implementations](http://openid.net/developers/libraries/). If you're looking for a client in Ruby check out [omniauth_openid_connect](https://github.com/m0n9oose/omniauth_openid_connect/).
|
|
@@ -22,6 +20,7 @@ OpenID Connect is a single-sign-on and identity layer with a [growing list of se
|
|
|
22
20
|
- [Routes](#routes)
|
|
23
21
|
- [Nonces](#nonces)
|
|
24
22
|
- [Internationalization (I18n)](#internationalization-i18n)
|
|
23
|
+
- [Dynamic Client Registration](#dynamic-client-registration)
|
|
25
24
|
- [Development](#development)
|
|
26
25
|
- [License](#license)
|
|
27
26
|
- [Sponsors](#sponsors)
|
|
@@ -35,8 +34,9 @@ The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-
|
|
|
35
34
|
- [UserInfo Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
|
|
36
35
|
- [Normal Claims](http://openid.net/specs/openid-connect-core-1_0.html#NormalClaims)
|
|
37
36
|
- [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
|
|
37
|
+
- [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
|
|
38
38
|
|
|
39
|
-
In addition we also support most of [OpenID Connect Discovery 1.0](http://openid.net/specs/openid-connect-discovery-1_0.html) for automatic configuration discovery.
|
|
39
|
+
In addition, we also support most of [OpenID Connect Discovery 1.0](http://openid.net/specs/openid-connect-discovery-1_0.html) for automatic configuration discovery.
|
|
40
40
|
|
|
41
41
|
Take a look at the [DiscoveryController](app/controllers/doorkeeper/openid_connect/discovery_controller.rb) for more details on supported features.
|
|
42
42
|
|
|
@@ -72,7 +72,8 @@ rails generate doorkeeper:openid_connect:migration
|
|
|
72
72
|
rake db:migrate
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
If you're upgrading from an earlier version, check [
|
|
75
|
+
If you're upgrading from an earlier version, check [Migration from old versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration%E2%80%90from%E2%80%90old%E2%80%90versions)
|
|
76
|
+
wiki and [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
|
|
76
77
|
|
|
77
78
|
## Configuration
|
|
78
79
|
|
|
@@ -104,7 +105,19 @@ The following settings are required in `config/initializers/doorkeeper_openid_co
|
|
|
104
105
|
|
|
105
106
|
- `issuer`
|
|
106
107
|
- Identifier for the issuer of the response (i.e. your application URL). The value is a case sensitive URL using the `https` scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.
|
|
107
|
-
- You can either pass a string value, or a block to generate the issuer dynamically
|
|
108
|
+
- You can either pass a string value, or a block to generate the issuer dynamically. The block receives `resource_owner`, `application`, and `request` so that the same configuration can serve both ID token issuance (where `resource_owner` and `application` are available) and the discovery endpoint (where only `request` is available):
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# config/initializers/doorkeeper_openid_connect.rb
|
|
112
|
+
Doorkeeper::OpenidConnect.configure do
|
|
113
|
+
# ...
|
|
114
|
+
issuer do |resource_owner, application, request|
|
|
115
|
+
request&.base_url || "https://default.example.com"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- For backward compatibility, blocks with arity 0, 1, or 2 are also accepted. An arity-1 block receives `request` from the discovery endpoint and `resource_owner` from the ID token context, while an arity-2 block always receives `resource_owner` and `application`.
|
|
108
121
|
- `subject`
|
|
109
122
|
- Identifier for the resource owner (i.e. the authenticated user). A locally unique and never reassigned identifier within the issuer for the end-user, which is intended to be consumed by the client. The value is a case-sensitive string and must not exceed 255 ASCII characters in length.
|
|
110
123
|
- The database ID of the user is an acceptable choice if you don't mind leaking that information.
|
|
@@ -128,15 +141,45 @@ The following settings are required in `config/initializers/doorkeeper_openid_co
|
|
|
128
141
|
- You can generate a private key with the `openssl` command, see e.g. [Generate an RSA keypair using OpenSSL](https://en.wikibooks.org/wiki/Cryptography/Generate_a_keypair_using_OpenSSL).
|
|
129
142
|
- You should not commit the key to your repository, but use an external file (in combination with `File.read`) and/or the [dotenv-rails](https://github.com/bkeepers/dotenv) gem (in combination with `ENV[...]`).
|
|
130
143
|
- `signing_algorithm`
|
|
131
|
-
- The
|
|
144
|
+
- The signing algorithm used for the ID token, which defaults to `:rs256`. The list of supported algorithms can be found [here](https://github.com/jwt/ruby-jwt#algorithms-and-usage)
|
|
132
145
|
- `resource_owner_from_access_token`
|
|
133
146
|
- Defines how to translate the Doorkeeper access token to a resource owner model.
|
|
134
147
|
|
|
148
|
+
> [!Note]
|
|
149
|
+
> Both `signing_key` and `signing_algorithm` also accept callable objects (e.g. a lambda), which are evaluated on each call — useful for multi-tenant setups where the key or algorithm varies per request:
|
|
150
|
+
>
|
|
151
|
+
> ```ruby
|
|
152
|
+
> signing_key -> { current_tenant.private_key }
|
|
153
|
+
> signing_algorithm -> { current_tenant.algorithm }
|
|
154
|
+
> ```
|
|
155
|
+
|
|
156
|
+
> [!Note]
|
|
157
|
+
> `signing_key` also accepts an array for key rotation. The first entry is the active key used to sign newly issued ID tokens; the remaining entries are still published in the JWKS so clients can validate tokens signed with a retired key during a rotation window. Callable forms returning an array are also supported.
|
|
158
|
+
>
|
|
159
|
+
> ```ruby
|
|
160
|
+
> signing_key [
|
|
161
|
+
> File.read("config/keys/current.pem"), # active, signs new tokens
|
|
162
|
+
> File.read("config/keys/previous.pem"), # retired, exposed in JWKS only
|
|
163
|
+
> ]
|
|
164
|
+
> ```
|
|
165
|
+
|
|
135
166
|
The following settings are optional, but recommended for better client compatibility:
|
|
136
167
|
|
|
137
168
|
- `auth_time_from_resource_owner`
|
|
138
169
|
- Returns the time of the user's last login, this can be a `Time`, `DateTime`, or any other class that responds to `to_i`
|
|
139
|
-
-
|
|
170
|
+
- Used to populate the `auth_time` claim on the ID Token.
|
|
171
|
+
- Used as a fallback for `max_age` enforcement when `auth_time_from_session` is not configured. **Note:** for multi-session deployments this is insecure (it returns the most recent login on _any_ device), and emits a deprecation warning — prefer `auth_time_from_session` below.
|
|
172
|
+
- `auth_time_from_session`
|
|
173
|
+
- Returns the time the user authenticated for the *current* session. Required for correct `max_age` enforcement when the same user can hold multiple concurrent sessions (e.g. PC + smartphone) — `auth_time_from_resource_owner` cannot distinguish between sessions and would let a stale session inherit a fresh login from another device.
|
|
174
|
+
- The block is executed in the controller's scope and receives `(session, request)`. Return value can be a `Time`, `DateTime`, or anything responding to `to_i`. Return `nil` to force reauthentication.
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Example: capture auth_time on the session at login,
|
|
178
|
+
# and surface it here for the OIDC max_age check.
|
|
179
|
+
auth_time_from_session do |session, _request|
|
|
180
|
+
session[:auth_time]
|
|
181
|
+
end
|
|
182
|
+
```
|
|
140
183
|
- `reauthenticate_resource_owner`
|
|
141
184
|
- Defines how to trigger reauthentication for the current user (e.g. display a password prompt, or sign-out the user and redirect to the login form).
|
|
142
185
|
- Required to support the `max_age` and `prompt=login` parameters.
|
|
@@ -177,7 +220,7 @@ The following settings are optional:
|
|
|
177
220
|
- `discovery_url_options`
|
|
178
221
|
- The URL options for every available endpoint to use when generating the endpoint URL in the
|
|
179
222
|
discovery response. Available endpoints: `authorization`, `token`, `revocation`,
|
|
180
|
-
`introspection`, `userinfo`, `jwks`, `
|
|
223
|
+
`introspection`, `userinfo`, `jwks`, `dynamic_client_registration`.
|
|
181
224
|
- This option requires option keys with an available endpoint and
|
|
182
225
|
[URL options](https://api.rubyonrails.org/v6.0.3.3/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for)
|
|
183
226
|
as value.
|
|
@@ -203,6 +246,20 @@ The following settings are optional:
|
|
|
203
246
|
end
|
|
204
247
|
```
|
|
205
248
|
|
|
249
|
+
- `apply_prompt_to_non_oidc_requests`
|
|
250
|
+
- Whether to honor the `prompt` authorization parameter (`none`, `login`, `consent`, `select_account`) on plain OAuth requests that do not include the `openid` scope.
|
|
251
|
+
- Defaults to `false`, which preserves the historical behavior of silently ignoring `prompt` outside of OIDC requests.
|
|
252
|
+
- `max_age` enforcement remains OIDC-only regardless of this option, since it is defined by OIDC Core.
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# config/initializers/doorkeeper_openid_connect.rb
|
|
256
|
+
Doorkeeper::OpenidConnect.configure do
|
|
257
|
+
# ...
|
|
258
|
+
apply_prompt_to_non_oidc_requests true
|
|
259
|
+
# ...
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
206
263
|
### Scopes
|
|
207
264
|
|
|
208
265
|
To perform authentication over OpenID Connect, an OAuth client needs to request the `openid` scope. This scope needs to be enabled using either `optional_scopes` in the global Doorkeeper configuration in `config/initializers/doorkeeper.rb`, or by adding it to any OAuth application's `scope` attribute.
|
|
@@ -211,6 +268,24 @@ To perform authentication over OpenID Connect, an OAuth client needs to request
|
|
|
211
268
|
>
|
|
212
269
|
> See [Using Scopes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes) in the Doorkeeper wiki for more information.
|
|
213
270
|
|
|
271
|
+
#### `offline_access`
|
|
272
|
+
|
|
273
|
+
Per [OIDC Core §11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess), the `offline_access` scope signals that the client wants a refresh token so it can access the user's resources while the user is offline. Doorkeeper's existing `use_refresh_token` block already covers the basic flow — issue a refresh token only when the client actually asked for `offline_access`:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# config/initializers/doorkeeper.rb
|
|
277
|
+
Doorkeeper.configure do
|
|
278
|
+
optional_scopes :openid, :offline_access
|
|
279
|
+
|
|
280
|
+
# Issue a refresh token only when the client requests offline_access
|
|
281
|
+
use_refresh_token do |context|
|
|
282
|
+
context.scopes.exists?("offline_access")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
> **Note:** This does not automatically enforce [OIDC Core §11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess)'s strict requirements — for example, the OP MUST ignore `offline_access` unless `prompt=consent` is present and `response_type` returns an Authorization Code. If you need that level of enforcement, filter the scope in your `use_refresh_token` block or authorization controller override.
|
|
288
|
+
|
|
214
289
|
### Claims
|
|
215
290
|
|
|
216
291
|
Claims can be defined in a `claims` block inside `config/initializers/doorkeeper_openid_connect.rb`:
|
|
@@ -249,6 +324,48 @@ By default all custom claims are only returned from the `UserInfo` endpoint and
|
|
|
249
324
|
|
|
250
325
|
You can also pass a `scope:` keyword argument on each claim to specify which OAuth scope should be required to access the claim. If you define any of the defined [Standard Claims](http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) they will by default use their [corresponding scopes](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) (`profile`, `email`, `address` and `phone`), and any other claims will by default use the `profile` scope. Again, to use any of these scopes you need to enable them as described above.
|
|
251
326
|
|
|
327
|
+
You can also pass an array of scopes, in which case the claim is returned whenever the access token grants any of the listed scopes. This is useful when you want to expose the same claim under both a standard scope and an aggregate scope:
|
|
328
|
+
|
|
329
|
+
``` ruby
|
|
330
|
+
claim :given_name, scope: [:profile, :all_data] do |user|
|
|
331
|
+
user.first_name
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
claim :email, scope: [:email, :all_data] do |user|
|
|
335
|
+
user.email
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Authentication Context (`acr`) and Methods (`amr`)
|
|
340
|
+
|
|
341
|
+
The `claim` DSL also handles standard top-level ID Token claims such as [`acr`](http://openid.net/specs/openid-connect-core-1_0.html#IDToken) (Authentication Context Class Reference) and [`amr`](https://www.rfc-editor.org/rfc/rfc8176) (Authentication Methods References) — commonly used to expose MFA status to clients:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
claims do
|
|
345
|
+
claim :acr, response: [:id_token, :user_info], scope: :openid do |resource_owner|
|
|
346
|
+
# Single string — e.g. a URI like "urn:mace:incommon:iap:silver",
|
|
347
|
+
# or a numeric Level of Assurance "1".."4" per ISO/IEC 29115.
|
|
348
|
+
resource_owner.mfa_enabled? ? "2" : "1"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
claim :amr, response: [:id_token, :user_info], scope: :openid do |resource_owner|
|
|
352
|
+
# Array of strings, per RFC 8176.
|
|
353
|
+
methods = ["pwd"]
|
|
354
|
+
methods << "mfa" if resource_owner.mfa_enabled?
|
|
355
|
+
methods << "otp" if resource_owner.last_login_used_totp?
|
|
356
|
+
methods
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Two defaults are worth calling out because they bite silently:
|
|
362
|
+
|
|
363
|
+
- **`response: [:id_token, :user_info]`** — custom claims default to UserInfo only, but relying parties usually expect `acr` / `amr` on the ID Token.
|
|
364
|
+
- **`scope: :openid`** — without it, non-standard claims fall back to the `profile` scope and disappear for clients that only requested `openid`.
|
|
365
|
+
|
|
366
|
+
Claim names you declare here are automatically advertised under `claims_supported` in the discovery document. The list of advertised `acr` values (`acr_values_supported`) is not currently generated.
|
|
367
|
+
|
|
368
|
+
|
|
252
369
|
### Routes
|
|
253
370
|
|
|
254
371
|
The installation generator will update your `config/routes.rb` to define all required routes:
|
|
@@ -267,11 +384,36 @@ GET /oauth/userinfo
|
|
|
267
384
|
POST /oauth/userinfo
|
|
268
385
|
GET /oauth/discovery/keys
|
|
269
386
|
GET /.well-known/openid-configuration
|
|
387
|
+
GET /.well-known/oauth-authorization-server
|
|
270
388
|
GET /.well-known/webfinger
|
|
271
389
|
```
|
|
272
390
|
|
|
273
391
|
With the exception of the hard-coded `/.well-known` paths (see [RFC 5785](https://tools.ietf.org/html/rfc5785)) you can customize routes in the same way as with Doorkeeper, please refer to [this page on their wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes#version--05-1).
|
|
274
392
|
|
|
393
|
+
#### Customizing the `jwks_uri` path in the discovery document
|
|
394
|
+
|
|
395
|
+
[`discovery_url_options`](#configuration) lets you tweak the host, protocol, or port of the published `jwks_uri`, but not the path itself. To advertise a custom path — while keeping `/oauth/discovery/keys` working for existing clients during a rollover — mount the discovery controller at the new path and re-point the `oauth_discovery_keys_url` helper at it via [`direct`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/CustomUrlHelpers.html#method-i-direct):
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# config/routes.rb
|
|
399
|
+
Rails.application.routes.draw do
|
|
400
|
+
use_doorkeeper_openid_connect
|
|
401
|
+
|
|
402
|
+
# 1. Mount the custom path under a non-conflicting helper name
|
|
403
|
+
get "/-/jwks",
|
|
404
|
+
to: "doorkeeper/openid_connect/discovery#keys",
|
|
405
|
+
as: :custom_jwks
|
|
406
|
+
|
|
407
|
+
# 2. Re-point oauth_discovery_keys_url at the new path
|
|
408
|
+
direct(:oauth_discovery_keys) { |opts| custom_jwks_url(opts) }
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
After this, `.well-known/openid-configuration` returns `"jwks_uri": "https://example.com/-/jwks"`, and the original `/oauth/discovery/keys` route still responds (handy during a rollover).
|
|
413
|
+
|
|
414
|
+
> [!Note]
|
|
415
|
+
> A naive `match "/-/jwks", ..., as: :oauth_discovery_keys` won't work — Rails has refused to reuse a route name [since 4.0](https://github.com/rails/rails/commit/a2b7c0e69d) and raises `ArgumentError: Invalid route name, already in use: 'oauth_discovery_keys'`. The `direct` helper sidesteps this by overriding the URL helper itself rather than re-declaring the route name.
|
|
416
|
+
|
|
275
417
|
### Nonces
|
|
276
418
|
|
|
277
419
|
To support clients who send nonces you have to tweak Doorkeeper's authorization view so the parameter is passed on.
|
|
@@ -309,6 +451,67 @@ Then tweak the template as follows:
|
|
|
309
451
|
|
|
310
452
|
We use Rails locale files for error messages and scope descriptions, see [config/locales/en.yml](config/locales/en.yml). You can override these by adding them to your own translations in `config/locale`.
|
|
311
453
|
|
|
454
|
+
### Dynamic Client Registration
|
|
455
|
+
|
|
456
|
+
This gem supports [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) based on [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591).
|
|
457
|
+
|
|
458
|
+
To enable dynamic client registration, add the following to `config/initializers/doorkeeper_openid_connect.rb`:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
Doorkeeper::OpenidConnect.configure do
|
|
462
|
+
# ...
|
|
463
|
+
dynamic_client_registration true
|
|
464
|
+
# ...
|
|
465
|
+
end
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
This exposes a `POST /oauth/registration` endpoint where OAuth clients can register themselves.
|
|
469
|
+
|
|
470
|
+
#### Supported parameters
|
|
471
|
+
|
|
472
|
+
The registration endpoint currently accepts the following [RFC 7591 §2](https://www.rfc-editor.org/rfc/rfc7591#section-2) parameters:
|
|
473
|
+
|
|
474
|
+
| Parameter | Description |
|
|
475
|
+
| --- | --- |
|
|
476
|
+
| `client_name` | Human-readable name of the client |
|
|
477
|
+
| `redirect_uris` | Array of redirection URIs |
|
|
478
|
+
| `scope` | Space-delimited list of requested scopes |
|
|
479
|
+
| `token_endpoint_auth_method` | Requested authentication method. Defaults to `client_secret_basic`. `none` is always allowed (and registers a public client); other allowed values depend on the host application's Doorkeeper `client_credentials_methods` configuration. Unsupported values are rejected with `invalid_client_metadata`. |
|
|
480
|
+
| `application_type` | Client type: `web` (default) or `native`, per [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html). Unsupported values are rejected with `invalid_client_metadata`. |
|
|
481
|
+
| `response_types` | Array of OAuth 2.0 response types the client will use (e.g. `["code"]`). Must be a subset of the server's supported response types. Defaults to the server's full set when omitted. |
|
|
482
|
+
| `grant_types` | Array of OAuth 2.0 grant types the client will use (e.g. `["authorization_code"]`). Must be a subset of the server's supported grant types. Defaults to the server's full set when omitted. |
|
|
483
|
+
|
|
484
|
+
When `token_endpoint_auth_method` is set to `none`, the client is registered as **public** (i.e. `confidential: false`). For all other values — or when the parameter is omitted — the client is registered as **confidential**, matching the RFC 7591 default of `client_secret_basic`.
|
|
485
|
+
|
|
486
|
+
Other RFC 7591 parameters (e.g. `client_uri`, `logo_uri`, `contacts`) require schema additions to `oauth_applications` and are not yet supported.
|
|
487
|
+
|
|
488
|
+
#### Authorization
|
|
489
|
+
|
|
490
|
+
By default, the registration endpoint is open to any request. To require authorization (e.g. an Initial Access Token per [RFC 7591 §3.1](https://www.rfc-editor.org/rfc/rfc7591#section-3.1)), configure `authorize_dynamic_client_registration`:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
Doorkeeper::OpenidConnect.configure do
|
|
494
|
+
# ...
|
|
495
|
+
dynamic_client_registration true
|
|
496
|
+
authorize_dynamic_client_registration do
|
|
497
|
+
provided = request.headers["Authorization"].to_s
|
|
498
|
+
expected = "Bearer #{ENV['DCR_INITIAL_ACCESS_TOKEN']}"
|
|
499
|
+
# Use a constant-time comparison to avoid leaking the token via timing.
|
|
500
|
+
# Digesting first keeps the comparison fixed-length so the token's length
|
|
501
|
+
# isn't leaked either.
|
|
502
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
|
503
|
+
Digest::SHA256.hexdigest(provided),
|
|
504
|
+
Digest::SHA256.hexdigest(expected),
|
|
505
|
+
)
|
|
506
|
+
end
|
|
507
|
+
# ...
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
The block is evaluated in the controller scope (with access to `request`, `params`, `request.headers`, etc.). Return a truthy value to allow the request, or a falsy value to reject it with `401 invalid_token`.
|
|
512
|
+
|
|
513
|
+
When not configured (default), the endpoint remains open for backward compatibility.
|
|
514
|
+
|
|
312
515
|
## Development
|
|
313
516
|
|
|
314
517
|
Run `bundle install` to setup all development dependencies.
|
|
@@ -334,7 +537,7 @@ bundle exec rake server
|
|
|
334
537
|
By default, the latest Rails version is used. To use a specific version run:
|
|
335
538
|
|
|
336
539
|
```
|
|
337
|
-
rails=
|
|
540
|
+
rails=7.2 bundle update
|
|
338
541
|
```
|
|
339
542
|
|
|
340
543
|
## License
|
|
@@ -5,8 +5,9 @@ module Doorkeeper
|
|
|
5
5
|
class DiscoveryController < ::Doorkeeper::ApplicationMetalController
|
|
6
6
|
include Doorkeeper::Helpers::Controller
|
|
7
7
|
include GrantTypesSupportedMixin
|
|
8
|
+
include TokenEndpointAuthMethodsSupportedMixin
|
|
8
9
|
|
|
9
|
-
WEBFINGER_RELATION =
|
|
10
|
+
WEBFINGER_RELATION = "http://openid.net/specs/connect/1.0/issuer"
|
|
10
11
|
|
|
11
12
|
def provider
|
|
12
13
|
render json: provider_response
|
|
@@ -47,7 +48,7 @@ module Doorkeeper
|
|
|
47
48
|
# TODO: look into doorkeeper-jwt_assertion for these
|
|
48
49
|
# 'client_secret_jwt',
|
|
49
50
|
# 'private_key_jwt'
|
|
50
|
-
token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported
|
|
51
|
+
token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
|
|
51
52
|
|
|
52
53
|
subject_types_supported: openid_connect.subject_types_supported,
|
|
53
54
|
|
|
@@ -56,7 +57,7 @@ module Doorkeeper
|
|
|
56
57
|
],
|
|
57
58
|
|
|
58
59
|
claim_types_supported: [
|
|
59
|
-
|
|
60
|
+
"normal",
|
|
60
61
|
|
|
61
62
|
# TODO: support these
|
|
62
63
|
# 'aggregated',
|
|
@@ -79,11 +80,6 @@ module Doorkeeper
|
|
|
79
80
|
doorkeeper.authorization_response_flows.flat_map(&:response_mode_matches).uniq
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
def token_endpoint_auth_methods_supported(doorkeeper)
|
|
83
|
-
mapping = { from_basic: 'client_secret_basic', from_params: 'client_secret_post' }
|
|
84
|
-
doorkeeper.client_credentials_methods.filter_map { |method| mapping[method] }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
83
|
def code_challenge_methods_supported(doorkeeper)
|
|
88
84
|
return unless doorkeeper.access_grant_model.pkce_supported?
|
|
89
85
|
|
|
@@ -96,27 +92,19 @@ module Doorkeeper
|
|
|
96
92
|
links: [
|
|
97
93
|
{
|
|
98
94
|
rel: WEBFINGER_RELATION,
|
|
99
|
-
href:
|
|
95
|
+
href: issuer,
|
|
100
96
|
}
|
|
101
97
|
]
|
|
102
98
|
}
|
|
103
99
|
end
|
|
104
100
|
|
|
105
101
|
def keys_response
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
keys: [
|
|
110
|
-
signing_key.merge(
|
|
111
|
-
use: 'sig',
|
|
112
|
-
alg: Doorkeeper::OpenidConnect.signing_algorithm
|
|
113
|
-
)
|
|
114
|
-
]
|
|
115
|
-
}
|
|
102
|
+
{ keys: Doorkeeper::OpenidConnect.signing_keys_normalized }
|
|
116
103
|
end
|
|
117
104
|
|
|
118
105
|
def protocol
|
|
119
|
-
Doorkeeper::OpenidConnect.configuration.protocol
|
|
106
|
+
configured = Doorkeeper::OpenidConnect.configuration.protocol
|
|
107
|
+
configured.respond_to?(:call) ? configured.call : configured
|
|
120
108
|
end
|
|
121
109
|
|
|
122
110
|
def discovery_url_options
|
|
@@ -130,14 +118,11 @@ module Doorkeeper
|
|
|
130
118
|
end
|
|
131
119
|
|
|
132
120
|
def issuer
|
|
133
|
-
|
|
134
|
-
Doorkeeper::OpenidConnect.configuration.issuer.call(request).to_s
|
|
135
|
-
else
|
|
136
|
-
Doorkeeper::OpenidConnect.configuration.issuer
|
|
137
|
-
end
|
|
121
|
+
Doorkeeper::OpenidConnect.resolve_issuer(request: request)
|
|
138
122
|
end
|
|
139
123
|
|
|
140
|
-
%i[authorization token revocation introspection userinfo jwks
|
|
124
|
+
%i[authorization token revocation introspection userinfo jwks
|
|
125
|
+
dynamic_client_registration].each do |endpoint|
|
|
141
126
|
define_method :"#{endpoint}_url_options" do
|
|
142
127
|
discovery_url_default_options.merge(discovery_url_options[endpoint.to_sym] || {})
|
|
143
128
|
end
|
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
module OpenidConnect
|
|
5
5
|
class DynamicClientRegistrationController < ::Doorkeeper::ApplicationMetalController
|
|
6
|
-
|
|
6
|
+
before_action :authorize_dynamic_client_registration!
|
|
7
7
|
|
|
8
8
|
def register
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
registration = OAuth::DynamicRegistrationRequest.new(::Doorkeeper.configuration, params)
|
|
10
|
+
|
|
11
|
+
unless registration.valid?
|
|
12
|
+
render json: registration.error_response, status: :bad_request
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
client = Doorkeeper::Application.create!(application_params(registration))
|
|
17
|
+
render json: registration_response(client, registration), status: :created
|
|
11
18
|
rescue ActiveRecord::RecordInvalid => e
|
|
12
19
|
render json: { error: "invalid_client_params", error_description: e.record.errors.full_messages.join(", ") },
|
|
13
20
|
status: :bad_request
|
|
@@ -15,29 +22,59 @@ module Doorkeeper
|
|
|
15
22
|
|
|
16
23
|
private
|
|
17
24
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def authorize_dynamic_client_registration!
|
|
26
|
+
authorizer = ::Doorkeeper::OpenidConnect.configuration.authorize_dynamic_client_registration
|
|
27
|
+
return if authorizer.nil?
|
|
28
|
+
|
|
29
|
+
return if authorized?(authorizer)
|
|
30
|
+
|
|
31
|
+
response.headers["WWW-Authenticate"] = "Bearer error=\"invalid_token\""
|
|
32
|
+
render json: {
|
|
33
|
+
error: "invalid_token",
|
|
34
|
+
error_description: I18n.t(
|
|
35
|
+
"doorkeeper.openid_connect.errors.messages.dynamic_client_registration_unauthorized",
|
|
36
|
+
),
|
|
37
|
+
}, status: :unauthorized
|
|
25
38
|
end
|
|
26
39
|
|
|
27
|
-
def
|
|
28
|
-
|
|
40
|
+
def authorized?(authorizer)
|
|
41
|
+
if authorizer.respond_to?(:to_proc)
|
|
42
|
+
instance_exec(&authorizer.to_proc)
|
|
43
|
+
elsif authorizer.respond_to?(:call)
|
|
44
|
+
authorizer.call(self)
|
|
45
|
+
else
|
|
46
|
+
authorizer
|
|
47
|
+
end
|
|
48
|
+
end
|
|
29
49
|
|
|
50
|
+
def application_params(registration)
|
|
30
51
|
{
|
|
31
|
-
|
|
52
|
+
name: params[:client_name],
|
|
53
|
+
redirect_uri: params[:redirect_uris] || [],
|
|
54
|
+
scopes: params[:scope],
|
|
55
|
+
confidential: registration.confidential_client?,
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def registration_response(doorkeeper_application, registration)
|
|
60
|
+
response = {
|
|
32
61
|
client_id: doorkeeper_application.uid,
|
|
33
62
|
client_id_issued_at: doorkeeper_application.created_at.to_i,
|
|
34
63
|
redirect_uris: doorkeeper_application.redirect_uri.split,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
token_endpoint_auth_method: registration.token_endpoint_auth_method,
|
|
65
|
+
token_endpoint_auth_methods_supported: registration.token_endpoint_auth_methods_supported,
|
|
66
|
+
response_types: registration.requested_response_types,
|
|
67
|
+
grant_types: registration.requested_grant_types,
|
|
38
68
|
scope: doorkeeper_application.scopes.to_s,
|
|
39
|
-
application_type:
|
|
69
|
+
application_type: registration.requested_application_type,
|
|
40
70
|
}
|
|
71
|
+
|
|
72
|
+
if registration.confidential_client?
|
|
73
|
+
response[:client_secret] =
|
|
74
|
+
doorkeeper_application.plaintext_secret || doorkeeper_application.secret
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
response
|
|
41
78
|
end
|
|
42
79
|
end
|
|
43
80
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -21,3 +21,5 @@ en:
|
|
|
21
21
|
reauthenticate_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.reauthenticate_resource_owner missing configuration.'
|
|
22
22
|
select_account_for_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.select_account_for_resource_owner missing configuration.'
|
|
23
23
|
subject_not_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.'
|
|
24
|
+
signing_key_not_configured: 'Doorkeeper::OpenidConnect.configure.signing_key must resolve to at least one key.'
|
|
25
|
+
dynamic_client_registration_unauthorized: 'Authorization required for client registration'
|
|
@@ -8,6 +8,7 @@ module Doorkeeper
|
|
|
8
8
|
attr_accessor :pre_auth, :auth, :id_token
|
|
9
9
|
|
|
10
10
|
def initialize(pre_auth, auth, id_token)
|
|
11
|
+
super()
|
|
11
12
|
@pre_auth = pre_auth
|
|
12
13
|
@auth = auth
|
|
13
14
|
@id_token = id_token
|
|
@@ -19,7 +20,6 @@ module Doorkeeper
|
|
|
19
20
|
|
|
20
21
|
def body
|
|
21
22
|
{
|
|
22
|
-
expires_in: auth.token.expires_in_seconds,
|
|
23
23
|
state: pre_auth.state,
|
|
24
24
|
id_token: id_token.as_jws_token
|
|
25
25
|
}
|