doorkeeper-openid_connect 1.10.5 → 2.0.0.beta1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/README.md +47 -20
  4. data/app/controllers/concerns/doorkeeper/openid_connect/metadata_extension.rb +72 -0
  5. data/app/controllers/doorkeeper/openid_connect/discovery_controller.rb +20 -42
  6. data/app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb +35 -4
  7. data/app/controllers/doorkeeper/openid_connect/userinfo_controller.rb +21 -1
  8. data/config/locales/en.yml +18 -0
  9. data/lib/doorkeeper/oauth/id_token_request.rb +3 -6
  10. data/lib/doorkeeper/oauth/id_token_response.rb +25 -1
  11. data/lib/doorkeeper/oauth/id_token_token_request.rb +6 -1
  12. data/lib/doorkeeper/oauth/id_token_token_response.rb +5 -1
  13. data/lib/doorkeeper/openid_connect/claims/claim.rb +5 -1
  14. data/lib/doorkeeper/openid_connect/claims_builder.rb +11 -2
  15. data/lib/doorkeeper/openid_connect/config.rb +102 -0
  16. data/lib/doorkeeper/openid_connect/discovery_helpers_mixin.rb +69 -0
  17. data/lib/doorkeeper/openid_connect/engine.rb +7 -0
  18. data/lib/doorkeeper/openid_connect/errors.rb +11 -5
  19. data/lib/doorkeeper/openid_connect/grant_types_supported_mixin.rb +24 -2
  20. data/lib/doorkeeper/openid_connect/helpers/controller/error_response.rb +7 -0
  21. data/lib/doorkeeper/openid_connect/helpers/controller/max_age.rb +20 -2
  22. data/lib/doorkeeper/openid_connect/helpers/controller/prompt.rb +27 -5
  23. data/lib/doorkeeper/openid_connect/helpers/controller.rb +8 -9
  24. data/lib/doorkeeper/openid_connect/{id_token_token.rb → hybrid_id_token_concern.rb} +10 -2
  25. data/lib/doorkeeper/openid_connect/id_token.rb +17 -5
  26. data/lib/doorkeeper/openid_connect/oauth/authorization/code.rb +5 -14
  27. data/lib/doorkeeper/openid_connect/oauth/authorization_code_request.rb +10 -5
  28. data/lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb +7 -2
  29. data/lib/doorkeeper/openid_connect/oauth/metadata_response.rb +33 -0
  30. data/lib/doorkeeper/openid_connect/oauth/password_access_token_request.rb +7 -11
  31. data/lib/doorkeeper/openid_connect/oauth/token_response.rb +53 -13
  32. data/lib/doorkeeper/openid_connect/orm/active_record/mixins/application.rb +93 -0
  33. data/lib/doorkeeper/openid_connect/orm/active_record.rb +36 -3
  34. data/lib/doorkeeper/openid_connect/rails/routes.rb +15 -1
  35. data/lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb +49 -3
  36. data/lib/doorkeeper/openid_connect/user_info.rb +11 -6
  37. data/lib/doorkeeper/openid_connect/version.rb +4 -4
  38. data/lib/doorkeeper/openid_connect.rb +192 -39
  39. data/lib/generators/doorkeeper/openid_connect/add_post_logout_redirect_uris_generator.rb +38 -0
  40. data/lib/generators/doorkeeper/openid_connect/templates/add_post_logout_redirect_uris.rb.erb +5 -0
  41. data/lib/generators/doorkeeper/openid_connect/templates/initializer.rb +30 -0
  42. data/lib/generators/doorkeeper/openid_connect/templates/migration.rb.erb +2 -0
  43. metadata +13 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc9243cc386621bb9b9474617c885f398632d60fdeabc7ecb50deab1364bb787
4
- data.tar.gz: e33f32f4992a35ec43762c4c5c60d0ba6e98bb8b4f7c60c8cd0d4d69b4c1f535
3
+ metadata.gz: 960d2cf9c359d5bae10fc7be2eff5fba0eb5309b359422d81ea0e0a3c451f192
4
+ data.tar.gz: e2efc3c51af4c54d599dd884264b9ce41cf136fa94f291d4222186dac3e266bd
5
5
  SHA512:
6
- metadata.gz: ca8e15dde2fb57d5badf44ddb67f5109eb7b419e92ae33b4f7c3bee6cc998ae4ec50a31bc57aefae40578c5c094a85ac5cfc8cabdbbd4a1c77316fcc7651faf4
7
- data.tar.gz: 7ac0048a88dc1968427dbb67ab814e85ebc361d3210d2d6d6bf29e52bd4f09fd94f6eb9792ab1c495ca7ef9323a9d0ca29d43aba8c13e6658cfe97c17f2a2230
6
+ metadata.gz: f349f5d093f30c5ed07a77349cc63d0651047185587c149172ec5d38eb29bd4644a74f3b10fd54674f43beda2885f58b7b400094e19afda4d24daadd56dc1e8d
7
+ data.tar.gz: 73e1e48f7fa439a9f7df513db74ba0a5df969a19a32abdf9c1b8bda34db08356abde81b3b3fdf425a0bbc46c8f258b669d1368cf25d067f187405e2f7cc8efd0
data/CHANGELOG.md CHANGED
@@ -1,9 +1,57 @@
1
+ Upgrading? [Migration from Old Versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions) walks through every breaking change and the steps it asks for, and the rest of the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki) documents the configuration and behavior the entries below refer to.
2
+
1
3
  ## Unreleased
2
4
 
5
+ - Add entry here
6
+
7
+ ## v2.0.0.beta1 (2026-08-20)
8
+
9
+ >[!IMPORTANT]
10
+ >
11
+ >- **This is a prerelease.** RubyGems does not resolve prereleases from an unqualified requirement, so it must be requested explicitly: `gem "doorkeeper-openid_connect", "2.0.0.beta1"`
12
+ >- **Migration required:** existing installations must add the `post_logout_redirect_uris` column — `rails generate doorkeeper:openid_connect:add_post_logout_redirect_uris` followed by `rails db:migrate` ([#243])
13
+ >- **Breaking (hybrid response type):** the `id_token token` response object is now the configured `id_token_class` extended with `HybridIdTokenConcern`, and a custom `id_token_class` must expose an `#access_token` reader ([#337])
14
+ >- **Breaking (constant removed):** `Doorkeeper::OpenidConnect::IdTokenToken` is gone — custom subclasses must subclass `IdToken` and include `HybridIdTokenConcern` ([#338])
15
+ >- **Breaking (Dynamic Client Registration):** a registration request that omits `response_types` / `grant_types` now defaults to `["code"]` / `["authorization_code"]` per RFC 7591 §2, instead of inheriting the server's global configuration ([#350])
16
+ >
17
+ >[Migration from Old Versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions) walks through each one.
18
+
19
+ - [#243] Add per-client `post_logout_redirect_uris` for RP-Initiated Logout, exposed via `Doorkeeper::Application#post_logout_redirect_uris` and `#valid_post_logout_redirect_uri?(uri)`. URIs are validated with the same rules as `redirect_uri`; Dynamic Client Registration accepts and echoes them back
20
+ - [#320] Support mounting the engine under multiple named scopes — `use_doorkeeper_openid_connect as: :users` makes each mount's discovery document advertise its own endpoints ([#192])
21
+ - [#322] Fall back to Doorkeeper's `issuer` configuration when the OpenID Connect `issuer` is not set. The OpenID Connect value still takes precedence ([#321])
22
+ - [#323] Resolve `token_endpoint_auth_methods_supported` from Doorkeeper's client authentication methods registry when available, falling back to legacy `client_credentials_methods` on older versions
23
+ - [#332] Fix `grant_types_supported` listing `refresh_token` twice when enabled via both `grant_flows` and `use_refresh_token`
24
+ - [#333] Emit the RFC 9207 `iss` parameter on `id_token` / `id_token token` authorization responses and on OIDC error redirects, and advertise `authorization_response_iss_parameter_supported` in the discovery document. No behavior change until Doorkeeper is configured with an `issuer`
25
+ - [#334] Allow Doorkeeper 6.0 (`>= 5.5, < 7.0`). Note: Doorkeeper 6.0's `force_pkce` applies to confidential clients too — `id_token` / `id_token token` authorization requests are rejected unless they carry a `code_challenge`
26
+ - [#335] Add `id_token_class` and `user_info_class` config options for custom ID Token / UserInfo response objects
27
+ - [#337] Rework `id_token_class` / `user_info_class` internals — lazy-resolve and validate configured classes at first use (fixes zeitwerk on Rails 7+) — and deprecate `Doorkeeper::OpenidConnect::IdTokenToken`. A custom `id_token_class` must now expose an `#access_token` reader. **Breaking (type change):** the hybrid response token is now the configured `id_token_class` extended with `HybridIdTokenConcern` — see the [migration guide](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions)
28
+ - [#338] **Breaking:** Remove the deprecated `IdTokenToken` class. Custom subclasses must subclass `IdToken` and include `HybridIdTokenConcern` instead
29
+ - [#341] Fix dynamic client registration to respect the `application_class` configuration
30
+ - [#347] Omit symmetric (`oct`) keys from the JWKS endpoint when signing with an HMAC algorithm (RFC 7517)
31
+ - [#348] Authorization endpoint hardening:
32
+ - Treat a malformed `max_age` parameter (e.g. `max_age[]=1`) as absent instead of a 500
33
+ - Build `prompt=login` / `prompt=select_account` return URLs without mutating the shared `request.query_parameters` hash
34
+ - Route internal errors (e.g. `InvalidConfiguration`) to a 500 instead of leaking them as authorization error redirects
35
+ - [#349] Fix several 500 errors in the claims pipeline: gracefully omit `id_token` when the access token has no resource owner, no application, or an owner that no longer resolves; return `401 invalid_token` at userinfo for the same cases. Also dispatch claims whose `response:` option is configured with strings (e.g. `response: ["id_token"]`), which were previously dropped from every response
36
+ - [#350] Discovery / Dynamic Client Registration spec compliance:
37
+ - Advertise the standard `implicit` grant type instead of the internal `implicit_oidc` name (RFC 7591 §2)
38
+ - **Breaking:** omitted `response_types` / `grant_types` in a registration request now default to `["code"]` / `["authorization_code"]` per RFC 7591 §2
39
+ - Use RFC 7591 §3.2.2 error codes (`invalid_redirect_uri` / `invalid_client_metadata`) instead of the invented `invalid_client_params`
40
+ - Serve WebFinger as `application/jrd+json` with `Access-Control-Allow-Origin: *` (RFC 7033)
41
+ - Create dynamically registered clients through `application_model` so custom models work with DCR
42
+ - [#351] Align OpenID Connect token/authorization responses with Doorkeeper's response contract: return the plaintext access token from the `id_token token` implicit response (fixes `hash_token_secrets`), define `#issued_token` on OIDC responses, and attach the ID token before `after_successful_strategy_response` fires
43
+ - [#353] Enrich Doorkeeper 6.0's RFC 8414 metadata document (`/.well-known/oauth-authorization-server`) with the OpenID Connect fields (`jwks_uri`, `userinfo_endpoint`, signing algs, claims) via the `custom_metadata` seam; app-configured `custom_metadata` keeps precedence. No behavior change on Doorkeeper < 6.0
44
+ - [#355] Compute `at_hash` from the plaintext access token instead of the stored value, fixing ID Token validation when `hash_token_secrets` is enabled
45
+ - [#360] Load the OpenID Connect constants with `autoload` instead of `require`, including the requests and responses this gem contributes to Doorkeeper's own namespace. No public constant changes name or moves ([#362])
46
+ - [#363] Extend [#349]'s `id_token` guard to the password grant with `skip_client_authentication_for_password_grant` — previously returned an unhandled 500
47
+ - [#370] Correct the stale `rake db:migrate` command to `rails db:migrate` in the README, the [#243] migration note, and the `post_logout_redirect_uris` missing-column error message
48
+ - [#372] Fix duplicate entries in the discovery document's `claims_supported` when a custom claim shadows a base claim (`iss`/`sub`/`aud`/`exp`/`iat`)
49
+ - [#373] Fix `prompt=consent` under Doorkeeper's `api_only` mode — the consent step now returns the pre-authorization as JSON instead of attempting to render a template that `ActionController::API` cannot serve
50
+ - [#381] Echo the registered `client_name` in the Dynamic Client Registration response, as RFC 7591 §3.2.1 requires the response to include all registered client metadata
51
+
3
52
  ## v1.10.5 (2026-07-09)
4
53
 
5
54
  - [#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`
6
- - Please add here
7
55
  - [#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
56
  - [#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
57
  - [#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
data/README.md CHANGED
@@ -3,25 +3,28 @@
3
3
  [![CI](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/actions/workflows/ci.yml/badge.svg)](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/actions/workflows/ci.yml)
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
+ [![Downloads](https://img.shields.io/gem/dt/doorkeeper-openid_connect.svg)](https://rubygems.org/gems/doorkeeper-openid_connect)
6
7
 
7
8
  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.
8
9
 
9
10
  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/).
10
11
 
12
+ Full documentation lives in the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki).
13
+
11
14
  ## Table of Contents
12
15
 
13
16
  - [Status](#status)
14
- - [Known Issues](#known-issues)
15
17
  - [Example Applications](#example-applications)
18
+ - [Requirements](#requirements)
16
19
  - [Installation](#installation)
17
- - [Configuration](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration)
20
+ - [Documentation](#documentation)
18
21
  - [Development](#development)
19
22
  - [License](#license)
20
23
  - [Sponsors](#sponsors)
21
24
 
22
25
  ## Status
23
26
 
24
- The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-connect-core-1_0.html) are currently supported:
27
+ The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-connect-core-1_0.html) and related specifications are currently supported:
25
28
  - [Authentication using the Authorization Code Flow](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
26
29
  - [Authentication using the Implicit Flow](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth)
27
30
  - [Requesting Claims using Scope Values](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
@@ -29,20 +32,30 @@ The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-
29
32
  - [Normal Claims](http://openid.net/specs/openid-connect-core-1_0.html#NormalClaims)
30
33
  - [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
31
34
  - [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
35
+ - [RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) client metadata — per-client `post_logout_redirect_uris` registration and validation; see [RP-Initiated Logout](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/RP-Initiated-Logout) for the host application's part
36
+ - [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) `iss` authorization response parameter, emitted when Doorkeeper itself is configured with an `issuer`
37
+ - [RFC 8414 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) — see [Routes](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Routes) for how this interacts with the metadata endpoint Doorkeeper 6.0 serves itself
32
38
 
33
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.
34
40
 
35
- Take a look at the [DiscoveryController](app/controllers/doorkeeper/openid_connect/discovery_controller.rb) for more details on supported features.
36
-
37
- ### Known Issues
38
-
39
- - Doorkeeper's API mode (`Doorkeeper.configuration.api_only`) is not properly supported yet
41
+ Take a look at the [DiscoveryController](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/blob/master/app/controllers/doorkeeper/openid_connect/discovery_controller.rb) for more details on supported features.
40
42
 
41
43
  ### Example Applications
42
44
 
43
- - [GitLab](https://gitlab.com/gitlab-org/gitlab-ce) ([original MR](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8018))
45
+ - [GitLab](https://gitlab.com/gitlab-org/gitlab) ([original MR](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8018))
44
46
  - [Testing app for this gem](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/tree/master/spec/dummy)
45
47
 
48
+ ## Requirements
49
+
50
+ | Dependency | Supported versions |
51
+ | --- | --- |
52
+ | Ruby | 3.2 or newer |
53
+ | Rails | 7.0 to 8.1 |
54
+ | Doorkeeper | 5.5 or newer, below 7.0 |
55
+ | ORM | Active Record only |
56
+
57
+ The [CI matrix](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/blob/master/.github/workflows/ci.yml) is the authoritative statement of what is tested: each Rails series above against Ruby 3.2 through 4.0 and — except for Rails 7.0 — against `ruby-head`, plus one build per supported Doorkeeper series and one against Doorkeeper's `main` branch.
58
+
46
59
  ## Installation
47
60
 
48
61
  Make sure your application is already set up with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper#installation).
@@ -63,22 +76,22 @@ Generate a migration for Active Record (other ORMs are currently not supported):
63
76
 
64
77
  ```sh
65
78
  rails generate doorkeeper:openid_connect:migration
66
- rake db:migrate
79
+ rails db:migrate
67
80
  ```
68
81
 
69
82
  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.
83
+ wiki and [CHANGELOG.md](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/blob/master/CHANGELOG.md) for upgrade instructions, including the
84
+ migrations that newer versions add to existing installations.
71
85
 
72
- ## Configuration
86
+ ## Documentation
73
87
 
74
- See the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration) for detailed configuration instructions, including:
88
+ Configuration and usage are documented in the [wiki](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki), whose home page indexes every topic. The pages to start from:
75
89
 
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)
90
+ - [Configuration](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Configuration) — the issuer, subject, signing keys, and every other initializer option
91
+ - [Migration from Old Versions](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Migration-from-Old-Versions) — upgrade instructions for breaking changes
92
+ - [Troubleshooting](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/wiki/Troubleshooting) — symptoms, causes, and fixes for common integration pitfalls
93
+
94
+ The remaining pages cover scopes and claims, `prompt` and `max_age`, routes and multiple mounts, nonces, RP-Initiated Logout, Dynamic Client Registration, and I18n.
82
95
 
83
96
  ## Development
84
97
 
@@ -102,9 +115,23 @@ To run the local engine server:
102
115
  bundle exec rake server
103
116
  ```
104
117
 
105
- By default, the latest Rails version is used. To use a specific version run:
118
+ To run the browser end-to-end tests, which boot the dummy app with
119
+ [Capybara](https://github.com/teamcapybara/capybara) and drive its dashboard
120
+ through [Cuprite](https://github.com/rubycdp/cuprite):
106
121
 
122
+ ```sh
123
+ bundle exec rake e2e
107
124
  ```
125
+
126
+ They need an installed Chrome or Chromium and nothing else. Run them with
127
+ `HEADLESS=false` to watch a flow in a visible browser. The suite boots the app
128
+ in the development environment against its own `spec/dummy/db/e2e.sqlite3`
129
+ database, so it leaves the one behind `bundle exec rake server` alone, and it
130
+ saves the page of a failing example to `spec/dummy/tmp/e2e`.
131
+
132
+ By default, Rails 8.0 is used. To use a specific version run:
133
+
134
+ ```sh
108
135
  rails=7.2 bundle update
109
136
  ```
110
137
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module OpenidConnect
5
+ # Prepended to Doorkeeper::MetadataController (Doorkeeper >= 6.0) so the
6
+ # RFC 8414 document at `/.well-known/oauth-authorization-server` also
7
+ # carries the OpenID Connect metadata this gem serves from its discovery
8
+ # document — otherwise the two documents describe the same server with a
9
+ # different issuer and without the OIDC endpoints and capabilities.
10
+ #
11
+ # When mounted under a named scope, Doorkeeper's metadata route carries no
12
+ # `route_helper_prefix`, so the enriched endpoints resolve to the default
13
+ # mount — matching how the core document advertises its own endpoints
14
+ # there. Scoped mounts are fully served by the gem's namespaced
15
+ # `/.well-known/openid-configuration` document.
16
+ module MetadataExtension
17
+ include DiscoveryHelpersMixin
18
+
19
+ private
20
+
21
+ def metadata_response
22
+ return super unless Doorkeeper::OpenidConnect.configured? && oidc_routes_mounted?
23
+
24
+ @metadata_response ||= Doorkeeper::OpenidConnect::OAuth::MetadataResponse.new(
25
+ request.base_url,
26
+ ->(**args) { url_for(**args) },
27
+ issuer: issuer,
28
+ oidc_metadata: oidc_metadata,
29
+ )
30
+ end
31
+
32
+ # This controller is reachable through Doorkeeper's own routes, so it
33
+ # also serves apps that configure this gem without mounting
34
+ # `use_doorkeeper_openid_connect` (or that skip routes through its
35
+ # mapping) — where the gem's URL helpers are not defined. Rather than
36
+ # raise, or advertise an OIDC document missing its core endpoints, fall
37
+ # back to the plain Doorkeeper document.
38
+ def oidc_routes_mounted?
39
+ endpoint_defined?(:oauth_userinfo_url) &&
40
+ endpoint_defined?(:oauth_discovery_keys_url)
41
+ end
42
+
43
+ def oidc_metadata
44
+ openid_connect = Doorkeeper::OpenidConnect.configuration
45
+
46
+ {
47
+ userinfo_endpoint: endpoint_url(:oauth_userinfo_url, userinfo_url_options),
48
+ jwks_uri: endpoint_url(:oauth_discovery_keys_url, jwks_url_options),
49
+ end_session_endpoint: instance_exec(&openid_connect.end_session_endpoint),
50
+ registration_endpoint: registration_endpoint_url(openid_connect),
51
+ subject_types_supported: openid_connect.subject_types_supported,
52
+ id_token_signing_alg_values_supported: [::Doorkeeper::OpenidConnect.signing_algorithm],
53
+ claim_types_supported: ["normal"],
54
+ claims_supported: claims_supported(openid_connect),
55
+ }.compact
56
+ end
57
+
58
+ def registration_endpoint_url(openid_connect)
59
+ return unless openid_connect.dynamic_client_registration
60
+ # The registration route is drawn conditionally on the configuration
61
+ # at the time the app's routes were loaded, so it can be absent even
62
+ # while `dynamic_client_registration` is enabled.
63
+ return unless endpoint_defined?(:oauth_dynamic_client_registration_url)
64
+
65
+ endpoint_url(
66
+ :oauth_dynamic_client_registration_url,
67
+ dynamic_client_registration_url_options,
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -6,6 +6,7 @@ module Doorkeeper
6
6
  include Doorkeeper::Helpers::Controller
7
7
  include GrantTypesSupportedMixin
8
8
  include TokenEndpointAuthMethodsSupportedMixin
9
+ include DiscoveryHelpersMixin
9
10
 
10
11
  WEBFINGER_RELATION = "http://openid.net/specs/connect/1.0/issuer"
11
12
 
@@ -14,7 +15,11 @@ module Doorkeeper
14
15
  end
15
16
 
16
17
  def webfinger
17
- render json: webfinger_response
18
+ # RFC 7033 §5: the WebFinger resource must be queryable from browser
19
+ # scripts on other origins, §10.2: JRD responses use the dedicated
20
+ # `application/jrd+json` media type.
21
+ response.headers["Access-Control-Allow-Origin"] = "*"
22
+ render json: webfinger_response, content_type: "application/jrd+json"
18
23
  end
19
24
 
20
25
  def keys
@@ -29,18 +34,17 @@ module Doorkeeper
29
34
 
30
35
  {
31
36
  issuer: issuer,
32
- authorization_endpoint: oauth_authorization_url(authorization_url_options),
33
- token_endpoint: oauth_token_url(token_url_options),
34
- revocation_endpoint: oauth_revoke_url(revocation_url_options),
35
- introspection_endpoint: respond_to?(:oauth_introspect_url) ? oauth_introspect_url(introspection_url_options) : nil,
36
- userinfo_endpoint: oauth_userinfo_url(userinfo_url_options),
37
- jwks_uri: oauth_discovery_keys_url(jwks_url_options),
37
+ authorization_endpoint: endpoint_url(:oauth_authorization_url, authorization_url_options),
38
+ token_endpoint: endpoint_url(:oauth_token_url, token_url_options),
39
+ revocation_endpoint: endpoint_url(:oauth_revoke_url, revocation_url_options),
40
+ introspection_endpoint: endpoint_defined?(:oauth_introspect_url) ? endpoint_url(:oauth_introspect_url, introspection_url_options) : nil,
41
+ userinfo_endpoint: endpoint_url(:oauth_userinfo_url, userinfo_url_options),
42
+ jwks_uri: endpoint_url(:oauth_discovery_keys_url, jwks_url_options),
38
43
  end_session_endpoint: instance_exec(&openid_connect.end_session_endpoint),
39
- registration_endpoint: openid_connect.dynamic_client_registration ? oauth_dynamic_client_registration_url(dynamic_client_registration_url_options) : nil,
44
+ registration_endpoint: openid_connect.dynamic_client_registration ? endpoint_url(:oauth_dynamic_client_registration_url, dynamic_client_registration_url_options) : nil,
40
45
 
41
46
  scopes_supported: doorkeeper.scopes,
42
47
 
43
- # TODO: support id_token response type
44
48
  response_types_supported: doorkeeper.authorization_response_types,
45
49
  response_modes_supported: response_modes_supported(doorkeeper),
46
50
  grant_types_supported: grant_types_supported(doorkeeper),
@@ -50,6 +54,12 @@ module Doorkeeper
50
54
  # 'private_key_jwt'
51
55
  token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
52
56
 
57
+ # RFC 9207: mirrors Doorkeeper's own RFC 8414 document — true exactly
58
+ # when Doorkeeper is configured with an issuer and therefore emits the
59
+ # `iss` authorization response parameter. `false` survives the
60
+ # `.compact` below, so it is advertised explicitly.
61
+ authorization_response_iss_parameter_supported: Doorkeeper::OpenidConnect.doorkeeper_issuer.present?,
62
+
53
63
  subject_types_supported: openid_connect.subject_types_supported,
54
64
 
55
65
  id_token_signing_alg_values_supported: [
@@ -64,13 +74,7 @@ module Doorkeeper
64
74
  # 'distributed',
65
75
  ],
66
76
 
67
- claims_supported: %w[
68
- iss
69
- sub
70
- aud
71
- exp
72
- iat
73
- ] | openid_connect.claims.to_h.keys,
77
+ claims_supported: claims_supported(openid_connect),
74
78
 
75
79
  code_challenge_methods_supported: code_challenge_methods_supported(doorkeeper),
76
80
  }.compact
@@ -105,32 +109,6 @@ module Doorkeeper
105
109
  def keys_response
106
110
  { keys: Doorkeeper::OpenidConnect.signing_keys_normalized }
107
111
  end
108
-
109
- def protocol
110
- configured = Doorkeeper::OpenidConnect.configuration.protocol
111
- configured.respond_to?(:call) ? configured.call : configured
112
- end
113
-
114
- def discovery_url_options
115
- Doorkeeper::OpenidConnect.configuration.discovery_url_options.call(request)
116
- end
117
-
118
- def discovery_url_default_options
119
- {
120
- protocol: protocol,
121
- }
122
- end
123
-
124
- def issuer
125
- Doorkeeper::OpenidConnect.resolve_issuer(request: request)
126
- end
127
-
128
- %i[authorization token revocation introspection userinfo jwks
129
- dynamic_client_registration].each do |endpoint|
130
- define_method :"#{endpoint}_url_options" do
131
- discovery_url_default_options.merge(discovery_url_options[endpoint.to_sym] || {})
132
- end
133
- end
134
112
  end
135
113
  end
136
114
  end
@@ -13,11 +13,13 @@ module Doorkeeper
13
13
  return
14
14
  end
15
15
 
16
- client = Doorkeeper::Application.create!(application_params(registration))
16
+ client = Doorkeeper.configuration.application_model.create!(application_params(registration))
17
17
  render json: registration_response(client, registration), status: :created
18
18
  rescue ActiveRecord::RecordInvalid => e
19
- render json: { error: "invalid_client_params", error_description: e.record.errors.full_messages.join(", ") },
20
- status: :bad_request
19
+ render json: {
20
+ error: registration_error_code(e.record),
21
+ error_description: e.record.errors.full_messages.join(", "),
22
+ }, status: :bad_request
21
23
  end
22
24
 
23
25
  private
@@ -47,19 +49,43 @@ module Doorkeeper
47
49
  end
48
50
  end
49
51
 
52
+ # RFC 7591 §3.2.2 registration error codes: `invalid_redirect_uri` for
53
+ # redirect URI problems, `invalid_client_metadata` for everything else.
54
+ def registration_error_code(record)
55
+ record.errors.include?(:redirect_uri) ? "invalid_redirect_uri" : "invalid_client_metadata"
56
+ end
57
+
50
58
  def application_params(registration)
51
- {
59
+ application_params = {
52
60
  name: params[:client_name],
53
61
  redirect_uri: params[:redirect_uris] || [],
54
62
  scopes: registration.permitted_scopes,
55
63
  confidential: registration.confidential_client?,
56
64
  }
65
+
66
+ # Existing installations may not have run the migration that adds the
67
+ # `post_logout_redirect_uris` column yet. RFC 7591 §2 allows the server
68
+ # to ignore client metadata it does not understand, so the parameter is
69
+ # simply dropped in that case instead of failing the registration.
70
+ if post_logout_redirect_uris_supported?
71
+ application_params[:post_logout_redirect_uris] = params[:post_logout_redirect_uris] || []
72
+ end
73
+
74
+ application_params
75
+ end
76
+
77
+ def post_logout_redirect_uris_supported?
78
+ Doorkeeper.config.application_model.column_names.include?("post_logout_redirect_uris")
57
79
  end
58
80
 
59
81
  def registration_response(doorkeeper_application, registration)
60
82
  response = {
61
83
  client_id: doorkeeper_application.uid,
62
84
  client_id_issued_at: doorkeeper_application.created_at.to_i,
85
+ # RFC 7591 §3.2.1: the response must include all registered client
86
+ # metadata. The name is always present on a successfully created
87
+ # application (the model validates its presence).
88
+ client_name: doorkeeper_application.name,
63
89
  redirect_uris: doorkeeper_application.redirect_uri.split,
64
90
  token_endpoint_auth_method: registration.token_endpoint_auth_method,
65
91
  token_endpoint_auth_methods_supported: registration.token_endpoint_auth_methods_supported,
@@ -69,6 +95,11 @@ module Doorkeeper
69
95
  application_type: registration.requested_application_type,
70
96
  }
71
97
 
98
+ # Registration is optional (RP-Initiated Logout §2), so the metadata
99
+ # field is only echoed when post-logout redirect URIs were registered.
100
+ post_logout_uris = doorkeeper_application.post_logout_redirect_uris
101
+ response[:post_logout_redirect_uris] = post_logout_uris if post_logout_uris.present?
102
+
72
103
  if registration.confidential_client?
73
104
  response[:client_secret] =
74
105
  doorkeeper_application.plaintext_secret || doorkeeper_application.secret
@@ -6,7 +6,27 @@ module Doorkeeper
6
6
  before_action -> { doorkeeper_authorize! :openid }
7
7
 
8
8
  def show
9
- render json: Doorkeeper::OpenidConnect::UserInfo.new(doorkeeper_token), status: :ok
9
+ user_info = Doorkeeper::OpenidConnect.configuration.user_info_model.new(doorkeeper_token)
10
+
11
+ # A token can carry the openid scope yet resolve to no end user — a
12
+ # client_credentials token, or an owner deleted after issuance. The
13
+ # UserInfo response is a statement about the End-User, so such a token
14
+ # is not valid at this endpoint: answer with RFC 6750's 401
15
+ # invalid_token instead of letting the claim generators dereference a
16
+ # nil owner (500).
17
+ if user_info.respond_to?(:resource_owner) && user_info.resource_owner.nil?
18
+ render_resource_owner_missing_error
19
+ else
20
+ render json: user_info, status: :ok
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def render_resource_owner_missing_error
27
+ error = Doorkeeper::OAuth::InvalidTokenResponse.from_access_token(doorkeeper_token)
28
+ response.headers.merge!(error.headers)
29
+ render json: error.body, status: error.status
10
30
  end
11
31
  end
12
32
  end
@@ -1,4 +1,22 @@
1
1
  en:
2
+ activerecord:
3
+ attributes:
4
+ doorkeeper/application:
5
+ post_logout_redirect_uris: 'Post Logout Redirect URIs'
6
+ errors:
7
+ models:
8
+ doorkeeper/application:
9
+ attributes:
10
+ # Mirror Doorkeeper's own redirect_uri error messages so that an
11
+ # invalid post_logout_redirect_uris value (validated through
12
+ # Doorkeeper::RedirectUriValidator) produces a readable message.
13
+ post_logout_redirect_uris:
14
+ fragment_present: 'cannot contain a fragment.'
15
+ invalid_uri: 'must be a valid URI.'
16
+ unspecified_scheme: 'must specify a scheme.'
17
+ relative_uri: 'must be an absolute URI.'
18
+ secured_uri: 'must be an HTTPS/SSL URI.'
19
+ forbidden_uri: 'is forbidden by the server.'
2
20
  doorkeeper:
3
21
  scopes:
4
22
  openid: 'Authenticate your account'
@@ -12,11 +12,7 @@ module Doorkeeper
12
12
 
13
13
  def authorize
14
14
  @auth = Authorization::Token.new(pre_auth, resource_owner)
15
- if @auth.respond_to?(:issue_token!)
16
- @auth.issue_token!
17
- else
18
- @auth.issue_token
19
- end
15
+ @auth.issue_token!
20
16
  response
21
17
  end
22
18
 
@@ -28,7 +24,8 @@ module Doorkeeper
28
24
  private
29
25
 
30
26
  def response
31
- id_token = Doorkeeper::OpenidConnect::IdToken.new(auth.token, pre_auth.nonce)
27
+ id_token = Doorkeeper::OpenidConnect.configuration.id_token_model
28
+ .new(auth.token, pre_auth.nonce)
32
29
 
33
30
  IdTokenResponse.new(pre_auth, auth, id_token)
34
31
  end
@@ -18,16 +18,40 @@ module Doorkeeper
18
18
  true
19
19
  end
20
20
 
21
+ # Expose the issued access token, mirroring core `CodeResponse` /
22
+ # `TokenResponse`. `after_successful_authorization` hooks read
23
+ # `context.issued_token` (via `Hooks::Context#issued_token`), which
24
+ # would otherwise raise `NoMethodError` for the `id_token` and
25
+ # `id_token token` response types.
26
+ def issued_token
27
+ auth.token
28
+ end
29
+
21
30
  def body
22
31
  {
23
32
  state: pre_auth.state,
24
33
  id_token: id_token.as_jws_token,
25
- }
34
+ }.merge(iss_parameter)
26
35
  end
27
36
 
28
37
  def redirect_uri
29
38
  Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body)
30
39
  end
40
+
41
+ private
42
+
43
+ # RFC 9207 Authorization Server Issuer Identification. Emitted only when
44
+ # Doorkeeper itself is configured with an issuer, mirroring the gating of
45
+ # Doorkeeper's CodeResponse and its advertised
46
+ # `authorization_response_iss_parameter_supported` metadata flag. The
47
+ # value is this response's ID Token `iss` claim: RFC 9207 §2 requires the
48
+ # `iss` parameter to be identical to the `iss` claim of an ID Token
49
+ # returned from the authorization endpoint.
50
+ def iss_parameter
51
+ return {} if Doorkeeper::OpenidConnect.doorkeeper_issuer.blank?
52
+
53
+ { iss: id_token.issuer }
54
+ end
31
55
  end
32
56
  end
33
57
  end
@@ -6,7 +6,12 @@ module Doorkeeper
6
6
  private
7
7
 
8
8
  def response
9
- id_token_token = Doorkeeper::OpenidConnect::IdTokenToken.new(auth.token, pre_auth.nonce)
9
+ # `extend` is a no-op when the concern is already in the ancestry
10
+ # (e.g. a custom id_token_class that includes it), so the same path
11
+ # serves the default and any custom id_token_class alike.
12
+ id_token_token = Doorkeeper::OpenidConnect.configuration.id_token_model
13
+ .new(auth.token, pre_auth.nonce)
14
+ .extend(Doorkeeper::OpenidConnect::HybridIdTokenConcern)
10
15
 
11
16
  IdTokenTokenResponse.new(pre_auth, auth, id_token_token)
12
17
  end
@@ -5,7 +5,11 @@ module Doorkeeper
5
5
  class IdTokenTokenResponse < IdTokenResponse
6
6
  def body
7
7
  super.merge({
8
- access_token: auth.token.token,
8
+ # `plaintext_token`, not `token`: with a hashing token-secret
9
+ # strategy the stored `token` attribute is the hash, so returning it
10
+ # would hand the client an unusable access token (mirrors core
11
+ # `CodeResponse` / `TokenResponse`, which return `plaintext_token`).
12
+ access_token: auth.token.plaintext_token,
9
13
  token_type: auth.token.token_type,
10
14
  expires_in: auth.token.expires_in_seconds,
11
15
  })
@@ -21,7 +21,11 @@ module Doorkeeper
21
21
 
22
22
  def initialize(options = {})
23
23
  @name = options[:name].to_sym
24
- @response = Array.wrap(options[:response]).freeze
24
+ # Symbolize so a claim configured with string responses
25
+ # (`response: ["id_token"]`) matches the symbol tokens the builder
26
+ # dispatches with (`:id_token` / `:user_info`); otherwise such a
27
+ # claim would be silently dropped from every response.
28
+ @response = Array.wrap(options[:response]).compact.map(&:to_sym).freeze
25
29
  @scopes = normalize_scopes(options[:scope])
26
30
 
27
31
  # use default scope for Standard Claims, fallback to profile
@@ -5,8 +5,17 @@ require "ostruct"
5
5
  module Doorkeeper
6
6
  module OpenidConnect
7
7
  class ClaimsBuilder
8
- def self.generate(access_token, response)
9
- resource_owner = Doorkeeper::OpenidConnect.configuration.resource_owner_from_access_token.call(access_token)
8
+ # Sentinel distinct from any real resource owner (including `nil`) so a
9
+ # caller that already resolved the owner can pass it in — even a nil one —
10
+ # and skip the second `resource_owner_from_access_token` lookup.
11
+ NO_RESOURCE_OWNER = Object.new
12
+ private_constant :NO_RESOURCE_OWNER
13
+
14
+ def self.generate(access_token, response, resource_owner = NO_RESOURCE_OWNER)
15
+ if resource_owner.equal?(NO_RESOURCE_OWNER)
16
+ resource_owner =
17
+ Doorkeeper::OpenidConnect.configuration.resource_owner_from_access_token.call(access_token)
18
+ end
10
19
 
11
20
  Doorkeeper::OpenidConnect.configuration.claims.to_h.map do |name, claim|
12
21
  if claim.scopes.any? { |scope| access_token.scopes.exists?(scope) } &&