doorkeeper-openid_connect 1.10.0 → 1.10.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b09f0d036559583b3db8dd63e6e35452e5f9c42c90ce098ab263c1817016994
4
- data.tar.gz: f9c9584568d90c9d351a347d435ba471d8e798aa42c9ac28edebefcd59d33c1a
3
+ metadata.gz: a7213362be6e1d40b9d790e35b6b860d6cbf80691282732e1257a4a01998ee39
4
+ data.tar.gz: 0f0830786b8f43e64ee75c69c4ffadcfae0e1af2d5d4ace18214879740dbb7c3
5
5
  SHA512:
6
- metadata.gz: a5a7f986bd19adcc2f3b4a03467d6052e32722f06162446b5ba944a8b905a111aa27528ccfc827e493382e95ab9dc89834a4ee483895951883291bda1387db81
7
- data.tar.gz: c9fc3444e391e9b971bd193b18f388dafa47c5f5d6188d055fad2f524f9c8d6ceb67929a6b2ab5179ab50ff3c854e8d4883c9f6a43b9cd650cceba97a2e4dee2
6
+ metadata.gz: 626d292fa7351472982a54e48001973c7d9202bf146ce6dbd5f2577def44ee22d3f753117db18ae175134e727caaca64a4ab05b5b87f38e821254af13e93416b
7
+ data.tar.gz: d48ae4ce304b01e15f45200b9c3bf47e987decf85bd6c7270c87f56bb3f463617ac5de287b9b243d2538ff3c92f2b84d03771df81b5edea50360694029afa5dc
data/CHANGELOG.md CHANGED
@@ -2,8 +2,34 @@
2
2
 
3
3
  - Please add here
4
4
 
5
+ ## v1.10.2 (2026-06-22)
6
+
7
+ - [#315] Drop support for EOL Ruby 3.1 (EOL 2025-03-25) and require Ruby `>= 3.2`. `i18n 1.15.0` uses the `Fiber[]` storage API which only exists on Ruby 3.2+, so the Ruby 3.1 CI row no longer loads; the matrix now tests Ruby 3.2 as the minimum
8
+ - [#316] Set `fail-fast: false` in CI matrix so a single failing job no longer cancels the rest
9
+ - [#303] execute account selection even without owner, and `select_account_for_resource_owner` can now receive `nil` as the first argument.
10
+ - [#304] allow handle auth_time per grant
11
+ - [#305] Document the `auth_time_from_access_token` config option in the README (per-grant `auth_time`), clarifying that it only affects the ID Token `auth_time` claim and not `max_age` enforcement
12
+ - [#307] Fix `bundle exec rake server` for the test application
13
+ - [#313] Move Configuration documentation from README to Wiki
14
+ - [#312] Raise `Errors::MissingRequiredClaim` instead of silently dropping a blank REQUIRED ID Token claim (`iss`/`sub`/`aud`/`exp`/`iat`) in `IdToken#as_json`, which previously could emit a non-conformant ID Token (OIDC Core 1.0 §2). OPTIONAL claims such as `nonce`/`auth_time` are still omitted when blank
15
+ - [#311] Include the REQUIRED `client_secret_expires_at` member (value `0`, never expires) in the Dynamic Client Registration response whenever a `client_secret` is issued (RFC 7591 §3.2.1 / OpenID Connect Dynamic Client Registration 1.0 §3.2)
16
+ - [#309] Add a browser dashboard to the test application (`spec/dummy`) for exercising the OpenID Connect endpoints by hand — replacing the rails console + curl workflow with forms for Setup, Discovery, Authorization (code / implicit / PKCE / nonce / prompt / `max_age`), token exchange, UserInfo, introspection and revocation
17
+
18
+ ## v1.10.1 (2026-06-03)
19
+
20
+ - [#294] Drop stale `Metrics/ClassLength` and `Metrics/BlockLength` overrides from `.rubocop_todo.yml`
21
+ - [#293] Drop `Naming/VariableNumber` from `.rubocop_todo.yml` and normalise test variable names
22
+ - [#291] Document multi-namespace mount pattern for multiple resource owner models ([#192](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/192))
23
+ - [#292] Drop formatting cops from `.rubocop_todo.yml` and align trailing-comma style with upstream doorkeeper
24
+ - [#296] Fix the `prompt` parameter being rejected with `invalid_request` when it contains leading or duplicate spaces (e.g. `prompt=%20none`) — blank entries in the space-delimited value are now ignored
25
+ - [#299] Raise `InvalidConfiguration` when the `issuer` config resolves to a blank value instead of silently advertising an empty `issuer` in the discovery document. Since v1.10.0 an arity-2 `issuer` block receives `(resource_owner, application)` — both `nil` in the discovery context — so a block relying on the old v1.9.0 request argument could return `nil` and produce a discovery `issuer` that mismatched the ID token `iss` ([#298](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/298))
26
+
5
27
  ## v1.10.0 (2026-06-01)
6
28
 
29
+ >[!IMPORTANT]
30
+ >
31
+ >- **Breaking (arity-2 issuer blocks):** `resolve_issuer` now dispatches arity-2 blocks with `(resource_owner, application)` in all contexts, including discovery. In v1.9.0 `DiscoveryController` passed `request` as the first argument; existing arity-2 blocks that relied on this receive `(nil, nil)` in v1.10.0 and should migrate to arity-3 — see [#298](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/298) for details and migration examples
32
+
7
33
  - [#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
34
  - [#242] Fix `NoMethodError` for openid_request in testing environments.
9
35
  - [#246] Fix `at_hash` to use correct hash algorithm based on `signing_algorithm`
data/README.md CHANGED
@@ -14,13 +14,7 @@ OpenID Connect is a single-sign-on and identity layer with a [growing list of se
14
14
  - [Known Issues](#known-issues)
15
15
  - [Example Applications](#example-applications)
16
16
  - [Installation](#installation)
17
- - [Configuration](#configuration)
18
- - [Scopes](#scopes)
19
- - [Claims](#claims)
20
- - [Routes](#routes)
21
- - [Nonces](#nonces)
22
- - [Internationalization (I18n)](#internationalization-i18n)
23
- - [Dynamic Client Registration](#dynamic-client-registration)
17
+ - [Configuration](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration)
24
18
  - [Development](#development)
25
19
  - [License](#license)
26
20
  - [Sponsors](#sponsors)
@@ -77,440 +71,14 @@ wiki and [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
77
71
 
78
72
  ## Configuration
79
73
 
80
- Make sure you've [configured Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper#configuration) before continuing.
81
-
82
- Verify your settings in `config/initializers/doorkeeper.rb`:
83
-
84
- - `resource_owner_authenticator`
85
- - This callback needs to returns a falsey value if the current user can't be determined:
86
-
87
- ```ruby
88
- resource_owner_authenticator do
89
- if current_user
90
- current_user
91
- else
92
- redirect_to(new_user_session_url)
93
- nil
94
- end
95
- end
96
- ```
97
- - `grant_flows`
98
- - If you want to use `id_token` or `id_token token` response types you need to add `implicit_oidc` to `grant_flows`:
99
-
100
- ```ruby
101
- grant_flows %w(authorization_code implicit_oidc)
102
- ```
103
-
104
- The following settings are required in `config/initializers/doorkeeper_openid_connect.rb`:
105
-
106
- - `issuer`
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.
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`.
121
- - `subject`
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.
123
- - The database ID of the user is an acceptable choice if you don't mind leaking that information.
124
- - If you want to provide a different subject identifier to each client, use [pairwise subject identifier](http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) with configurations like below.
125
-
126
- ```ruby
127
- # config/initializers/doorkeeper_openid_connect.rb
128
- Doorkeeper::OpenidConnect.configure do
129
- # ...
130
- subject_types_supported [:pairwise]
131
-
132
- subject do |resource_owner, application|
133
- Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}")
134
- end
135
- # ...
136
- end
137
- ```
138
-
139
- - `signing_key`
140
- - Private key to be used for [JSON Web Signature](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31).
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).
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[...]`).
143
- - `signing_algorithm`
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)
145
- - `resource_owner_from_access_token`
146
- - Defines how to translate the Doorkeeper access token to a resource owner model.
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
-
166
- The following settings are optional, but recommended for better client compatibility:
167
-
168
- - `auth_time_from_resource_owner`
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`
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
- ```
183
- - `reauthenticate_resource_owner`
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).
185
- - Required to support the `max_age` and `prompt=login` parameters.
186
- - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.
187
- - `select_account_for_resource_owner`
188
- - Defines how to trigger account selection to choose the current login user.
189
- - Required to support the `prompt=select_account` parameter.
190
- - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.
191
-
192
- The following settings are optional:
193
-
194
- - `expiration`
195
- - Expiration time after which the ID Token must not be accepted for processing by clients.
196
- - The default is 120 seconds, it can be configured using a value or block.
197
- ```ruby
198
- # config/initializers/doorkeeper_openid_connect.rb
199
- Doorkeeper::OpenidConnect.configure do
200
- # ...
201
- expiration do |resource_owner, application|
202
- # You will have to ensure the application model implements an expiration method
203
- application.expiration
204
- end
205
- # ...
206
- end
207
- ```
208
-
209
- - `protocol`
210
- - The protocol to use when generating URIs for the discovery endpoints.
211
- - The default is `https` for production, and `http` for all other environments
212
- - Note that the OIDC specification mandates HTTPS, so you shouldn't change this
213
- for production environments unless you have a really good reason!
214
-
215
- - `end_session_endpoint`
216
- - The URL that the user is redirected to after ending the session on the client.
217
- - Used by implementations like https://github.com/IdentityModel/oidc-client-js.
218
- - The block is executed in the controller's scope, so you have access to your route helpers.
219
-
220
- - `discovery_url_options`
221
- - The URL options for every available endpoint to use when generating the endpoint URL in the
222
- discovery response. Available endpoints: `authorization`, `token`, `revocation`,
223
- `introspection`, `userinfo`, `jwks`, `dynamic_client_registration`.
224
- - This option requires option keys with an available endpoint and
225
- [URL options](https://api.rubyonrails.org/v6.0.3.3/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for)
226
- as value.
227
- - The default is to use the request host, just like all the other URLs in the discovery response.
228
- - This is useful when you want endpoints to use a different URL than other requests.
229
- For example, if your Doorkeeper server is behind a firewall with other servers, you might want
230
- other servers to use an "internal" URL to communicate with Doorkeeper, but you want to present
231
- an "external" URL to end-users for authentication requests. Note that this setting does not
232
- actually change the URL that your Doorkeeper server responds on - that is outside the scope of
233
- Doorkeeper.
234
-
235
- ```ruby
236
- # config/initializers/doorkeeper_openid_connect.rb
237
- Doorkeeper::OpenidConnect.configure do
238
- # ...
239
- discovery_url_options do |request|
240
- {
241
- authorization: { host: 'host.example.com' },
242
- jwks: { protocol: request.ssl? ? :https : :http }
243
- }
244
- end
245
- # ...
246
- end
247
- ```
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
-
263
- ### Scopes
264
-
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.
266
-
267
- > Note that any application defining its own scopes won't inherit the scopes defined in the initializer, so you might have to update existing applications as well.
268
- >
269
- > See [Using Scopes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes) in the Doorkeeper wiki for more information.
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`:
74
+ See the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration) for detailed configuration instructions, including:
274
75
 
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
-
289
- ### Claims
290
-
291
- Claims can be defined in a `claims` block inside `config/initializers/doorkeeper_openid_connect.rb`:
292
-
293
- ```ruby
294
- Doorkeeper::OpenidConnect.configure do
295
- claims do
296
- claim :email do |resource_owner|
297
- resource_owner.email
298
- end
299
-
300
- claim :full_name do |resource_owner|
301
- "#{resource_owner.first_name} #{resource_owner.last_name}"
302
- end
303
-
304
- claim :preferred_username, scope: :openid do |resource_owner, scopes, access_token|
305
- # Pass the resource_owner's preferred_username if the application has
306
- # `profile` scope access. Otherwise, provide a more generic alternative.
307
- scopes.exists?(:profile) ? resource_owner.preferred_username : "summer-sun-9449"
308
- end
309
-
310
- claim :groups, response: [:id_token, :user_info] do |resource_owner|
311
- resource_owner.groups
312
- end
313
- end
314
- end
315
- ```
316
-
317
- Each claim block will be passed:
318
-
319
- - the `resource_owner`, which is the return value of `resource_owner_authenticator` in your initializer
320
- - the `scopes` granted by the access token, which is an instance of `Doorkeeper::OAuth::Scopes`
321
- - the `access_token` itself, which is an instance of `Doorkeeper::AccessToken`
322
-
323
- By default all custom claims are only returned from the `UserInfo` endpoint and not included in the ID token. You can optionally pass a `response:` keyword with one or both of the symbols `:id_token` or `:user_info` to specify where the claim should be returned.
324
-
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.
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
-
369
- ### Routes
370
-
371
- The installation generator will update your `config/routes.rb` to define all required routes:
372
-
373
- ``` ruby
374
- Rails.application.routes.draw do
375
- use_doorkeeper_openid_connect
376
- # your routes
377
- end
378
- ```
379
-
380
- This will mount the following routes:
381
-
382
- ```
383
- GET /oauth/userinfo
384
- POST /oauth/userinfo
385
- GET /oauth/discovery/keys
386
- GET /.well-known/openid-configuration
387
- GET /.well-known/oauth-authorization-server
388
- GET /.well-known/webfinger
389
- ```
390
-
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).
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
-
417
- ### Nonces
418
-
419
- To support clients who send nonces you have to tweak Doorkeeper's authorization view so the parameter is passed on.
420
-
421
- If you don't already have custom templates, run this generator in your Rails application to add them:
422
-
423
- ```sh
424
- rails generate doorkeeper:views
425
- ```
426
-
427
- Then tweak the template as follows:
428
-
429
- ```patch
430
- --- i/app/views/doorkeeper/authorizations/new.html.erb
431
- +++ w/app/views/doorkeeper/authorizations/new.html.erb
432
- @@ -26,6 +26,7 @@
433
- <%= hidden_field_tag :state, @pre_auth.state %>
434
- <%= hidden_field_tag :response_type, @pre_auth.response_type %>
435
- <%= hidden_field_tag :scope, @pre_auth.scope %>
436
- + <%= hidden_field_tag :nonce, @pre_auth.nonce %>
437
- <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
438
- <% end %>
439
- <%= form_tag oauth_authorization_path, method: :delete do %>
440
- @@ -34,6 +35,7 @@
441
- <%= hidden_field_tag :state, @pre_auth.state %>
442
- <%= hidden_field_tag :response_type, @pre_auth.response_type %>
443
- <%= hidden_field_tag :scope, @pre_auth.scope %>
444
- + <%= hidden_field_tag :nonce, @pre_auth.nonce %>
445
- <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
446
- <% end %>
447
- </div>
448
- ```
449
-
450
- ### Internationalization (I18n)
451
-
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`.
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.
76
+ - [Scopes](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Scopes)
77
+ - [Claims](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Claims)
78
+ - [Routes](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Routes)
79
+ - [Nonces](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Nonces)
80
+ - [Internationalization (I18n)](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/I18n)
81
+ - [Dynamic Client Registration](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Dynamic-Client-Registration)
514
82
 
515
83
  ## Development
516
84
 
@@ -53,7 +53,7 @@ module Doorkeeper
53
53
  subject_types_supported: openid_connect.subject_types_supported,
54
54
 
55
55
  id_token_signing_alg_values_supported: [
56
- ::Doorkeeper::OpenidConnect.signing_algorithm
56
+ ::Doorkeeper::OpenidConnect.signing_algorithm,
57
57
  ],
58
58
 
59
59
  claim_types_supported: [
@@ -93,8 +93,8 @@ module Doorkeeper
93
93
  {
94
94
  rel: WEBFINGER_RELATION,
95
95
  href: issuer,
96
- }
97
- ]
96
+ },
97
+ ],
98
98
  }
99
99
  end
100
100
 
@@ -113,7 +113,7 @@ module Doorkeeper
113
113
 
114
114
  def discovery_url_default_options
115
115
  {
116
- protocol: protocol
116
+ protocol: protocol,
117
117
  }
118
118
  end
119
119
 
@@ -17,7 +17,7 @@ module Doorkeeper
17
17
  render json: registration_response(client, registration), status: :created
18
18
  rescue ActiveRecord::RecordInvalid => e
19
19
  render json: { error: "invalid_client_params", error_description: e.record.errors.full_messages.join(", ") },
20
- status: :bad_request
20
+ status: :bad_request
21
21
  end
22
22
 
23
23
  private
@@ -72,6 +72,10 @@ module Doorkeeper
72
72
  if registration.confidential_client?
73
73
  response[:client_secret] =
74
74
  doorkeeper_application.plaintext_secret || doorkeeper_application.secret
75
+ # RFC 7591 §3.2.1 / OIDC Dynamic Client Registration 1.0 §3.2:
76
+ # client_secret_expires_at is REQUIRED when a client_secret is issued.
77
+ # Doorkeeper secrets never expire, so the value is 0 (no expiration).
78
+ response[:client_secret_expires_at] = 0
75
79
  end
76
80
 
77
81
  response
@@ -22,4 +22,7 @@ en:
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
24
  signing_key_not_configured: 'Doorkeeper::OpenidConnect.configure.signing_key must resolve to at least one key.'
25
+ issuer_not_configured: 'Doorkeeper::OpenidConnect.configure.issuer must resolve to a non-blank value.'
25
26
  dynamic_client_registration_unauthorized: 'Authorization required for client registration'
27
+ # ID Token claim error messages
28
+ missing_required_claim: 'Required ID Token claim `%{claim}` is missing or blank'
@@ -21,7 +21,7 @@ module Doorkeeper
21
21
  def body
22
22
  {
23
23
  state: pre_auth.state,
24
- id_token: id_token.as_jws_token
24
+ id_token: id_token.as_jws_token,
25
25
  }
26
26
  end
27
27
 
@@ -7,7 +7,7 @@ module Doorkeeper
7
7
  super.merge({
8
8
  access_token: auth.token.token,
9
9
  token_type: auth.token.token_type,
10
- expires_in: auth.token.expires_in_seconds
10
+ expires_in: auth.token.expires_in_seconds,
11
11
  })
12
12
  end
13
13
  end
@@ -31,7 +31,7 @@ module Doorkeeper
31
31
  name: name,
32
32
  response: response,
33
33
  scope: scope,
34
- generator: block
34
+ generator: block,
35
35
  )
36
36
  end
37
37
  alias claim normal_claim
@@ -5,7 +5,7 @@ module Doorkeeper
5
5
  def self.configure(&block)
6
6
  if Doorkeeper.configuration.orm != :active_record
7
7
  raise Errors::InvalidConfiguration,
8
- "Doorkeeper OpenID Connect currently only supports the ActiveRecord ORM adapter"
8
+ "Doorkeeper OpenID Connect currently only supports the ActiveRecord ORM adapter"
9
9
  end
10
10
 
11
11
  @config = Config::Builder.new(&block).build
@@ -54,6 +54,7 @@ module Doorkeeper
54
54
  }
55
55
 
56
56
  option :auth_time_from_session, default: nil
57
+ option :auth_time_from_access_token, default: nil
57
58
 
58
59
  option :reauthenticate_resource_owner, default: lambda { |*_|
59
60
  raise Errors::InvalidConfiguration, I18n.translate("doorkeeper.openid_connect.errors.messages.reauthenticate_resource_owner_not_configured")
@@ -12,6 +12,18 @@ module Doorkeeper
12
12
  # internal errors
13
13
  class InvalidConfiguration < OpenidConnectError; end
14
14
 
15
+ # Raised when a REQUIRED ID Token claim (OIDC Core §2: iss/sub/aud/exp/iat)
16
+ # resolves to a blank value, which would otherwise be silently dropped and
17
+ # produce a non-conformant ID Token.
18
+ class MissingRequiredClaim < OpenidConnectError
19
+ attr_reader :claim
20
+
21
+ def initialize(claim)
22
+ @claim = claim
23
+ super(I18n.translate("doorkeeper.openid_connect.errors.messages.missing_required_claim", claim: claim))
24
+ end
25
+ end
26
+
15
27
  class MissingConfiguration < OpenidConnectError
16
28
  def initialize
17
29
  super("Configuration for Doorkeeper OpenID Connect missing. Do you have doorkeeper_openid_connect initializer?")
@@ -125,7 +125,7 @@ module Doorkeeper
125
125
  render :new
126
126
  end
127
127
  when "select_account"
128
- select_account_for_oidc_resource_owner(owner) if owner
128
+ select_account_for_oidc_resource_owner(owner)
129
129
  when "create"
130
130
  # NOTE: not supported, but not raise error.
131
131
  else
@@ -176,7 +176,11 @@ module Doorkeeper
176
176
  end
177
177
 
178
178
  def oidc_prompt_values
179
- @oidc_prompt_values ||= params[:prompt].to_s.split(/ +/).uniq
179
+ # Reject blank entries so leading/duplicate spaces in the
180
+ # space-delimited `prompt` parameter don't surface as an empty
181
+ # value (which would otherwise be treated as an unknown prompt and
182
+ # rejected with `invalid_request`).
183
+ @oidc_prompt_values ||= params[:prompt].to_s.split(/ +/).reject(&:blank?).uniq
180
184
  end
181
185
 
182
186
  # Resolve auth_time for max_age enforcement.
@@ -5,6 +5,10 @@ module Doorkeeper
5
5
  class IdToken
6
6
  include ActiveModel::Validations
7
7
 
8
+ # OIDC Core 1.0 §2 — these claims are REQUIRED in every ID Token, so they
9
+ # must never be silently dropped when blank.
10
+ REQUIRED_CLAIMS = %i[iss sub aud exp iat].freeze
11
+
8
12
  attr_reader :nonce
9
13
 
10
14
  def initialize(access_token, nonce = nil, expires_in = Doorkeeper::OpenidConnect.configuration.expiration)
@@ -26,19 +30,31 @@ module Doorkeeper
26
30
  exp: expiration,
27
31
  iat: issued_at,
28
32
  nonce: nonce,
29
- auth_time: auth_time
33
+ auth_time: auth_time,
30
34
  )
31
35
  end
32
36
 
33
37
  def as_json(*_)
34
- claims.reject { |_, value| value.nil? || value == "" }
38
+ claims.each_with_object({}) do |(key, value), result|
39
+ blank = value.nil? || value == ""
40
+
41
+ if blank
42
+ # A REQUIRED claim must never be silently omitted; surface the
43
+ # misconfiguration instead of issuing a non-conformant ID Token.
44
+ raise Errors::MissingRequiredClaim, key if REQUIRED_CLAIMS.include?(key)
45
+
46
+ next
47
+ end
48
+
49
+ result[key] = value
50
+ end
35
51
  end
36
52
 
37
53
  def as_jws_token
38
54
  ::JWT.encode(as_json,
39
- Doorkeeper::OpenidConnect.signing_key.keypair,
40
- Doorkeeper::OpenidConnect.signing_algorithm.to_s,
41
- { typ: "JWT", kid: Doorkeeper::OpenidConnect.signing_key.kid }).to_s
55
+ Doorkeeper::OpenidConnect.signing_key.keypair,
56
+ Doorkeeper::OpenidConnect.signing_algorithm.to_s,
57
+ { typ: "JWT", kid: Doorkeeper::OpenidConnect.signing_key.kid }).to_s
42
58
  end
43
59
 
44
60
  private
@@ -46,13 +62,15 @@ module Doorkeeper
46
62
  def issuer
47
63
  Doorkeeper::OpenidConnect.resolve_issuer(
48
64
  resource_owner: @resource_owner,
49
- application: @access_token.application
65
+ application: @access_token.application,
50
66
  )
51
67
  end
52
68
 
53
69
  def subject
54
- Doorkeeper::OpenidConnect.configuration.subject.call(@resource_owner,
55
- @access_token.application).to_s
70
+ Doorkeeper::OpenidConnect.configuration.subject.call(
71
+ @resource_owner,
72
+ @access_token.application,
73
+ ).to_s
56
74
  end
57
75
 
58
76
  def audience
@@ -76,7 +94,13 @@ module Doorkeeper
76
94
  end
77
95
 
78
96
  def auth_time
79
- Doorkeeper::OpenidConnect.configuration.auth_time_from_resource_owner.call(@resource_owner).try(:to_i)
97
+ config = Doorkeeper::OpenidConnect.configuration
98
+
99
+ if config.auth_time_from_access_token
100
+ config.auth_time_from_access_token.call(@access_token).try(:to_i)
101
+ else
102
+ config.auth_time_from_resource_owner.call(@resource_owner).try(:to_i)
103
+ end
80
104
  rescue Errors::InvalidConfiguration
81
105
  nil
82
106
  end
@@ -27,7 +27,7 @@ module Doorkeeper
27
27
  def create_openid_request(access_grant)
28
28
  ::Doorkeeper::OpenidConnect.configuration.open_id_request_model.create!(
29
29
  access_grant: access_grant,
30
- nonce: pre_auth.nonce
30
+ nonce: pre_auth.nonce,
31
31
  )
32
32
  end
33
33
  end
@@ -6,10 +6,10 @@ module Doorkeeper
6
6
  def self.prepended(base)
7
7
  base.class_eval do
8
8
  has_one :openid_request,
9
- class_name: Doorkeeper::OpenidConnect.configuration.open_id_request_class,
10
- foreign_key: "access_grant_id",
11
- inverse_of: :access_grant,
12
- dependent: :delete
9
+ class_name: Doorkeeper::OpenidConnect.configuration.open_id_request_class,
10
+ foreign_key: "access_grant_id",
11
+ inverse_of: :access_grant,
12
+ dependent: :delete
13
13
  end
14
14
  end
15
15
  end
@@ -11,7 +11,7 @@ module Doorkeeper
11
11
  module ActiveRecord
12
12
  module Mixins
13
13
  autoload :OpenidRequest,
14
- "doorkeeper/openid_connect/orm/active_record/mixins/openid_request"
14
+ "doorkeeper/openid_connect/orm/active_record/mixins/openid_request"
15
15
  end
16
16
 
17
17
  def run_hooks
@@ -30,7 +30,7 @@ module Doorkeeper
30
30
  if Doorkeeper.configuration.respond_to?(:active_record_options) && Doorkeeper.configuration.active_record_options[:establish_connection]
31
31
  [Doorkeeper::OpenidConnect.configuration.open_id_request_model].each do |c|
32
32
  c.send :establish_connection,
33
- Doorkeeper.configuration.active_record_options[:establish_connection]
33
+ Doorkeeper.configuration.active_record_options[:establish_connection]
34
34
  end
35
35
  end
36
36
  end
@@ -51,7 +51,7 @@ Doorkeeper.configuration.active_record_options[:establish_connection]
51
51
  if Doorkeeper.configuration.active_record_options[:establish_connection]
52
52
  [Doorkeeper::OpenidConnect.configuration.open_id_request_model].each do |c|
53
53
  c.send :establish_connection,
54
- Doorkeeper.configuration.active_record_options[:establish_connection]
54
+ Doorkeeper.configuration.active_record_options[:establish_connection]
55
55
  end
56
56
  end
57
57
  end
@@ -11,13 +11,13 @@ module Doorkeeper
11
11
  @controllers = {
12
12
  userinfo: "doorkeeper/openid_connect/userinfo",
13
13
  discovery: "doorkeeper/openid_connect/discovery",
14
- dynamic_client_registration: "doorkeeper/openid_connect/dynamic_client_registration"
14
+ dynamic_client_registration: "doorkeeper/openid_connect/dynamic_client_registration",
15
15
  }
16
16
 
17
17
  @as = {
18
18
  userinfo: :userinfo,
19
19
  discovery: :discovery,
20
- dynamic_client_registration: :dynamic_client_registration
20
+ dynamic_client_registration: :dynamic_client_registration,
21
21
  }
22
22
 
23
23
  @skips = []
@@ -26,7 +26,7 @@ module Doorkeeper
26
26
  def [](routes)
27
27
  {
28
28
  controllers: @controllers[routes],
29
- as: @as[routes]
29
+ as: @as[routes],
30
30
  }
31
31
  end
32
32
 
@@ -14,7 +14,7 @@ module Doorkeeper
14
14
  # the canonical subject identifier (which would defeat pairwise /
15
15
  # subject-type guarantees).
16
16
  ClaimsBuilder.generate(@access_token, :user_info).merge(
17
- sub: subject
17
+ sub: subject,
18
18
  )
19
19
  end
20
20
 
@@ -4,7 +4,7 @@ module Doorkeeper
4
4
  module OpenidConnect
5
5
  MAJOR = 1
6
6
  MINOR = 10
7
- TINY = 0
7
+ TINY = 2
8
8
  PRE = nil
9
9
 
10
10
  # Full version number
@@ -132,18 +132,29 @@ module Doorkeeper
132
132
  # @return [String] the issuer string
133
133
  def self.resolve_issuer(resource_owner: nil, application: nil, request: nil)
134
134
  issuer = configuration.issuer
135
- return issuer.to_s unless issuer.respond_to?(:call)
136
-
137
- case issuer.arity
138
- when 0
139
- issuer.call
140
- when 1
141
- issuer.call(request || resource_owner)
142
- when 2
143
- issuer.call(resource_owner, application)
144
- else
145
- issuer.call(resource_owner, application, request)
146
- end.to_s
135
+
136
+ value =
137
+ if issuer.respond_to?(:call)
138
+ case issuer.arity
139
+ when 0
140
+ issuer.call
141
+ when 1
142
+ issuer.call(request || resource_owner)
143
+ when 2
144
+ issuer.call(resource_owner, application)
145
+ else
146
+ issuer.call(resource_owner, application, request)
147
+ end
148
+ else
149
+ issuer
150
+ end.to_s
151
+
152
+ if value.blank?
153
+ raise Errors::InvalidConfiguration,
154
+ I18n.translate("doorkeeper.openid_connect.errors.messages.issuer_not_configured")
155
+ end
156
+
157
+ value
147
158
  end
148
159
 
149
160
  Doorkeeper::GrantFlow.register(
@@ -161,7 +172,7 @@ module Doorkeeper
161
172
  )
162
173
 
163
174
  Doorkeeper::GrantFlow.register_alias(
164
- "implicit_oidc", as: ["implicit", "id_token", "id_token token"]
175
+ "implicit_oidc", as: ["implicit", "id_token", "id_token token"],
165
176
  )
166
177
  end
167
178
  end
@@ -10,7 +10,7 @@ module Doorkeeper
10
10
  def install
11
11
  template "initializer.rb", "config/initializers/doorkeeper_openid_connect.rb"
12
12
  copy_file File.expand_path("../../../../config/locales/en.yml", __dir__),
13
- "config/locales/doorkeeper_openid_connect.en.yml"
13
+ "config/locales/doorkeeper_openid_connect.en.yml"
14
14
  route "use_doorkeeper_openid_connect"
15
15
  end
16
16
  end
@@ -13,7 +13,7 @@ module Doorkeeper
13
13
  migration_template(
14
14
  "migration.rb.erb",
15
15
  "db/migrate/create_doorkeeper_openid_connect_tables.rb",
16
- migration_version: migration_version
16
+ migration_version: migration_version,
17
17
  )
18
18
  end
19
19
 
@@ -57,6 +57,18 @@ Doorkeeper::OpenidConnect.configure do
57
57
  # session[:auth_time]
58
58
  # end
59
59
 
60
+ # Advanced:
61
+ # If you store `auth_time` in a custom authentication context record linked
62
+ # to the access token, you can configure a block like below to derive it
63
+ # from the access token instead of `auth_time_from_resource_owner`.
64
+ #
65
+ # This allows you to track `auth_time` per grant instead of per user,
66
+ # but requires more custom implementation on your part.
67
+ #
68
+ # auth_time_from_access_token do |access_token|
69
+ # access_token.your_custom_authentication_context_record.auth_time
70
+ # end
71
+
60
72
  reauthenticate_resource_owner do |resource_owner, return_to|
61
73
  # Example implementation:
62
74
  # store_location_for resource_owner, return_to
@@ -64,9 +76,11 @@ Doorkeeper::OpenidConnect.configure do
64
76
  # redirect_to new_user_session_url
65
77
  end
66
78
 
67
- select_account_for_resource_owner do |resource_owner, return_to|
79
+ select_account_for_resource_owner do |resource_owner_or_nil, return_to|
68
80
  # Example implementation:
69
- # store_location_for resource_owner, return_to
81
+ # if resource_owner_or_nil
82
+ # store_location_for resource_owner_or_nil, return_to
83
+ # end
70
84
  # redirect_to account_select_url
71
85
  end
72
86
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper-openid_connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Dengler
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-06-01 00:00:00.000000000 Z
13
+ date: 2026-06-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: doorkeeper
@@ -245,7 +245,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
245
245
  requirements:
246
246
  - - ">="
247
247
  - !ruby/object:Gem::Version
248
- version: '3.1'
248
+ version: '3.2'
249
249
  required_rubygems_version: !ruby/object:Gem::Requirement
250
250
  requirements:
251
251
  - - ">="