Dynamic Client Registration

One of the primary uses of OpenID Connect metadata is to create a dynamic platform. An important part of that is the client or apps that will use the OAuth and OpenID provider to obtain tokens. With the published metadata, clients can obtain information about endpoints and supported capabilities (i.e., flows or grant types). Even without discovering this information dynamically, it is very common to register new apps on the fly. There are three primary use cases where this is needed:

  1. When an app is installed by a user and the app instance needs an installation-specific client ID and secret.
  2. When an API manager or developer portal wishes to create new apps that will consume APIs.
  3. In an open environment where clients should be able to register a will.

For all of these cases, clients can dynamically register using the API defined in RFC 7591. This specification is a superset of the OpenID Connect Dynamic Client Registration protocol. It allows apps to request a new client ID and secret which can then be used to obtain user authorization. On top if initial registration, The Curity Security Token Server also supports Dynamic Client Registration Management per RFC 7592, to allow for changing the client during its lifetime.

The first case comes up when an iOS, Android, and other kind of native app is installed on a user’s device. At that time, it will register itself to get a client ID and secret that is unique to that installation. This is important because any such credential that is compiled into and distributed with the app is not truly secret. It can be extracted from the application with relative ease. By registering for an installation-specific client ID and secret instead, the app can be treated as a confidential client. Being able to keep a credential secret is a requirement imposed by the OAuth standard to issue a refresh token. Without this, the mobile app needs the user to re-authenticate every time its access token expired. This is a very poor User Experience (UX). Consequently, non-confidential mobile clients are usually given a refresh token despite the security risks. By using Dynamic Client Registration, this compromise does not need to be made.

In the second case, admins use a portal of some sort to register new apps that will consume APIs. This portal is usually provided by an API gateway product of some sort. In this case, the API consumer needs to be registered with the OAuth server, so that it can get its users logged in and obtain tokens from the OAuth server. Because token issuance is provided by Curity and not the gateway, the latter needs to register itself and get a client ID and secret that developers can use in their app. Curity’s admin REST API can be used for this purpose (and is actually preferable in many cases), but Dynamic Client Registration provides a standard API that most gateways support out of the box.

The Curity Security Token Server supports these specifications and can be used to implement the first two use cases; open registration is currently unsupported. Instead, some form of identification – of either the user or the client – is required. How this works is described below.

Architectural Overview of Dynamic Client Registration

There are many ways in which Dynamic Client Registration can be deployed. The specification focuses on the request and response, and does not cover the entire system architecture. It also does not specify how a client may obtain a token required to register itself, just that authentication may be required if needed. For this reason, a high-level overview of the architecture and flow is needed before looking at the actual request/response of the protocol.

Deployments and Configurations

To see how the protocol can be used, it is important to know that there are different configurations or deployments that are possible. In Curity, there are four ways that Dynamic Client Registration can be configured (from the least secure to the most):

  1. Dynamic Client Registration can be enabled and any client can register without authentication. This is the most promiscuous setting and is typically only used for testing or very open ecosystems. This case is very rare, and, as mentioned above, is not currently supported.
  2. Registration is only allowed for clients that have an initial access token that was obtained using the client credential flow. In this case, only the identity of the client is verified – not the user.
  3. Client registration is restricted to applications that have an initial access token obtained after a user has authenticated. In this case, the client is identified but only the end user is verified.
  4. It can be completely disabled (per OAuth profile). This is the default in Curity.

The second is useful when the user does not have an account and cannot create one using self-service signup methods during the login process. This might be the case for an employee app where the new hire’s account can only be created using some kind of back-office system. In such cases, the client can use an ID and secret which are compiled into it, obtaining an initial access token that is then usable for register. To obtain this initial access token, the app uses the client credential flow. The identity of that app is verified, but the level of assurance in that app’s identity is quite low. Consequently, this deployment option does not provide confidence that the app really is the expected one and not an impostor that has decompiled the actual one. In effect, this configuration creates a sort of “speed bump” to the first option of open registration. Having said that, if access to the app is restricted (e.g., via an MDM system that only employees can access), it could be secure enough.

../_images/cc_initial_token2.png

Fig. 135 Obtaining an initial token using the client credential flow which identifies only the client app

The third option is the one that is typically used when distributing an app to customers, partners, or a broad user base. In this setup, the app is distributed with only a client ID. It provides this during user authentication to identity itself, but the app is not actually authenticated. Instead, the user authenticate themselves. This is done using the implicit flow or the assisted token flow. In these cases, the user must login and the app identifies itself. After doing so, an initial access token is issued to the app. As in the second case, this initial access token can then be used by the app to dynamically register itself.

../_images/user_auth_initial_token2.png

Fig. 136 Obtaining an initial access token using a flow that authenticates the user and identifies the app

Note

When the user authenticates, a Single Sign-on (SSO) cookie will be saved. This is important because the app will perform another flow after it has registered and the user cannot be prompted to login again at that point, just a few seconds after the first.

Initial Access Token

In either of these two cases, the client application obtains an initial access token. This first token is used only for registering a specific instance of the application. To limit this token’s usage, it has a special scope called dcr. This scope is included because the application asks for it when performing the client credential flow or one of the flows that authenticates the user.

Warning

If the application does not ask for the dcr scope, the resulting token will not include it and the app will not be able to register itself.

For instance, in the first case above where the client is being identified and not the user, the application will obtain an initial access token by making a request to the OAuth server’s token endpoint using the client credential flow like this:

Listing 197 Obtaining an initial access token that includes the dcr scope using the client credential flow
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=dcr

The result will be a normal token except it will contain the dcr scope. This token can only be used once and it can only be used to call the registration endpoint.

For the code flow and implicit flow, the request is similar but it is made to the OAuth server’s authorization endpoint:

Listing 198 Obtaining an initial access token that includes the dcr scope using the implicit flow
GET /authorize?response_type=token&client_id=c1&scope=dcr HTTP/1.1
Host: server.example.com

Registration

With an initial access token in hand, the client can register itself. To do so, it will make an API call to Curity’s Dynamic Client Registration endpoint. This endpoint is protected and requires an OAuth access token to be presented in the Authorization request header using the bearer authentication scheme (per RFC 6750). What is included in the request body depends on how dynamic clients are registered:

  1. As instances of existing clients where the existing one functions as a sort of template for the dynamically registered one
  2. As entirely new clients

These often correspond to the first two primary use cases described above but they need not.

Registration Based on a Template Client

Dynamic clients can be based on an existing client that is configured to be a template. This greatly simples administration, as all dynamic clients can be updated as a whole. If the configuration of the template changes (e.g., authentication requirements, redirect URI(s), allowed scopes, etc.), then all dynamic clients based on that client are immediately updated. When registering instances of a native app as in the first use case above, this can be very helpful since only the client ID and secret will vary per installation. Registration of a new client that is to be based on another is also very easy; there is only one request parameter: software_id. This ID (as defined in RFC 7591) is the client ID of the template client. No other settings need to be included in the request, and, if there are any, the Curity Security Token Server will ignore them. The settings are always taken from the template. A sample request is shown in the following listing:

Listing 199 A request to register a new dynamic client based on an existing one that is configured as a template
POST /dynimo HTTP/1.1
Host: localhost:8443
Authorization: Bearer 9e565e0b-a487-4092-a4af-461add3155f2
Content-Type: application/javascript

{ "software_id": "client-one" }

A successful response would be a JSON document like this:

Listing 200 A sample request after registering a new client based on a client template using the software_id input parameter
{
    "grant_types": [
        "authorization_code",
        "refresh_token",
        "implicit",
        "password"
    ],
    "subject_type": "public",
    "redirect_uris": [
        "https://localhost/client-one/cb1",
        "https://localhost/client-one/cb2"
    ],
    "client_id": "5bc6f48b-b3c4-413d-a374-20a77ffd1d59",
    "token_endpoint_auth_method": "client_secret_basic",
    "software_id": "client-one",
    "client_secret_expires_at": 0,
    "scope": "email openid profile read write",
    "client_secret": "E4wYA0LOU7hQd-DDh6W2rrqP-OnSsj3pAk243a-j-oE",
    "client_id_issued_at": 1503067741,
    "client_name": "client-one",
    "response_types": [
        "code",
        "token",
        "id_token"
    ],
    "refresh_token_ttl": 3600,
    "id_token_signed_response_alg": "RS256"
}

The values in the response are taken from template identified by the software_id input parameter. The client ID used for the software_id is only acceptable if that client is a template. That is specified by configuring Dynamic-client-registration-template.

Registration Based on a Non-templatized Client

Clients can also be registered without having to be based on an existing one. This method results in a new client that cannot be managed through the UI, CLI, or REST API. For this reason, which configuration will be used during non-templatized Dynamic Client Registration must be specified in the OAuth profile. This way, configuration can be validated even though much of the client metadata is stored in a separate database. When using this method, changes to which scopes are allowed per client, grant types, secrets, etc. have to be updated in the database itself. This database can be any relational database that is supported.

To register a non-templatized client, all metadata for that client must be included in the request. If the client attempts to register a grant type, scope or other metadata that is not specified in the profile, it will be ignored. A sample request is shown in Listing 201:

Listing 201 A sample request showing how a non-templatized client can dynamically register
POST /dynimo HTTP/1.1
Host: localhost:8443
Authorization: Bearer 9e565e0b-a487-4092-a4af-461add3155f2
Content-Type: application/javascript

{
    "scope": "openid read foo test_scope_1",
    "redirect_uris": [ "https://localhost:9000/cb" ],
    "grant_types": [ "authorization_code" ],
    "response_types": "foo",
    "client_name": "XYZ",
    "client_uri": "https://localhost:9000/",
    "tos_uri": "https://localhost:9000/tos",
    "default_max_age": 44,
    "i-am-XYZ": true
}

For such a request, the metadata included in the request which is accepted will be echoed back together any other data the client is registered with. An example of the corresponding response is shown in Listing 202:

Listing 202 A sample response for a non-templatized dynamic client registration
{
    "client_uri": "https://localhost:9000/",
    "grant_types": [
        "authorization_code",
        "refresh_token"
    ],
    "subject_type": "public",
    "default_max_age": 44,
    "redirect_uris": [
        "https://localhost:9000/cb"
    ],
    "client_id": "040eee90-a308-4764-afdd-bcd3cab1b9c8",
    "token_endpoint_auth_method": [
        "client_secret_post",
        "client_secret_basic"
    ],
    "client_secret_expires_at": 0,
    "scope": "read openid test_scope_1",
    "client_secret": "aCzPCvaohIPtPgLbMG5hy_bteL9OFSAb9AJvkdhyfYA",
    "client_id_issued_at": 1503058698,
    "tos_uri": "https://localhost:9000/tos",
    "client_name": "XYZ",
    "response_types": [
        "code",
        "id_token"
    ],
    "allowed_origins": [],
    "refresh_token_ttl": 3600,
    "id_token_signed_response_alg": "RS256",
    "response_types": "foo",
    "i-am-XYZ": true
}

Note how non-standard request parameters, like response_types and i-am-XYZ, are accepted as-is, and as such these parameters are returned in the response. These request parameters can later be accessed as a client’s properties, for example when writing a custom token procedure.

Enabling Dynamic Client Registration

Dynamic registration must be explicitly configured; it is off by default. Enabling it can be done on a per profile basis. In the admin UI, dynamic client registration can be toggled on from the screen OAuth ‣ $PROFILE_NAME ‣ General. In the CLI, it can be enabled by setting dynamic-client-registration in the OAuth profile. Enabling it in the REST API can be done by making a POST request like that shown in Listing 203:

Listing 203 Enabling dynamic client registration using the REST API
$ curl -X POST \
    -k -u admin:Password1 \
    -H 'content-type: application/vnd.yang.data+xml' \
    'https://localhost:6749/admin/api/rest/running/profiles/profile/oauth-dev,as:oauth-service/settings/authorization-server' \
    -d '<dynamic-client-registration>
            <client-data-source>DefaultHSQLDB</client-data-source>
        </dynamic-client-registration>'

Note that a data source also needs to be configured when enabling dynamic client registration. This is the database where the per-app instance data like client ID, secret, the associated user, etc. will be saved. Another required setting is templatized and/or non-templatized. These were explained above.

Dynamic Client Registration Management (DCRM)

Once a client is registered, it is persisted through a datasource and can be used as any other configured client. When enabled, the Curity Identity Server can offer a CRUD-API where clients can be managed, following RFC 7592. This means that, upon returning the client’s metadata as a result of registration, a so called Registration Access Token is issued to the client, together with a registration_access_token_expires_in field that indicates the number of seconds before the token will expire. The Registration Access Token gives access to an endpoint where the client can be managed, which is also part of the metadata in the response of a registration request through the registration_client_uri field:

Listing 204 A sample response for a non-templatized dynamic client registration
{
    "client_id": "040eee90-a308-4764-afdd-bcd3cab1b9c8",
    "client_secret_expires_at": 0,
    "registration_access_token": "b6e65bb7-e54a-401d-9c3e-5ec5ba7abf8b",
    "registration_access_token_expires_in": 0,
    "registration_client_uri", "https://localhost:8443/dcr/040eee90-a308-4764-afdd-bcd3cab1b9c8",
    "...": "...",
}

The DCRM endpoint is always bound to a client, and accepts HTTP GET (read), PUT (update) and DELETE (delete) requests. The result of a read or update request is a JSON-document that is the client’s metadata and contains the full set of properties that describe the client’s registration entry.

Note that a client_secret will also be rotated, so the actual client_secret to use is in the initial response, or if any DCRM read or update request was made to the registration endpoint, the actual client_secret is in the metadata that was returned from the latest read or update request.

Please see RFC 7592 for more details on each operation.

Warning

A registration access token can only be used once. When a request to the DCRM endpoint is made, the response to that request will be metadata that includes a new registration access token that can be used in a subsequent request. This also implies that the registration access token can not be introspected.

The scope of a registration access token is two-fold, meaning that it has the power to read and update the client, as well as the power to delete the client. By default, the power to read and update the client is limited to 28 days after it is issued. However, the power to delete the client is valid up to 365 after issuing the registration access token. These lifetimes can be adjusted by configuration.

Client Certificates and DCRM

When the authentication method for DCR is set to mutual-tls or mutual-tls-by-proxy, a client certificate is used for making the request to the registration endpoint. When DCRM is enabled then the registration access token is bound to this client certificate. To enforce this binding when a token is presented to the DCRM-endpoint, a configuration option must be set in the DCRM configuration section. When enabled, this binding ensures that whenever the registration access token is presented to the DCRM-endpoint, then that same client certificate must be used to authenticate to the DCRM-endpoint for the token to be accepted.

This confirm-certificate-binding setting is disabled by default, so replacing a client’s certificate will not invalidate previously issued registration access tokens.

If the client’s dynamic registration will last longer than the certificate used to authenticate it, then this setting should be off (the default); if, however, the registration will be shorter than the validity of the certificate, enabling this setting (the non-default) is a good measure because the registration access token will be bound to that client throughout its entire registration period.

DCRM Management Clients

Where RFC 7592 describes the functionality for clients to manage themselves, the Curity Identity Server also features so called management clients. A management client is a client that can request a token with the dcrm scope (e.g. through a client credentials flow) and use the resulting token to manage any client. The resulting token is not a registration access token, so it can be used until it expires or it is revoked. The expiration time is the same as the time to live of a registration access token, which is configurable with a default of 28 days.

The management registration access token can be used at the DCRM endpoints, which can be deconstructed as uri-to-the-registration-endpoint/client_id.

Hint

If the Curity Identity Server is served on https://curity.example.com, and the registration endpoint is mapped on the /dcr path, then the DCRM endpoint for a client with id 123abc is https://curity.example.com/dcr/123abc.

Note that only dynamically registered clients can be managed through DCRM.