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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +212 -9
  4. data/app/controllers/concerns/doorkeeper/openid_connect/authorizations_extension.rb +2 -1
  5. data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +11 -26
  6. data/app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb +54 -17
  7. data/config/locales/en.yml +2 -0
  8. data/lib/doorkeeper/oauth/id_token_response.rb +1 -1
  9. data/lib/doorkeeper/oauth/id_token_token_response.rb +2 -1
  10. data/lib/doorkeeper/openid_connect/claims/claim.rb +21 -9
  11. data/lib/doorkeeper/openid_connect/claims_builder.rb +3 -2
  12. data/lib/doorkeeper/openid_connect/config.rb +20 -9
  13. data/lib/doorkeeper/openid_connect/engine.rb +1 -1
  14. data/lib/doorkeeper/openid_connect/errors.rb +3 -1
  15. data/{app/controllers/concerns → lib}/doorkeeper/openid_connect/grant_types_supported_mixin.rb +1 -1
  16. data/lib/doorkeeper/openid_connect/helpers/controller.rb +160 -31
  17. data/lib/doorkeeper/openid_connect/id_token.rb +15 -11
  18. data/lib/doorkeeper/openid_connect/id_token_token.rb +11 -4
  19. data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +5 -5
  20. data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +108 -0
  21. data/lib/doorkeeper/openid_connect/oauth/password_access_token_request.rb +5 -3
  22. data/lib/doorkeeper/openid_connect/oauth/pre_authorization.rb +2 -2
  23. data/lib/doorkeeper/openid_connect/oauth/token_response.rb +1 -1
  24. data/lib/doorkeeper/openid_connect/orm/active_record/access_grant.rb +1 -1
  25. data/lib/doorkeeper/openid_connect/orm/active_record/mixins/openid_request.rb +2 -2
  26. data/lib/doorkeeper/openid_connect/orm/active_record/request.rb +1 -1
  27. data/lib/doorkeeper/openid_connect/orm/active_record.rb +22 -14
  28. data/lib/doorkeeper/openid_connect/rails/routes/mapping.rb +3 -3
  29. data/lib/doorkeeper/openid_connect/rails/routes.rb +11 -11
  30. data/lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb +18 -0
  31. data/lib/doorkeeper/openid_connect/user_info.rb +6 -3
  32. data/lib/doorkeeper/openid_connect/version.rb +1 -1
  33. data/lib/doorkeeper/openid_connect.rb +128 -48
  34. data/lib/doorkeeper/request/id_token.rb +1 -1
  35. data/lib/doorkeeper/request/id_token_token.rb +1 -1
  36. data/lib/generators/doorkeeper/openid_connect/install_generator.rb +6 -5
  37. data/lib/generators/doorkeeper/openid_connect/migration_generator.rb +8 -8
  38. data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +36 -2
  39. metadata +17 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b43afb85064c2b12a0837aec166ab720b352813fec7004dfce38cfc0b4338fc6
4
- data.tar.gz: de24da5d37377dadf9cb2bffcdf15c2b725075327efe301e0a1c907748ced3b8
3
+ metadata.gz: 1b09f0d036559583b3db8dd63e6e35452e5f9c42c90ce098ab263c1817016994
4
+ data.tar.gz: f9c9584568d90c9d351a347d435ba471d8e798aa42c9ac28edebefcd59d33c1a
5
5
  SHA512:
6
- metadata.gz: 0e9c242e0e9dbf48500810be186040b6cdc4afdb564196453a005ae420b9f61e6b104afcfeb1e71be2f9d1356ccca8ec1265a1e8dfdd12a0e796a9d8d19d532e
7
- data.tar.gz: b4832c67b7f870e9fa5b7745c5e1ddfd0ded602b0ca2f2cb9a351d4542f88b6e95b230c91cd24332ac6c6e6109035e589062db9c8a03faedf991a44a6ab7ddea
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
  [![Maintainability](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-openid_connect/maintainability.svg)](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-openid_connect)
5
5
  [![Gem Version](https://badge.fury.io/rb/doorkeeper-openid_connect.svg)](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 [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
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 based on the `resource_owner` and `application` or [request](app/controllers/doorkeeper/openid_connect/discovery_controller.rb#L123) passed to the block.
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 encryption type of the private key which defaults to `:rs256`. The list of supported algorithms can be found [here](https://github.com/nov/json-jwt/wiki/JWE#supported-algorithms)
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
- - Required to support the `max_age` parameter and the `auth_time` claim.
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`, `webfinger`.
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=4.2.0 bundle update
540
+ rails=7.2 bundle update
338
541
  ```
339
542
 
340
543
  ## License
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module OpenidConnect
3
5
  module AuthorizationsExtension
@@ -9,4 +11,3 @@ module Doorkeeper
9
11
  end
10
12
  end
11
13
  end
12
-
@@ -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 = 'http://openid.net/specs/connect/1.0/issuer'
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(doorkeeper),
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
- 'normal',
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: root_url(webfinger_url_options),
95
+ href: issuer,
100
96
  }
101
97
  ]
102
98
  }
103
99
  end
104
100
 
105
101
  def keys_response
106
- signing_key = Doorkeeper::OpenidConnect.signing_key_normalized
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.call
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
- if Doorkeeper::OpenidConnect.configuration.issuer.respond_to?(:call)
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 webfinger dynamic_client_registration].each do |endpoint|
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
- include GrantTypesSupportedMixin
6
+ before_action :authorize_dynamic_client_registration!
7
7
 
8
8
  def register
9
- client = Doorkeeper::Application.create!(application_params)
10
- render json: registration_response(client), status: :created
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 application_params
19
- {
20
- name: params.dig(:client_name),
21
- redirect_uri: params.dig(:redirect_uris) || [],
22
- scopes: params.dig(:scope),
23
- confidential: false,
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 registration_response(doorkeeper_application)
28
- doorkeeper_config = ::Doorkeeper.configuration
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
- client_secret: doorkeeper_application.plaintext_secret || doorkeeper_application.secret,
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
- token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
36
- response_types: doorkeeper_config.authorization_response_types,
37
- grant_types: grant_types_supported(doorkeeper_config),
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: "web"
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
@@ -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
  }
@@ -6,7 +6,8 @@ module Doorkeeper
6
6
  def body
7
7
  super.merge({
8
8
  access_token: auth.token.token,
9
- token_type: auth.token.token_type
9
+ token_type: auth.token.token_type,
10
+ expires_in: auth.token.expires_in_seconds
10
11
  })
11
12
  end
12
13
  end