doorkeeper-openid_connect 1.9.0 → 1.10.5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/README.md +13 -242
  4. data/app/controllers/concerns/doorkeeper/openid_connect/authorizations_extension.rb +7 -1
  5. data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +19 -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/error_response.rb +45 -0
  17. data/lib/doorkeeper/openid_connect/helpers/controller/max_age.rb +69 -0
  18. data/lib/doorkeeper/openid_connect/helpers/controller/prompt.rb +113 -0
  19. data/lib/doorkeeper/openid_connect/helpers/controller/token_matching.rb +59 -0
  20. data/lib/doorkeeper/openid_connect/helpers/controller.rb +65 -121
  21. data/lib/doorkeeper/openid_connect/id_token.rb +43 -15
  22. data/lib/doorkeeper/openid_connect/id_token_token.rb +11 -4
  23. data/lib/doorkeeper/openid_connect/oauth/authorization/code.rb +1 -1
  24. data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +10 -5
  25. data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +142 -0
  26. data/lib/doorkeeper/openid_connect/oauth/password_access_token_request.rb +5 -3
  27. data/lib/doorkeeper/openid_connect/oauth/pre_authorization.rb +2 -2
  28. data/lib/doorkeeper/openid_connect/oauth/token_response.rb +5 -1
  29. data/lib/doorkeeper/openid_connect/orm/active_record/access_grant.rb +4 -4
  30. data/lib/doorkeeper/openid_connect/orm/active_record/mixins/openid_request.rb +14 -2
  31. data/lib/doorkeeper/openid_connect/orm/active_record/request.rb +1 -1
  32. data/lib/doorkeeper/openid_connect/orm/active_record.rb +42 -37
  33. data/lib/doorkeeper/openid_connect/rails/routes/mapping.rb +5 -5
  34. data/lib/doorkeeper/openid_connect/rails/routes.rb +11 -11
  35. data/lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb +18 -0
  36. data/lib/doorkeeper/openid_connect/user_info.rb +7 -4
  37. data/lib/doorkeeper/openid_connect/version.rb +2 -2
  38. data/lib/doorkeeper/openid_connect.rb +139 -48
  39. data/lib/doorkeeper/request/id_token.rb +1 -1
  40. data/lib/doorkeeper/request/id_token_token.rb +1 -1
  41. data/lib/generators/doorkeeper/openid_connect/install_generator.rb +6 -5
  42. data/lib/generators/doorkeeper/openid_connect/migration_generator.rb +9 -9
  43. data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +109 -4
  44. metadata +22 -13
  45. data/lib/doorkeeper/openid_connect/claims/aggregated_claim.rb +0 -11
  46. data/lib/doorkeeper/openid_connect/claims/distributed_claim.rb +0 -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: cc9243cc386621bb9b9474617c885f398632d60fdeabc7ecb50deab1364bb787
4
+ data.tar.gz: e33f32f4992a35ec43762c4c5c60d0ba6e98bb8b4f7c60c8cd0d4d69b4c1f535
5
5
  SHA512:
6
- metadata.gz: 0e9c242e0e9dbf48500810be186040b6cdc4afdb564196453a005ae420b9f61e6b104afcfeb1e71be2f9d1356ccca8ec1265a1e8dfdd12a0e796a9d8d19d532e
7
- data.tar.gz: b4832c67b7f870e9fa5b7745c5e1ddfd0ded602b0ca2f2cb9a351d4542f88b6e95b230c91cd24332ac6c6e6109035e589062db9c8a03faedf991a44a6ab7ddea
6
+ metadata.gz: ca8e15dde2fb57d5badf44ddb67f5109eb7b419e92ae33b4f7c3bee6cc998ae4ec50a31bc57aefae40578c5c094a85ac5cfc8cabdbbd4a1c77316fcc7651faf4
7
+ data.tar.gz: 7ac0048a88dc1968427dbb67ab814e85ebc361d3210d2d6d6bf29e52bd4f09fd94f6eb9792ab1c495ca7ef9323a9d0ca29d43aba8c13e6658cfe97c17f2a2230
data/CHANGELOG.md CHANGED
@@ -1,6 +1,87 @@
1
1
  ## Unreleased
2
2
 
3
+ ## v1.10.5 (2026-07-09)
4
+
5
+ - [#329] Restore compatibility with the full `doorkeeper >= 5.5` range declared in the gemspec ([#328](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/328)). Doorkeeper 5.5.x raised `NameError: uninitialized constant Doorkeeper::Orm::ActiveRecord::Mixins` at load time (the mixin file ships since 5.5.0 but is only autoloadable since 5.6.0 — it is now required explicitly when absent), and Doorkeeper 5.6/5.7 broke the discovery endpoint because `pkce_code_challenge_methods` only exists since 5.8.0 (the discovery response now falls back to `plain S256`, which is what those versions accept). CI now exercises the oldest supported Doorkeeper series via `gemfiles/doorkeeper_5.{5,6,7}.gemfile`
3
6
  - Please add here
7
+ - [#325] Remove the dead `Claims::AggregatedClaim` and `Claims::DistributedClaim` classes. They have never been required, instantiated, or reachable through the claims DSL (`ClaimsBuilder` only builds `NormalClaim`), and the discovery document only advertises `claim_types_supported: ["normal"]` — the files just shipped in the gem unused since 2016
8
+ - [#324] Split `Helpers::Controller` (294 lines, one module) into focused submodules — `Prompt`, `MaxAge`, `ErrorResponse`, `TokenMatching` — and decompose the two most complex methods (`handle_oidc_prompt_param!`, `handle_oidc_max_age_param!`). This is a behavior-preserving refactor that allows dropping the `Metrics/ModuleLength`, `Metrics/CyclomaticComplexity` and `Metrics/PerceivedComplexity` overrides from `.rubocop_todo.yml`; the codebase now passes those cops at RuboCop's default thresholds
9
+ - [#326] Make the dynamic client registration spec robust against pre-existing `Doorkeeper::Application` rows: replace absolute `Application.count` assertions and `Application.first` lookups with relative `change` matchers and `Application.find_by(uid:)` lookups keyed on the `client_id` returned by each request. With a polluted `test.sqlite3` (e.g. left behind by a `rake server` session against the same dummy app) 12 of the 20 examples failed spuriously; they now pass regardless of prior DB state
10
+ - [#327] Document previously-undocumented config options in the generated initializer template: `authorize_dynamic_client_registration` (DCR endpoint authorization gate), `apply_prompt_to_non_oidc_requests`, `end_session_endpoint`, `discovery_url_options`, and the `response:` option for claims (controlling whether a claim appears in the ID Token, UserInfo, or both). No behavior change
11
+
12
+ ## v1.10.4 (2026-07-07)
13
+
14
+ - 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.
15
+
16
+ ## v1.10.3 (2026-06-23)
17
+
18
+ - [#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))
19
+
20
+ ## v1.10.2 (2026-06-22)
21
+
22
+ - [#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
23
+ - [#316] Set `fail-fast: false` in CI matrix so a single failing job no longer cancels the rest
24
+ - [#303] execute account selection even without owner, and `select_account_for_resource_owner` can now receive `nil` as the first argument.
25
+ - [#304] allow handle auth_time per grant
26
+ - [#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
27
+ - [#307] Fix `bundle exec rake server` for the test application
28
+ - [#313] Move Configuration documentation from README to Wiki
29
+ - [#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
30
+ - [#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)
31
+ - [#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
32
+
33
+ ## v1.10.1 (2026-06-03)
34
+
35
+ - [#294] Drop stale `Metrics/ClassLength` and `Metrics/BlockLength` overrides from `.rubocop_todo.yml`
36
+ - [#293] Drop `Naming/VariableNumber` from `.rubocop_todo.yml` and normalise test variable names
37
+ - [#291] Document multi-namespace mount pattern for multiple resource owner models ([#192](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/192))
38
+ - [#292] Drop formatting cops from `.rubocop_todo.yml` and align trailing-comma style with upstream doorkeeper
39
+ - [#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
40
+ - [#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))
41
+
42
+ ## v1.10.0 (2026-06-01)
43
+
44
+ >[!IMPORTANT]
45
+ >
46
+ >- **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
47
+
48
+ - [#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))
49
+ - [#242] Fix `NoMethodError` for openid_request in testing environments.
50
+ - [#246] Fix `at_hash` to use correct hash algorithm based on `signing_algorithm`
51
+ - [#250] Return configured `issuer` instead of `root_url` in WebFinger response (thanks to @sato11 for the original work in #172)
52
+ - [#248] Fix `max_age` always triggering reauthentication when `auth_time_from_resource_owner` returns Integer
53
+ - [#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`)
54
+ - [#252] Treat `auth_time_from_resource_owner` as optional in `IdToken` — omit `auth_time` claim when unconfigured instead of raising `InvalidConfiguration`
55
+ - [#256] Accept non-callable values (symbol / string) for the `protocol` config option, matching the pattern used by `issuer` / `signing_algorithm` / `signing_key` / `expiration`
56
+ - [#258] Skip `IdToken` construction on password grants without the `openid` scope
57
+ - [#259] Skip `IdToken` construction on authorization code grants without the `openid` scope
58
+ - [#261] Fix obsolete RuboCop configuration (`require:` → `plugins:`, `RSpec/FilePath` split, remove `Capybara/FeatureMethods`)
59
+ - [#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
60
+ - [#264] Apply safe RuboCop autocorrections and fix resulting artifacts
61
+ - [#265] Add Dynamic Client Registration section to README
62
+ - [#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
63
+ - [#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
64
+ - [#268] Update Dynamic Client Registration README for validated metadata parameters
65
+ - [#269] Document `authorize_dynamic_client_registration` in README
66
+ - [#270] Document the unified issuer block signature in README
67
+ - [#278] Test against Ruby 4.0.
68
+ - [#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))
69
+ - [#272] Document `auth_time_from_session` in README (follow-up to [#271](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/pull/271))
70
+ - [#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).
71
+ - [#276] Get RuboCop to zero offenses: fix `Lint/MissingSuper` in `IdTokenResponse`, replace `puts` with `warn` for deprecation notices, and modernise spec style
72
+ - [#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
73
+ - [#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`
74
+ - [#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)
75
+ - [#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
76
+ - [#288] Document `offline_access` scope recipe in README — show how to wire `use_refresh_token` with scope-based filtering for OIDC offline access
77
+ - [#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`.
78
+ - [#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
79
+ - [#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
80
+ - [#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
81
+ - [#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
82
+ - [#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`.
83
+ - [#290] Freeze `Claim#scopes` and `Claim#response` arrays at construction so callers can't accidentally mutate the claim's internal state from outside
84
+ - [#297] Fix the generated initializer's `issuer` example referencing an undefined `request` local (the block parameter is `_request`), which raised `NameError` when copied verbatim
4
85
 
5
86
  ## v1.9.0 (2026-03-16)
6
87
 
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-from-Old-Versions)
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,12 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Doorkeeper
2
4
  module OpenidConnect
3
5
  module AuthorizationsExtension
4
6
  private
5
7
 
8
+ # Whitelist the OIDC `nonce` authorization parameter so Doorkeeper
9
+ # carries it into the PreAuthorization (see
10
+ # Doorkeeper::OpenidConnect::OAuth::PreAuthorization#initialize, which
11
+ # reads `attrs[:nonce]`). Without this the nonce would be dropped and
12
+ # never make it onto the issued ID Token.
6
13
  def pre_auth_param_fields
7
14
  super.append(:nonce)
8
15
  end
9
16
  end
10
17
  end
11
18
  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,14 +80,13 @@ 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
 
86
+ # Doorkeeper < 5.8 has no `pkce_code_challenge_methods` option and
87
+ # always accepts both methods whenever PKCE is available.
88
+ return %w[plain S256] unless doorkeeper.respond_to?(:pkce_code_challenge_methods)
89
+
90
90
  doorkeeper.pkce_code_challenge_methods
91
91
  end
92
92
 
@@ -96,27 +96,19 @@ module Doorkeeper
96
96
  links: [
97
97
  {
98
98
  rel: WEBFINGER_RELATION,
99
- href: root_url(webfinger_url_options),
100
- }
101
- ]
99
+ href: issuer,
100
+ },
101
+ ],
102
102
  }
103
103
  end
104
104
 
105
105
  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
- }
106
+ { keys: Doorkeeper::OpenidConnect.signing_keys_normalized }
116
107
  end
117
108
 
118
109
  def protocol
119
- Doorkeeper::OpenidConnect.configuration.protocol.call
110
+ configured = Doorkeeper::OpenidConnect.configuration.protocol
111
+ configured.respond_to?(:call) ? configured.call : configured
120
112
  end
121
113
 
122
114
  def discovery_url_options
@@ -125,19 +117,16 @@ module Doorkeeper
125
117
 
126
118
  def discovery_url_default_options
127
119
  {
128
- protocol: protocol
120
+ protocol: protocol,
129
121
  }
130
122
  end
131
123
 
132
124
  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
125
+ Doorkeeper::OpenidConnect.resolve_issuer(request: request)
138
126
  end
139
127
 
140
- %i[authorization token revocation introspection userinfo jwks webfinger dynamic_client_registration].each do |endpoint|
128
+ %i[authorization token revocation introspection userinfo jwks
129
+ dynamic_client_registration].each do |endpoint|
141
130
  define_method :"#{endpoint}_url_options" do
142
131
  discovery_url_default_options.merge(discovery_url_options[endpoint.to_sym] || {})
143
132
  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'