# Doorkeeper::OpenidConnect

[![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)
[![Maintainability](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-openid_connect/maintainability.svg)](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-openid_connect)
[![Gem Version](https://badge.fury.io/rb/doorkeeper-openid_connect.svg)](https://rubygems.org/gems/doorkeeper-openid_connect)

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.

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/).

## Table of Contents

- [Status](#status)
  - [Known Issues](#known-issues)
  - [Example Applications](#example-applications)
- [Installation](#installation)
- [Configuration](#configuration)
  - [Scopes](#scopes)
  - [Claims](#claims)
  - [Routes](#routes)
  - [Nonces](#nonces)
  - [Internationalization (I18n)](#internationalization-i18n)
  - [Dynamic Client Registration](#dynamic-client-registration)
- [Development](#development)
- [License](#license)
- [Sponsors](#sponsors)

## Status

The following parts of [OpenID Connect Core 1.0](http://openid.net/specs/openid-connect-core-1_0.html) are currently supported:
- [Authentication using the Authorization Code Flow](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
- [Authentication using the Implicit Flow](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth)
- [Requesting Claims using Scope Values](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
- [UserInfo Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
- [Normal Claims](http://openid.net/specs/openid-connect-core-1_0.html#NormalClaims)
- [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
- [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)

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.

Take a look at the [DiscoveryController](app/controllers/doorkeeper/openid_connect/discovery_controller.rb) for more details on supported features.

### Known Issues

- Doorkeeper's API mode (`Doorkeeper.configuration.api_only`) is not properly supported yet

### Example Applications

- [GitLab](https://gitlab.com/gitlab-org/gitlab-ce) ([original MR](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8018))
- [Testing app for this gem](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/tree/master/spec/dummy)

## Installation

Make sure your application is already set up with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper#installation).

Add this line to your application's `Gemfile` and run `bundle install`:

```ruby
gem 'doorkeeper-openid_connect'
```

Run the installation generator to update routes and create the initializer:

```sh
rails generate doorkeeper:openid_connect:install
```

Generate a migration for Active Record (other ORMs are currently not supported):

```sh
rails generate doorkeeper:openid_connect:migration
rake db:migrate
```

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)
wiki and [CHANGELOG.md](CHANGELOG.md) for upgrade instructions.

## Configuration

Make sure you've [configured Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper#configuration) before continuing.

Verify your settings in `config/initializers/doorkeeper.rb`:

- `resource_owner_authenticator`
  - This callback needs to returns a falsey value if the current user can't be determined:

    ```ruby
    resource_owner_authenticator do
      if current_user
        current_user
      else
        redirect_to(new_user_session_url)
        nil
      end
    end
    ```
- `grant_flows`
  - If you want to use `id_token` or `id_token token` response types you need to add `implicit_oidc` to `grant_flows`:

    ```ruby
    grant_flows %w(authorization_code implicit_oidc)
    ```

The following settings are required in `config/initializers/doorkeeper_openid_connect.rb`:

- `issuer`
  - 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.
  - You can either pass a string value, or a block to generate the issuer dynamically. The block receives `resource_owner`, `application`, and `request` so that the same configuration can serve both ID token issuance (where `resource_owner` and `application` are available) and the discovery endpoint (where only `request` is available):

    ```ruby
    # config/initializers/doorkeeper_openid_connect.rb
    Doorkeeper::OpenidConnect.configure do
      # ...
      issuer do |resource_owner, application, request|
        request&.base_url || "https://default.example.com"
      end
    end
    ```

  - For backward compatibility, blocks with arity 0, 1, or 2 are also accepted. An arity-1 block receives `request` from the discovery endpoint and `resource_owner` from the ID token context, while an arity-2 block always receives `resource_owner` and `application`.
- `subject`
  - 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.
  - The database ID of the user is an acceptable choice if you don't mind leaking that information.
  - 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.

    ```ruby
    # config/initializers/doorkeeper_openid_connect.rb
    Doorkeeper::OpenidConnect.configure do
    # ...
      subject_types_supported [:pairwise]

      subject do |resource_owner, application|
        Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}")
      end
    # ...
    end
    ```

- `signing_key`
  - Private key to be used for [JSON Web Signature](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31).
  - 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).
  - 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[...]`).
- `signing_algorithm`
  - The signing algorithm used for the ID token, which defaults to `:rs256`. The list of supported algorithms can be found [here](https://github.com/jwt/ruby-jwt#algorithms-and-usage)
- `resource_owner_from_access_token`
  - Defines how to translate the Doorkeeper access token to a resource owner model.

> [!Note]
> Both `signing_key` and `signing_algorithm` also accept callable objects (e.g. a lambda), which are evaluated on each call — useful for multi-tenant setups where the key or algorithm varies per request:
>
> ```ruby
> signing_key -> { current_tenant.private_key }
> signing_algorithm -> { current_tenant.algorithm }
> ```

> [!Note]
> `signing_key` also accepts an array for key rotation. The first entry is the active key used to sign newly issued ID tokens; the remaining entries are still published in the JWKS so clients can validate tokens signed with a retired key during a rotation window. Callable forms returning an array are also supported.
>
> ```ruby
> signing_key [
>   File.read("config/keys/current.pem"),  # active, signs new tokens
>   File.read("config/keys/previous.pem"), # retired, exposed in JWKS only
> ]
> ```

The following settings are optional, but recommended for better client compatibility:

- `auth_time_from_resource_owner`
  - Returns the time of the user's last login, this can be a `Time`, `DateTime`, or any other class that responds to `to_i`
  - Used to populate the `auth_time` claim on the ID Token.
  - Used as a fallback for `max_age` enforcement when `auth_time_from_session` is not configured. **Note:** for multi-session deployments this is insecure (it returns the most recent login on _any_ device), and emits a deprecation warning — prefer `auth_time_from_session` below.
- `auth_time_from_session`
  - Returns the time the user authenticated for the *current* session. Required for correct `max_age` enforcement when the same user can hold multiple concurrent sessions (e.g. PC + smartphone) — `auth_time_from_resource_owner` cannot distinguish between sessions and would let a stale session inherit a fresh login from another device.
  - The block is executed in the controller's scope and receives `(session, request)`. Return value can be a `Time`, `DateTime`, or anything responding to `to_i`. Return `nil` to force reauthentication.

    ```ruby
    # Example: capture auth_time on the session at login,
    # and surface it here for the OIDC max_age check.
    auth_time_from_session do |session, _request|
      session[:auth_time]
    end
    ```
- `reauthenticate_resource_owner`
  - 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).
  - Required to support the `max_age` and `prompt=login` parameters.
  - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.
- `select_account_for_resource_owner`
  - Defines how to trigger account selection to choose the current login user.
  - Required to support the `prompt=select_account` parameter.
  - The block is executed in the controller's scope, so you have access to methods like `params`, `redirect_to` etc.

The following settings are optional:

- `expiration`
  - Expiration time after which the ID Token must not be accepted for processing by clients.
  - The default is 120 seconds, it can be configured using a value or block.
    ```ruby
    # config/initializers/doorkeeper_openid_connect.rb
    Doorkeeper::OpenidConnect.configure do
      # ...
      expiration do |resource_owner, application|
        # You will have to ensure the application model implements an expiration method
        application.expiration
      end
      # ...
    end
    ```

- `protocol`
  - The protocol to use when generating URIs for the discovery endpoints.
  - The default is `https` for production, and `http` for all other environments
  - Note that the OIDC specification mandates HTTPS, so you shouldn't change this
    for production environments unless you have a really good reason!

- `end_session_endpoint`
  - The URL that the user is redirected to after ending the session on the client.
  - Used by implementations like https://github.com/IdentityModel/oidc-client-js.
  - The block is executed in the controller's scope, so you have access to your route helpers.

- `discovery_url_options`
  - The URL options for every available endpoint to use when generating the endpoint URL in the
    discovery response. Available endpoints: `authorization`, `token`, `revocation`,
    `introspection`, `userinfo`, `jwks`, `dynamic_client_registration`.
  - This option requires option keys with an available endpoint and
    [URL options](https://api.rubyonrails.org/v6.0.3.3/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for)
    as value.
  - The default is to use the request host, just like all the other URLs in the discovery response.
  - This is useful when you want endpoints to use a different URL than other requests.
    For example, if your Doorkeeper server is behind a firewall with other servers, you might want
    other servers to use an "internal" URL to communicate with Doorkeeper, but you want to present
    an "external" URL to end-users for authentication requests. Note that this setting does not
    actually change the URL that your Doorkeeper server responds on - that is outside the scope of
    Doorkeeper.

    ```ruby
    # config/initializers/doorkeeper_openid_connect.rb
    Doorkeeper::OpenidConnect.configure do
    # ...
      discovery_url_options do |request|
        {
          authorization: { host: 'host.example.com' },
          jwks:          { protocol: request.ssl? ? :https : :http }
        }
      end
    # ...
    end
    ```

- `apply_prompt_to_non_oidc_requests`
  - Whether to honor the `prompt` authorization parameter (`none`, `login`, `consent`, `select_account`) on plain OAuth requests that do not include the `openid` scope.
  - Defaults to `false`, which preserves the historical behavior of silently ignoring `prompt` outside of OIDC requests.
  - `max_age` enforcement remains OIDC-only regardless of this option, since it is defined by OIDC Core.

    ```ruby
    # config/initializers/doorkeeper_openid_connect.rb
    Doorkeeper::OpenidConnect.configure do
      # ...
      apply_prompt_to_non_oidc_requests true
      # ...
    end
    ```

### Scopes

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.

> 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.
>
> See [Using Scopes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes) in the Doorkeeper wiki for more information.

#### `offline_access`

Per [OIDC Core §11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess), the `offline_access` scope signals that the client wants a refresh token so it can access the user's resources while the user is offline. Doorkeeper's existing `use_refresh_token` block already covers the basic flow — issue a refresh token only when the client actually asked for `offline_access`:

```ruby
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  optional_scopes :openid, :offline_access

  # Issue a refresh token only when the client requests offline_access
  use_refresh_token do |context|
    context.scopes.exists?("offline_access")
  end
end
```

> **Note:** This does not automatically enforce [OIDC Core §11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess)'s strict requirements — for example, the OP MUST ignore `offline_access` unless `prompt=consent` is present and `response_type` returns an Authorization Code. If you need that level of enforcement, filter the scope in your `use_refresh_token` block or authorization controller override.

### Claims

Claims can be defined in a `claims` block inside `config/initializers/doorkeeper_openid_connect.rb`:

```ruby
Doorkeeper::OpenidConnect.configure do
  claims do
    claim :email do |resource_owner|
      resource_owner.email
    end

    claim :full_name do |resource_owner|
      "#{resource_owner.first_name} #{resource_owner.last_name}"
    end

    claim :preferred_username, scope: :openid do |resource_owner, scopes, access_token|
      # Pass the resource_owner's preferred_username if the application has
      # `profile` scope access. Otherwise, provide a more generic alternative.
      scopes.exists?(:profile) ? resource_owner.preferred_username : "summer-sun-9449"
    end

    claim :groups, response: [:id_token, :user_info] do |resource_owner|
      resource_owner.groups
    end
  end
end
```

Each claim block will be passed:

- the `resource_owner`, which is the return value of `resource_owner_authenticator` in your initializer
- the `scopes` granted by the access token, which is an instance of `Doorkeeper::OAuth::Scopes`
- the `access_token` itself, which is an instance of `Doorkeeper::AccessToken`

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.

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.

You can also pass an array of scopes, in which case the claim is returned whenever the access token grants any of the listed scopes. This is useful when you want to expose the same claim under both a standard scope and an aggregate scope:

``` ruby
claim :given_name, scope: [:profile, :all_data] do |user|
  user.first_name
end

claim :email, scope: [:email, :all_data] do |user|
  user.email
end
```

#### Authentication Context (`acr`) and Methods (`amr`)

The `claim` DSL also handles standard top-level ID Token claims such as [`acr`](http://openid.net/specs/openid-connect-core-1_0.html#IDToken) (Authentication Context Class Reference) and [`amr`](https://www.rfc-editor.org/rfc/rfc8176) (Authentication Methods References) — commonly used to expose MFA status to clients:

```ruby
claims do
  claim :acr, response: [:id_token, :user_info], scope: :openid do |resource_owner|
    # Single string — e.g. a URI like "urn:mace:incommon:iap:silver",
    # or a numeric Level of Assurance "1".."4" per ISO/IEC 29115.
    resource_owner.mfa_enabled? ? "2" : "1"
  end

  claim :amr, response: [:id_token, :user_info], scope: :openid do |resource_owner|
    # Array of strings, per RFC 8176.
    methods = ["pwd"]
    methods << "mfa" if resource_owner.mfa_enabled?
    methods << "otp" if resource_owner.last_login_used_totp?
    methods
  end
end
```

Two defaults are worth calling out because they bite silently:

- **`response: [:id_token, :user_info]`** — custom claims default to UserInfo only, but relying parties usually expect `acr` / `amr` on the ID Token.
- **`scope: :openid`** — without it, non-standard claims fall back to the `profile` scope and disappear for clients that only requested `openid`.

Claim names you declare here are automatically advertised under `claims_supported` in the discovery document. The list of advertised `acr` values (`acr_values_supported`) is not currently generated.


### Routes

The installation generator will update your `config/routes.rb` to define all required routes:

``` ruby
Rails.application.routes.draw do
  use_doorkeeper_openid_connect
  # your routes
end
```

This will mount the following routes:

```
GET   /oauth/userinfo
POST  /oauth/userinfo
GET   /oauth/discovery/keys
GET   /.well-known/openid-configuration
GET   /.well-known/oauth-authorization-server
GET   /.well-known/webfinger
```

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).

#### Customizing the `jwks_uri` path in the discovery document

[`discovery_url_options`](#configuration) lets you tweak the host, protocol, or port of the published `jwks_uri`, but not the path itself. To advertise a custom path — while keeping `/oauth/discovery/keys` working for existing clients during a rollover — mount the discovery controller at the new path and re-point the `oauth_discovery_keys_url` helper at it via [`direct`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/CustomUrlHelpers.html#method-i-direct):

```ruby
# config/routes.rb
Rails.application.routes.draw do
  use_doorkeeper_openid_connect

  # 1. Mount the custom path under a non-conflicting helper name
  get "/-/jwks",
      to: "doorkeeper/openid_connect/discovery#keys",
      as: :custom_jwks

  # 2. Re-point oauth_discovery_keys_url at the new path
  direct(:oauth_discovery_keys) { |opts| custom_jwks_url(opts) }
end
```

After this, `.well-known/openid-configuration` returns `"jwks_uri": "https://example.com/-/jwks"`, and the original `/oauth/discovery/keys` route still responds (handy during a rollover).

> [!Note]
> A naive `match "/-/jwks", ..., as: :oauth_discovery_keys` won't work — Rails has refused to reuse a route name [since 4.0](https://github.com/rails/rails/commit/a2b7c0e69d) and raises `ArgumentError: Invalid route name, already in use: 'oauth_discovery_keys'`. The `direct` helper sidesteps this by overriding the URL helper itself rather than re-declaring the route name.

#### Mounting under multiple namespaces (multiple resource owner models)

If your app authenticates more than one kind of resource owner (e.g. a `User`
and a `Customer` Devise model) you may want to mount Doorkeeper — and this engine
— more than once, each under its own namespace:

```ruby
# config/routes.rb
Rails.application.routes.draw do
  scope :users, as: :users do
    use_doorkeeper { controllers authorizations: "users/authorizations" }
    use_doorkeeper_openid_connect
  end

  scope :customers, as: :customers do
    use_doorkeeper { controllers authorizations: "customers/authorizations" }
    use_doorkeeper_openid_connect
  end
end
```

Most of the request flow is model-agnostic: the `resource_owner_authenticator`
block can dispatch on whichever owner is signed in (`current_user ||
current_customer`), and claims / userinfo / ID token generation follow from
whatever it returns.

The one piece that needs attention is the **discovery document**.
[`DiscoveryController#provider_response`](app/controllers/doorkeeper/openid_connect/discovery_controller.rb)
builds the published endpoints by calling named route helpers
(`oauth_authorization_url`, `oauth_token_url`, …) directly. The
[`discovery_url_options`](#configuration) setting (added in #126) lets you
override the `host` / `protocol` / `port` of those URLs, but not *which* named
helper is resolved — so under multiple mounts every namespace's discovery
document would point at the same set of endpoints.

The idiomatic fix is to subclass the discovery controller per namespace and
re-point the helper calls at that namespace's routes:

```ruby
# app/controllers/users/discovery_controller.rb
module Users
  class DiscoveryController < Doorkeeper::OpenidConnect::DiscoveryController
    private

    # Re-point each helper used by `provider_response` at the namespaced route.
    def oauth_authorization_url(opts = {})
      users_oauth_authorization_url(opts)
    end

    def oauth_token_url(opts = {})
      users_oauth_token_url(opts)
    end

    def oauth_revoke_url(opts = {})
      users_oauth_revoke_url(opts)
    end

    def oauth_userinfo_url(opts = {})
      users_oauth_userinfo_url(opts)
    end

    def oauth_discovery_keys_url(opts = {})
      users_oauth_discovery_keys_url(opts)
    end

    # ...and `oauth_introspect_url` / `oauth_dynamic_client_registration_url`
    # if you advertise those.
  end
end
```

```ruby
# config/routes.rb (inside the `:users` scope)
get "/.well-known/openid-configuration",
    to: "users/discovery#provider", as: :users_openid_connect_config
```

Repeat for the `customers` namespace. Each `.well-known/openid-configuration`
then advertises the endpoints for its own namespace.

> [!Note]
> See [#192](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/192)
> for the original discussion. First-class multi-mount support is not provided
> out of the box; the per-namespace controller override above is the supported
> extension pattern for now.

### Nonces

To support clients who send nonces you have to tweak Doorkeeper's authorization view so the parameter is passed on.

If you don't already have custom templates, run this generator in your Rails application to add them:

```sh
rails generate doorkeeper:views
```

Then tweak the template as follows:

```patch
--- i/app/views/doorkeeper/authorizations/new.html.erb
+++ w/app/views/doorkeeper/authorizations/new.html.erb
@@ -26,6 +26,7 @@
       <%= hidden_field_tag :state, @pre_auth.state %>
       <%= hidden_field_tag :response_type, @pre_auth.response_type %>
       <%= hidden_field_tag :scope, @pre_auth.scope %>
+      <%= hidden_field_tag :nonce, @pre_auth.nonce %>
       <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %>
     <% end %>
     <%= form_tag oauth_authorization_path, method: :delete do %>
@@ -34,6 +35,7 @@
       <%= hidden_field_tag :state, @pre_auth.state %>
       <%= hidden_field_tag :response_type, @pre_auth.response_type %>
       <%= hidden_field_tag :scope, @pre_auth.scope %>
+      <%= hidden_field_tag :nonce, @pre_auth.nonce %>
       <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %>
     <% end %>
   </div>
```

### Internationalization (I18n)

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`.

### Dynamic Client Registration

This gem supports [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) based on [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591).

To enable dynamic client registration, add the following to `config/initializers/doorkeeper_openid_connect.rb`:

```ruby
Doorkeeper::OpenidConnect.configure do
  # ...
  dynamic_client_registration true
  # ...
end
```

This exposes a `POST /oauth/registration` endpoint where OAuth clients can register themselves.

#### Supported parameters

The registration endpoint currently accepts the following [RFC 7591 §2](https://www.rfc-editor.org/rfc/rfc7591#section-2) parameters:

| Parameter | Description |
| --- | --- |
| `client_name` | Human-readable name of the client |
| `redirect_uris` | Array of redirection URIs |
| `scope` | Space-delimited list of requested scopes |
| `token_endpoint_auth_method` | Requested authentication method. Defaults to `client_secret_basic`. `none` is always allowed (and registers a public client); other allowed values depend on the host application's Doorkeeper `client_credentials_methods` configuration. Unsupported values are rejected with `invalid_client_metadata`. |
| `application_type` | Client type: `web` (default) or `native`, per [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html). Unsupported values are rejected with `invalid_client_metadata`. |
| `response_types` | Array of OAuth 2.0 response types the client will use (e.g. `["code"]`). Must be a subset of the server's supported response types. Defaults to the server's full set when omitted. |
| `grant_types` | Array of OAuth 2.0 grant types the client will use (e.g. `["authorization_code"]`). Must be a subset of the server's supported grant types. Defaults to the server's full set when omitted. |

When `token_endpoint_auth_method` is set to `none`, the client is registered as **public** (i.e. `confidential: false`). For all other values — or when the parameter is omitted — the client is registered as **confidential**, matching the RFC 7591 default of `client_secret_basic`.

Other RFC 7591 parameters (e.g. `client_uri`, `logo_uri`, `contacts`) require schema additions to `oauth_applications` and are not yet supported.

#### Authorization

By default, the registration endpoint is open to any request. To require authorization (e.g. an Initial Access Token per [RFC 7591 §3.1](https://www.rfc-editor.org/rfc/rfc7591#section-3.1)), configure `authorize_dynamic_client_registration`:

```ruby
Doorkeeper::OpenidConnect.configure do
  # ...
  dynamic_client_registration true
  authorize_dynamic_client_registration do
    provided = request.headers["Authorization"].to_s
    expected = "Bearer #{ENV['DCR_INITIAL_ACCESS_TOKEN']}"
    # Use a constant-time comparison to avoid leaking the token via timing.
    # Digesting first keeps the comparison fixed-length so the token's length
    # isn't leaked either.
    ActiveSupport::SecurityUtils.secure_compare(
      Digest::SHA256.hexdigest(provided),
      Digest::SHA256.hexdigest(expected),
    )
  end
  # ...
end
```

The block is evaluated in the controller scope (with access to `request`, `params`, `request.headers`, etc.). Return a truthy value to allow the request, or a falsy value to reject it with `401 invalid_token`.

When not configured (default), the endpoint remains open for backward compatibility.

## Development

Run `bundle install` to setup all development dependencies.

To run all specs:

```sh
bundle exec rake spec
```

To generate and run migrations in the test application:

```sh
bundle exec rake migrate
```

To run the local engine server:

```sh
bundle exec rake server
```

By default, the latest Rails version is used. To use a specific version run:

```
rails=7.2 bundle update
```

## License

Doorkeeper::OpenidConnect is released under the [MIT License](http://www.opensource.org/licenses/MIT).

## Sponsors

Initial development of this project was sponsored by [PlayOn! Sports](https://github.com/playon).
