doorkeeper-openid_connect 1.9.0 → 1.10.4

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/README.md +13 -242
  4. data/app/controllers/concerns/doorkeeper/openid_connect/authorizations_extension.rb +2 -1
  5. data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +15 -30
  6. data/app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb +59 -18
  7. data/config/locales/en.yml +5 -0
  8. data/lib/doorkeeper/oauth/id_token_response.rb +2 -2
  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 +4 -3
  12. data/lib/doorkeeper/openid_connect/config.rb +21 -9
  13. data/lib/doorkeeper/openid_connect/engine.rb +1 -1
  14. data/lib/doorkeeper/openid_connect/errors.rb +15 -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 +163 -30
  17. data/lib/doorkeeper/openid_connect/id_token.rb +43 -15
  18. data/lib/doorkeeper/openid_connect/id_token_token.rb +11 -4
  19. data/lib/doorkeeper/openid_connect/oauth/authorization/code.rb +1 -1
  20. data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +5 -5
  21. data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +142 -0
  22. data/lib/doorkeeper/openid_connect/oauth/password_access_token_request.rb +5 -3
  23. data/lib/doorkeeper/openid_connect/oauth/pre_authorization.rb +2 -2
  24. data/lib/doorkeeper/openid_connect/oauth/token_response.rb +1 -1
  25. data/lib/doorkeeper/openid_connect/orm/active_record/access_grant.rb +4 -4
  26. data/lib/doorkeeper/openid_connect/orm/active_record/mixins/openid_request.rb +14 -2
  27. data/lib/doorkeeper/openid_connect/orm/active_record/request.rb +1 -1
  28. data/lib/doorkeeper/openid_connect/orm/active_record.rb +35 -37
  29. data/lib/doorkeeper/openid_connect/rails/routes/mapping.rb +5 -5
  30. data/lib/doorkeeper/openid_connect/rails/routes.rb +11 -11
  31. data/lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb +18 -0
  32. data/lib/doorkeeper/openid_connect/user_info.rb +7 -4
  33. data/lib/doorkeeper/openid_connect/version.rb +2 -2
  34. data/lib/doorkeeper/openid_connect.rb +139 -48
  35. data/lib/doorkeeper/request/id_token.rb +1 -1
  36. data/lib/doorkeeper/request/id_token_token.rb +1 -1
  37. data/lib/generators/doorkeeper/openid_connect/install_generator.rb +6 -5
  38. data/lib/generators/doorkeeper/openid_connect/migration_generator.rb +9 -9
  39. data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +52 -4
  40. metadata +18 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b43afb85064c2b12a0837aec166ab720b352813fec7004dfce38cfc0b4338fc6
4
- data.tar.gz: de24da5d37377dadf9cb2bffcdf15c2b725075327efe301e0a1c907748ced3b8
3
+ metadata.gz: 7c7b483ae4502881dca6aece41d05d538677d0e8d53f2fa3b82d9b89dc9aa1d8
4
+ data.tar.gz: dd8ed48877f8c68199c466afba4bd27a3b26671a00bd5af949146e103849544e
5
5
  SHA512:
6
- metadata.gz: 0e9c242e0e9dbf48500810be186040b6cdc4afdb564196453a005ae420b9f61e6b104afcfeb1e71be2f9d1356ccca8ec1265a1e8dfdd12a0e796a9d8d19d532e
7
- data.tar.gz: b4832c67b7f870e9fa5b7745c5e1ddfd0ded602b0ca2f2cb9a351d4542f88b6e95b230c91cd24332ac6c6e6109035e589062db9c8a03faedf991a44a6ab7ddea
6
+ metadata.gz: 18b20b9a1d04b2dfe1bfd6a169057e08fee82d538990e506f46df58f70ff29a8681d30055e18542f2af82fdb3ebceb11dc976d8b4d55f2b0f01e41fb406b931c
7
+ data.tar.gz: 86b69e55903f371666178cfcaa8989e4d0c82939172a79bb070d3e50742ad4cac885edfd498d5c53af18cc98643c25cb63dc094b0c55f91c1279aec446f039a0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  - Please add here
4
4
 
5
+ ## v1.10.4 (2026-07-07)
6
+
7
+ - Validate the `scope` supplied to Dynamic Client Registration against the server's configured scopes (`default_scopes` + `optional_scopes`) before persisting it. Unrecognized scopes are silently dropped and the confirmed set is echoed in the registration response (RFC 7591 §2); a request whose scopes are all unsupported is rejected with `invalid_client_metadata` instead of being registered with an empty scope set.
8
+
9
+ ## v1.10.3 (2026-06-23)
10
+
11
+ - [#308] Fix `NameError: uninitialized constant Auth::ApplicationRecord` on boot when using a namespaced custom access grant model (e.g. `Auth::OAuthAccessGrant < ApplicationRecord`). Since v1.10.0 ([#241]) the `openid_request` association was wired inside an `ActiveSupport.on_load(:active_record)` block, which fires while `ActiveRecord::Base` is first loaded and constantizes the grant model too early. The association is now added from Doorkeeper's `AccessGrant` mixin `included` callback — at the model's own load time, without constantizing — mirroring the fix doorkeeper made in [#1830](https://github.com/doorkeeper-gem/doorkeeper/pull/1830) ([#306](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/306))
12
+
13
+ ## v1.10.2 (2026-06-22)
14
+
15
+ - [#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
16
+ - [#316] Set `fail-fast: false` in CI matrix so a single failing job no longer cancels the rest
17
+ - [#303] execute account selection even without owner, and `select_account_for_resource_owner` can now receive `nil` as the first argument.
18
+ - [#304] allow handle auth_time per grant
19
+ - [#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
20
+ - [#307] Fix `bundle exec rake server` for the test application
21
+ - [#313] Move Configuration documentation from README to Wiki
22
+ - [#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
23
+ - [#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)
24
+ - [#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
25
+
26
+ ## v1.10.1 (2026-06-03)
27
+
28
+ - [#294] Drop stale `Metrics/ClassLength` and `Metrics/BlockLength` overrides from `.rubocop_todo.yml`
29
+ - [#293] Drop `Naming/VariableNumber` from `.rubocop_todo.yml` and normalise test variable names
30
+ - [#291] Document multi-namespace mount pattern for multiple resource owner models ([#192](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/192))
31
+ - [#292] Drop formatting cops from `.rubocop_todo.yml` and align trailing-comma style with upstream doorkeeper
32
+ - [#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
33
+ - [#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))
34
+
35
+ ## v1.10.0 (2026-06-01)
36
+
37
+ >[!IMPORTANT]
38
+ >
39
+ >- **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
40
+
41
+ - [#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))
42
+ - [#242] Fix `NoMethodError` for openid_request in testing environments.
43
+ - [#246] Fix `at_hash` to use correct hash algorithm based on `signing_algorithm`
44
+ - [#250] Return configured `issuer` instead of `root_url` in WebFinger response (thanks to @sato11 for the original work in #172)
45
+ - [#248] Fix `max_age` always triggering reauthentication when `auth_time_from_resource_owner` returns Integer
46
+ - [#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`)
47
+ - [#252] Treat `auth_time_from_resource_owner` as optional in `IdToken` — omit `auth_time` claim when unconfigured instead of raising `InvalidConfiguration`
48
+ - [#256] Accept non-callable values (symbol / string) for the `protocol` config option, matching the pattern used by `issuer` / `signing_algorithm` / `signing_key` / `expiration`
49
+ - [#258] Skip `IdToken` construction on password grants without the `openid` scope
50
+ - [#259] Skip `IdToken` construction on authorization code grants without the `openid` scope
51
+ - [#261] Fix obsolete RuboCop configuration (`require:` → `plugins:`, `RSpec/FilePath` split, remove `Capybara/FeatureMethods`)
52
+ - [#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
53
+ - [#264] Apply safe RuboCop autocorrections and fix resulting artifacts
54
+ - [#265] Add Dynamic Client Registration section to README
55
+ - [#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
56
+ - [#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
57
+ - [#268] Update Dynamic Client Registration README for validated metadata parameters
58
+ - [#269] Document `authorize_dynamic_client_registration` in README
59
+ - [#270] Document the unified issuer block signature in README
60
+ - [#278] Test against Ruby 4.0.
61
+ - [#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))
62
+ - [#272] Document `auth_time_from_session` in README (follow-up to [#271](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/pull/271))
63
+ - [#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).
64
+ - [#276] Get RuboCop to zero offenses: fix `Lint/MissingSuper` in `IdTokenResponse`, replace `puts` with `warn` for deprecation notices, and modernise spec style
65
+ - [#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
66
+ - [#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`
67
+ - [#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)
68
+ - [#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
69
+ - [#288] Document `offline_access` scope recipe in README — show how to wire `use_refresh_token` with scope-based filtering for OIDC offline access
70
+ - [#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`.
71
+ - [#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
72
+ - [#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
73
+ - [#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
74
+ - [#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
75
+ - [#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`.
76
+ - [#290] Freeze `Claim#scopes` and `Claim#response` arrays at construction so callers can't accidentally mutate the claim's internal state from outside
77
+ - [#297] Fix the generated initializer's `issuer` example referencing an undefined `request` local (the block parameter is `_request`), which raised `NameError` when copied verbatim
78
+
5
79
  ## v1.9.0 (2026-03-16)
6
80
 
7
81
  - [#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/).
@@ -16,12 +14,7 @@ OpenID Connect is a single-sign-on and identity layer with a [growing list of se
16
14
  - [Known Issues](#known-issues)
17
15
  - [Example Applications](#example-applications)
18
16
  - [Installation](#installation)
19
- - [Configuration](#configuration)
20
- - [Scopes](#scopes)
21
- - [Claims](#claims)
22
- - [Routes](#routes)
23
- - [Nonces](#nonces)
24
- - [Internationalization (I18n)](#internationalization-i18n)
17
+ - [Configuration](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration)
25
18
  - [Development](#development)
26
19
  - [License](#license)
27
20
  - [Sponsors](#sponsors)
@@ -35,8 +28,9 @@ The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-
35
28
  - [UserInfo Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
36
29
  - [Normal Claims](http://openid.net/specs/openid-connect-core-1_0.html#NormalClaims)
37
30
  - [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
31
+ - [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
38
32
 
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.
33
+ 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
34
 
41
35
  Take a look at the [DiscoveryController](app/controllers/doorkeeper/openid_connect/discovery_controller.rb) for more details on supported features.
42
36
 
@@ -72,242 +66,19 @@ rails generate doorkeeper:openid_connect:migration
72
66
  rake db:migrate
73
67
  ```
74
68
 
75
- If you're upgrading from an earlier version, check [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
69
+ 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)
70
+ wiki and [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.
76
71
 
77
72
  ## Configuration
78
73
 
79
- Make sure you've [configured Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper#configuration) before continuing.
80
-
81
- Verify your settings in `config/initializers/doorkeeper.rb`:
82
-
83
- - `resource_owner_authenticator`
84
- - This callback needs to returns a falsey value if the current user can't be determined:
85
-
86
- ```ruby
87
- resource_owner_authenticator do
88
- if current_user
89
- current_user
90
- else
91
- redirect_to(new_user_session_url)
92
- nil
93
- end
94
- end
95
- ```
96
- - `grant_flows`
97
- - If you want to use `id_token` or `id_token token` response types you need to add `implicit_oidc` to `grant_flows`:
98
-
99
- ```ruby
100
- grant_flows %w(authorization_code implicit_oidc)
101
- ```
102
-
103
- The following settings are required in `config/initializers/doorkeeper_openid_connect.rb`:
104
-
105
- - `issuer`
106
- - 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
- - `subject`
109
- - 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
- - The database ID of the user is an acceptable choice if you don't mind leaking that information.
111
- - 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.
112
-
113
- ```ruby
114
- # config/initializers/doorkeeper_openid_connect.rb
115
- Doorkeeper::OpenidConnect.configure do
116
- # ...
117
- subject_types_supported [:pairwise]
118
-
119
- subject do |resource_owner, application|
120
- Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}")
121
- end
122
- # ...
123
- end
124
- ```
125
-
126
- - `signing_key`
127
- - Private key to be used for [JSON Web Signature](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31).
128
- - 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
- - 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
- - `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)
132
- - `resource_owner_from_access_token`
133
- - Defines how to translate the Doorkeeper access token to a resource owner model.
134
-
135
- The following settings are optional, but recommended for better client compatibility:
136
-
137
- - `auth_time_from_resource_owner`
138
- - 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.
140
- - `reauthenticate_resource_owner`
141
- - 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
- - Required to support the `max_age` and `prompt=login` parameters.
143
- - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.
144
- - `select_account_for_resource_owner`
145
- - Defines how to trigger account selection to choose the current login user.
146
- - Required to support the `prompt=select_account` parameter.
147
- - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.
148
-
149
- The following settings are optional:
150
-
151
- - `expiration`
152
- - Expiration time after which the ID Token must not be accepted for processing by clients.
153
- - The default is 120 seconds, it can be configured using a value or block.
154
- ```ruby
155
- # config/initializers/doorkeeper_openid_connect.rb
156
- Doorkeeper::OpenidConnect.configure do
157
- # ...
158
- expiration do |resource_owner, application|
159
- # You will have to ensure the application model implements an expiration method
160
- application.expiration
161
- end
162
- # ...
163
- end
164
- ```
165
-
166
- - `protocol`
167
- - The protocol to use when generating URIs for the discovery endpoints.
168
- - The default is `https` for production, and `http` for all other environments
169
- - Note that the OIDC specification mandates HTTPS, so you shouldn't change this
170
- for production environments unless you have a really good reason!
171
-
172
- - `end_session_endpoint`
173
- - The URL that the user is redirected to after ending the session on the client.
174
- - Used by implementations like https://github.com/IdentityModel/oidc-client-js.
175
- - The block is executed in the controller's scope, so you have access to your route helpers.
176
-
177
- - `discovery_url_options`
178
- - The URL options for every available endpoint to use when generating the endpoint URL in the
179
- discovery response. Available endpoints: `authorization`, `token`, `revocation`,
180
- `introspection`, `userinfo`, `jwks`, `webfinger`.
181
- - This option requires option keys with an available endpoint and
182
- [URL options](https://api.rubyonrails.org/v6.0.3.3/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for)
183
- as value.
184
- - The default is to use the request host, just like all the other URLs in the discovery response.
185
- - This is useful when you want endpoints to use a different URL than other requests.
186
- For example, if your Doorkeeper server is behind a firewall with other servers, you might want
187
- other servers to use an "internal" URL to communicate with Doorkeeper, but you want to present
188
- an "external" URL to end-users for authentication requests. Note that this setting does not
189
- actually change the URL that your Doorkeeper server responds on - that is outside the scope of
190
- Doorkeeper.
191
-
192
- ```ruby
193
- # config/initializers/doorkeeper_openid_connect.rb
194
- Doorkeeper::OpenidConnect.configure do
195
- # ...
196
- discovery_url_options do |request|
197
- {
198
- authorization: { host: 'host.example.com' },
199
- jwks: { protocol: request.ssl? ? :https : :http }
200
- }
201
- end
202
- # ...
203
- end
204
- ```
205
-
206
- ### Scopes
207
-
208
- 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.
209
-
210
- > 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.
211
- >
212
- > See [Using Scopes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes) in the Doorkeeper wiki for more information.
213
-
214
- ### Claims
215
-
216
- Claims can be defined in a `claims` block inside `config/initializers/doorkeeper_openid_connect.rb`:
217
-
218
- ```ruby
219
- Doorkeeper::OpenidConnect.configure do
220
- claims do
221
- claim :email do |resource_owner|
222
- resource_owner.email
223
- end
224
-
225
- claim :full_name do |resource_owner|
226
- "#{resource_owner.first_name} #{resource_owner.last_name}"
227
- end
228
-
229
- claim :preferred_username, scope: :openid do |resource_owner, scopes, access_token|
230
- # Pass the resource_owner's preferred_username if the application has
231
- # `profile` scope access. Otherwise, provide a more generic alternative.
232
- scopes.exists?(:profile) ? resource_owner.preferred_username : "summer-sun-9449"
233
- end
234
-
235
- claim :groups, response: [:id_token, :user_info] do |resource_owner|
236
- resource_owner.groups
237
- end
238
- end
239
- end
240
- ```
241
-
242
- Each claim block will be passed:
243
-
244
- - the `resource_owner`, which is the return value of `resource_owner_authenticator` in your initializer
245
- - the `scopes` granted by the access token, which is an instance of `Doorkeeper::OAuth::Scopes`
246
- - the `access_token` itself, which is an instance of `Doorkeeper::AccessToken`
247
-
248
- 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.
249
-
250
- 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
-
252
- ### Routes
253
-
254
- The installation generator will update your `config/routes.rb` to define all required routes:
255
-
256
- ``` ruby
257
- Rails.application.routes.draw do
258
- use_doorkeeper_openid_connect
259
- # your routes
260
- end
261
- ```
262
-
263
- This will mount the following routes:
264
-
265
- ```
266
- GET /oauth/userinfo
267
- POST /oauth/userinfo
268
- GET /oauth/discovery/keys
269
- GET /.well-known/openid-configuration
270
- GET /.well-known/webfinger
271
- ```
272
-
273
- 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
-
275
- ### Nonces
276
-
277
- To support clients who send nonces you have to tweak Doorkeeper's authorization view so the parameter is passed on.
278
-
279
- If you don't already have custom templates, run this generator in your Rails application to add them:
280
-
281
- ```sh
282
- rails generate doorkeeper:views
283
- ```
284
-
285
- Then tweak the template as follows:
286
-
287
- ```patch
288
- --- i/app/views/doorkeeper/authorizations/new.html.erb
289
- +++ w/app/views/doorkeeper/authorizations/new.html.erb
290
- @@ -26,6 +26,7 @@
291
- <%= hidden_field_tag :state, @pre_auth.state %>
292
- <%= hidden_field_tag :response_type, @pre_auth.response_type %>
293
- <%= hidden_field_tag :scope, @pre_auth.scope %>
294
- + <%= hidden_field_tag :nonce, @pre_auth.nonce %>
295
- <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
296
- <% end %>
297
- <%= form_tag oauth_authorization_path, method: :delete do %>
298
- @@ -34,6 +35,7 @@
299
- <%= hidden_field_tag :state, @pre_auth.state %>
300
- <%= hidden_field_tag :response_type, @pre_auth.response_type %>
301
- <%= hidden_field_tag :scope, @pre_auth.scope %>
302
- + <%= hidden_field_tag :nonce, @pre_auth.nonce %>
303
- <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
304
- <% end %>
305
- </div>
306
- ```
307
-
308
- ### Internationalization (I18n)
74
+ See the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration) for detailed configuration instructions, including:
309
75
 
310
- 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`.
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)
311
82
 
312
83
  ## Development
313
84
 
@@ -334,7 +105,7 @@ bundle exec rake server
334
105
  By default, the latest Rails version is used. To use a specific version run:
335
106
 
336
107
  ```
337
- rails=4.2.0 bundle update
108
+ rails=7.2 bundle update
338
109
  ```
339
110
 
340
111
  ## 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,16 +48,16 @@ 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
 
54
55
  id_token_signing_alg_values_supported: [
55
- ::Doorkeeper::OpenidConnect.signing_algorithm
56
+ ::Doorkeeper::OpenidConnect.signing_algorithm,
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),
100
- }
101
- ]
95
+ href: issuer,
96
+ },
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
@@ -125,19 +113,16 @@ module Doorkeeper
125
113
 
126
114
  def discovery_url_default_options
127
115
  {
128
- protocol: protocol
116
+ protocol: protocol,
129
117
  }
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,41 +3,82 @@
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
- status: :bad_request
20
+ status: :bad_request
14
21
  end
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: registration.permitted_scopes,
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
+ # 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
79
+ end
80
+
81
+ response
41
82
  end
42
83
  end
43
84
  end
@@ -21,3 +21,8 @@ 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
+ issuer_not_configured: 'Doorkeeper::OpenidConnect.configure.issuer must resolve to a non-blank value.'
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'
@@ -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,9 +20,8 @@ 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
- id_token: id_token.as_jws_token
24
+ id_token: id_token.as_jws_token,
25
25
  }
26
26
  end
27
27
 
@@ -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