# Token Endpoint

Endpoint to obtain an ID token. The ID token is a signed JWT that contains user information in the sub claim, and is signed by Singpass Auth. RPs will be able to verify the ID token’s JWT signature with our JWKS endpoint.

For the Step-up authentication flow, only the following grant(s) is supported:

* `urn:openid:params:grant-type:ciba`

In the below sections, we are going to describe the API contract.

## Client-Initiated Backchannel Authentication (CIBA) Grant <a href="#client_initiated_backchannel_authentication_ciba_grant" id="client_initiated_backchannel_authentication_ciba_grant"></a>

This grant type allows the RP to obtain an ID token for the user indicated by the `login_hint` parameter that was provided during initiation. This is only allowed for `confidential` RPs.

Currently, Singpass only supports **Poll** mode, which means that RPs need to poll the token endpoint at regular intervals until they receive an ID token or receive certain error codes (see [Error Responses](#error-responses)).

| Important | RPs should **NOT** send two overlapping token requests with the same `auth_req_id`. RPs should always wait until receiving the response to the previous request before sending out the next request. |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| Note | It is recommended for RPs to wait for at least **30s** for a response before trying again. |
| ---- | ------------------------------------------------------------------------------------------ |

### **Client Authentication - Client Assertion JWT**

RPs must authenticate themselves using **Client Assertion JWT**.

In this authentication mechanism, the RP is required to present an assertion JWT in the request body. Singpass will then verify this JWT against a JWK provided by the RP during onboarding.

This method of authentication relies on PKI and eradicates the need to keep and transfer secrets. You may refer to [this RFC spec documentation](https://tools.ietf.org/html/rfc7523#section-2.2) for more details.

#### **Assertion JWT Structure**

The RP is required to generate an assertion JWT that has the following header and claims, and is signed with the JWK that was provided during onboarding.

**JWT Header**

The header **must include `alg` and `typ`**.

The supported `alg` types are:

* `ES256`
* `ES384`
* `ES512`.

This must match the `alg` value in the signing key used to sign the assertion (if the signing JWK specifies `alg` explicitly).

The header should also include `kid` of the signing key to help identify which of the RP’s signing keys was used, though this is not mandatory. If omitted, we will test against all known signing keys when attempting to verify the signature.

example

```json
{
  "typ" : "JWT",
  "alg" : "ES256",
  "kid" : "rp_key_01"
}
```

**JWT Claims**

| Path  | Type     | Description                                                                                                                                                                                                                                                     |
| ----- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sub` | `String` | This should be `client_id` of the registered client.                                                                                                                                                                                                            |
| `aud` | `String` | The recipient that the JWT is intended for. This must match the `issuer` field in the response of the OpenID discovery endpoint (<https://id.singpass.gov.sg/.well-known/openid-configuration>). e.g. [https://id.singpass.gov.sg](https://id.singpass.gov.sg/) |
| `iss` | `String` | This should be `client_id` of the registered client.                                                                                                                                                                                                            |
| `iat` | `Number` | The time at which the JWT was issued. <https://tools.ietf.org/html/rfc7519#section-4.1.6>                                                                                                                                                                       |
| `exp` | `Number` | The expiration time on or after which the JWT MUST NOT be accepted by Singpass for processing. Additionally, Singpass will not accept tokens with an `exp` longer than 2 minutes since `iat`. <https://tools.ietf.org/html/rfc7519#section-4.1.4>               |

### **Request / Response Structure**

#### **Curl request**

```bash
$ curl 'https://stg-id.singpass.gov.sg/token' -i -X POST \
    -H 'Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1' \
    -d 'client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&auth_req_id=9f7e4f01-45cc-4eed-84c2-f7f58233f7fa&client_assertion=eyJraWQiOiJzaWdJZCIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJpTUJ3MlhkR2VHanRYRVl4TURoT1RxUnN5a000dHRaZiIsInN1YiI6ImlNQncyWGRHZUdqdFhFWXhNRGhPVHFSc3lrTTR0dFpmIiwiYXVkIjoiaHR0cHM6Ly9zdGctaWQuc2luZ3Bhc3MuZ292LnNnIiwiZXhwIjoxNzM5NDk3MjY4LCJpYXQiOjE3Mzk0OTcxNDh9.8LhU6OSaZhZqJ90T2i6S1ay-Ir_mbw_PXOvjI49BhtR--3A2F3WgLoiNesDHFVCSzz7uv4NoiQqGWxn5C0hdWA&grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba'
```

#### **Form parameters**

| Parameter               | Description                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `auth_req_id`           | Authentication request id.                                                                  |
| `grant_type`            | The type of grant being requested. This must be set to `urn:openid:params:grant-type:ciba`. |
| `client_assertion_type` | This MUST be set to `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`                |
| `client_assertion`      | A JWT identifying the client.                                                               |

#### **HTTP request**

```http
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: stg-id.singpass.gov.sg
Content-Length: 557

client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&auth_req_id=9f7e4f01-45cc-4eed-84c2-f7f58233f7fa&client_assertion=eyJraWQiOiJzaWdJZCIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJpTUJ3MlhkR2VHanRYRVl4TURoT1RxUnN5a000dHRaZiIsInN1YiI6ImlNQncyWGRHZUdqdFhFWXhNRGhPVHFSc3lrTTR0dFpmIiwiYXVkIjoiaHR0cHM6Ly9zdGctaWQuc2luZ3Bhc3MuZ292LnNnIiwiZXhwIjoxNzM5NDk3MjY4LCJpYXQiOjE3Mzk0OTcxNDh9.8LhU6OSaZhZqJ90T2i6S1ay-Ir_mbw_PXOvjI49BhtR--3A2F3WgLoiNesDHFVCSzz7uv4NoiQqGWxn5C0hdWA&grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba
```

#### **HTTP response**

```http
HTTP/1.1 200 OK
Expires: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
X-XSS-Protection: 0
Pragma: no-cache
X-Frame-Options: DENY
Date: Fri, 14 Feb 2025 01:39:09 GMT
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
Transfer-Encoding: chunked
Content-Type: application/json
Content-Length: 446

{"token_type":"Bearer","id_token":"eyJraWQiOiJuZGlfc3RnXzAxIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJhdWQiOiJpTUJ3MlhkR2VHanRYRVl4TURoT1RxUnN5a000dHRaZiIsInN1YiI6InM9Uzg4MjkzMTRCLHU9MWMwY2VlMzgtM2E4Zi00ZjhhLTgzYmMtN2EwZTRjNTlkNmE5IiwiYW1yIjpbInB3ZCIsInN3ayJdLCJpc3MiOiJodHRwczovL3N0Zy1pZC5zaW5ncGFzcy5nb3Yuc2ciLCJleHAiOjE3Mzk0OTc3NDgsImlhdCI6MTczOTQ5NzE0OH0.BxXLysNFj7TKZBydqXICOCzLzQyylt_Gde18_lSLq4ZcWNz8TY3ZGT_DXU5dWkBEJ4OSaDcEfpxpwNdd-MeEDA"}
```

#### **Request body**

```x-www-form-urlencoded
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&auth_req_id=9f7e4f01-45cc-4eed-84c2-f7f58233f7fa&client_assertion=eyJraWQiOiJzaWdJZCIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJpTUJ3MlhkR2VHanRYRVl4TURoT1RxUnN5a000dHRaZiIsInN1YiI6ImlNQncyWGRHZUdqdFhFWXhNRGhPVHFSc3lrTTR0dFpmIiwiYXVkIjoiaHR0cHM6Ly9zdGctaWQuc2luZ3Bhc3MuZ292LnNnIiwiZXhwIjoxNzM5NDk3MjY4LCJpYXQiOjE3Mzk0OTcxNDh9.8LhU6OSaZhZqJ90T2i6S1ay-Ir_mbw_PXOvjI49BhtR--3A2F3WgLoiNesDHFVCSzz7uv4NoiQqGWxn5C0hdWA&grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba
```

#### **Response body**

```json
{"token_type":"Bearer","id_token":"eyJraWQiOiJuZGlfc3RnXzAxIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJhdWQiOiJpTUJ3MlhkR2VHanRYRVl4TURoT1RxUnN5a000dHRaZiIsInN1YiI6InM9Uzg4MjkzMTRCLHU9MWMwY2VlMzgtM2E4Zi00ZjhhLTgzYmMtN2EwZTRjNTlkNmE5IiwiYW1yIjpbInB3ZCIsInN3ayJdLCJpc3MiOiJodHRwczovL3N0Zy1pZC5zaW5ncGFzcy5nb3Yuc2ciLCJleHAiOjE3Mzk0OTc3NDgsImlhdCI6MTczOTQ5NzE0OH0.BxXLysNFj7TKZBydqXICOCzLzQyylt_Gde18_lSLq4ZcWNz8TY3ZGT_DXU5dWkBEJ4OSaDcEfpxpwNdd-MeEDA"}
```

#### **Response fields**

| Path         | Type     | Description                                                                                                                                                                                                                                                                       |
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_type` | `String` | The type of token being requested, Bearer only so far.                                                                                                                                                                                                                            |
| `id_token`   | `String` | The ID token with relevant claims in JWT format signed by the ASP. Note that the example response body shows a JWS (3-part structure separated by dots), but the format will differ for a JWS in JWE (5-part structure). See below for more details about the ID token structure. |

#### **Error Responses**

The table below shows the list of common error codes that may be returned for this endpoint. RPs must take note to only re-poll the token endpoint for scenarios as specified below.

| Important | Note that only the `error` field in the JSON response should be used to determine the error scenario. Do NOT use the `error_description` field to determine the error scenario. Singpass reserves the right to change the content of the `error_description` field without prior notice to RPs. |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| Error code (returned in `error` field) | Error scenario                                                          | Can perform re-poll? |
| -------------------------------------- | ----------------------------------------------------------------------- | -------------------- |
| `authorization_pending`                | The session is still pending as the end-user has not authenticated yet. | **Yes**              |
| `access_denied`                        | End-user rejected the authentication via SP mobile app.                 | No                   |
| `expired_token`                        | The authentication session has expired or is not found.                 | No                   |
| `unauthorized_client`                  | RP is not authorized to perform this flow.                              | No                   |
| `invalid_client`                       | RP failed client assertion authentication.                              | No                   |
| `invalid_grant`                        | -                                                                       | No                   |
| `invalid_request`                      | -                                                                       | No                   |
| `server_error`                         | -                                                                       | No                   |

### **ID Token Structure**

The format and structure of the issued ID Token will vary depending on the client’s profile as specified in this table below:

| Client Profile                            | `sub` Claim Content                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | ID token format                                                                                                |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `direct`                                  | UUID only (eg. `u=32af8b7d-ad1d-4c25-8dc7-0a981b533000`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | JWS                                                                                                            |
| `direct_pii_allowed`                      | <p><strong>Regular NRIC holders</strong>: NRIC and UUID (eg. <code>s=S1234567A,u=32af8b7d-ad1d-4c25-8dc7-0a981b533000</code>)<br></p><p><strong>Singpass Foreign Account (SFA) holders</strong>: Singpass User ID (UID), Foreigner ID (FID), Country-of-Issuance (COI) and UUID (eg. <code>s=Y7613265T,fid=G730Z-H5P96,coi=DE,u=e2af740e-25b4-4b19-b527-494670952cb0</code>)<br></p><p>This class of users were previously known as "Foreign Unique Account" or "Singpass Foreign Unique Account" users. Only designated relying parties are able to have SFA users authenticate & complete token exchange.</p> | <p>JWS in JWE (encrypted with client’s JWK)</p><p>See section below for more details about the JWE format.</p> |
| `bridge` (special case internal use only) | NRIC and UUID (eg. `s=S1234567A,u=32af8b7d-ad1d-4c25-8dc7-0a981b533000`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | JWS                                                                                                            |

#### **Overview of a JWS in JWE**

An encrypted ID token will returned from the `/token` endpoint is a JWS in JWE that is in compact serialization form. It has the following structure:

* JWE Header
* Encrypted Key
* Initialization Vector
* Encrypted Payload (if decrypted, this would be the Base64 encoded form of a [JWS ID Token](#overview-of-a-jws))
* Authentication Tag

See <https://datatracker.ietf.org/doc/html/rfc7516#section-3.1> for more details.

**JWE Header**

The JWE will contain these standard headers. Refer to <https://tools.ietf.org/html/rfc7515#section-4> for more information about each header.

Note: Relying parties should use the `kid` field in the header to determine which key Singpass used for encryption.

example

```json
{
	"kid": "client_01",
	"cty": "JWT",
	"enc": "A256CBC-HS512",
	"alg": "ECDH-ES+A256KW"
}
```

#### **Overview of a JWS**

The [JWS](https://tools.ietf.org/html/rfc7515) ID token returned from the `/token` endpoint is in compact serialization form. A JWS has the following structure.

* JWS Header
* Payload (containing claims)
* Signature

**JWS Header**

The JWS ID token will contain these standard headers. Refer to <https://tools.ietf.org/html/rfc7515#section-4> for more information about each header.

example

```json
{
  "kid" : "ndi_stg_01",
  "typ" : "JWT",
  "alg" : "ES256"
}
```

**JWT Claims**

The JWS ID token will contain the following claims.

example

```json
{
  "aud" : "iMBw2XdGeGjtXEYxMDhOTqRsykM4ttZf",
  "sub" : "s=S8829314B,u=1c0cee38-3a8f-4f8a-83bc-7a0e4c59d6a9",
  "amr" : [ "pwd", "swk" ],
  "iss" : "https://stg-id.singpass.gov.sg",
  "exp" : 1739497748,
  "iat" : 1739497148
}
```

| Path  | Type     | Description                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sub` | `String` | The principal that is the subject of the JWT. Contains a comma-separated, key=value mapping that identifies the user; possibly including multiple alternate IDs representing the user. The keys included vary by the profile of the OIDC client, and the user type, however the minimal format is `u=<UUID>` where UUID represents the user’s globally unique identifier. <https://tools.ietf.org/html/rfc7519#section-4.1.2> |
| `aud` | `String` | The client\_id of the relying party. <https://tools.ietf.org/html/rfc7519#section-4.1.3>                                                                                                                                                                                                                                                                                                                                      |
| `iss` | `String` | The principal that issued the JWT. <https://tools.ietf.org/html/rfc7519#section-4.1.1>                                                                                                                                                                                                                                                                                                                                        |
| `iat` | `Number` | The time at which the JWT was issued. <https://tools.ietf.org/html/rfc7519#section-4.1.6>                                                                                                                                                                                                                                                                                                                                     |
| `exp` | `Number` | The expiration time on or after which the JWT MUST NOT be accepted for processing. Defaults to 10 minutes since "iat". <https://tools.ietf.org/html/rfc7519#section-4.1.4>                                                                                                                                                                                                                                                    |
| `amr` | `Array`  | Authentication method references. Example values are `["face"]`, `["fv"]`, `["fv-alt"]`, `["otp"]`, `["pwd","fv"]`, `["pwd","otp-email"]`, `["pwd","sms"]`, `["pwd","swk"]`, `["pwd"]`, `["sso"]`. Note that this list is non-exhaustive, and Singpass reserves the right to introduce new values without prior notice to RPs.                                                                                                |

## Client JWK Requirements <a href="#client_jwk_requirements" id="client_jwk_requirements"></a>

Clients are expected to provide public keys to Singpass in the [JWK](https://tools.ietf.org/html/rfc7517) format. These public keys will be used in the following (non-exhaustive) scenarios:

* Signature JWK used to verify the signature of the [client assertion JWT](#client-authentication-client-assertion-jwt) presented during token request.
* Encryption JWK used to [encrypt an ID token](#id-token-structure) which contains the user’s PII.

The client must provide the public key(s) during onboarding, and they can do so only via **ONE** of the following forms:

* Provide a JWKS in a JSON format.
* Host the JWKS on a publicly accessible URL. This endpoint must be compatible with Singpass' [service level expectations](#jwks-url-service-level-expectations).

Signing key is always required for both `direct` and `direct_pii_allowed` clients. Encryption key is only required for `direct_pii_allowed` clients. `direct_pii_allowed` clients must ensure that the provided JWKS or the resource returned by the JWKS URL contains both the signing and encryption keys.

| Tip | [mkjwk.org](https://mkjwk.org/) is a useful open-source tool to generate different types of JWK for signing and encryption; compliant with Singpass' broad requirements on structure. While we **DO NOT** suggest this as a secure way to generate your *real* keypair (including private key); this can be a useful tool to understand how JWK works; and how it is represented for signing and encryption purposes; while you are reviewing against our supported algorithms below. |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

### **JWK for signing**

The signing JWK will be used to verify the [client assertion JWT](#client-authentication-client-assertion-jwt) provided during `/token` request, thereby authenticating the client.

Clients are allowed to provide multiple signature keys in the JWKS / hosted on the JWKS url provided during client creation.

The signature JWK should have the following attributes:

* Must have key `use` of value `sig` per [rfc7517#section-4.2](https://tools.ietf.org/html/rfc7517#section-4.2)
* Must contain a key ID in the standard `kid` field per [rfc7517#section-4.5](https://tools.ietf.org/html/rfc7517#section-4.5)
  * Will be used by Singpass to select the relevant key to verify the client assertion
* Must be an EC key, with curves: `P-256`, `P-384` or `P-521` *(NIST curves, aka `secp256r1`, `secp384r1`, `secp521r1` respectively)*

Example EC signing key using P-256 and a timestamped key Id

```json
{
    "kty": "EC",
    "use": "sig",
    "kid": "sig-2021-01-15T12:09:06Z",
    "crv": "P-256",
    "x": "Tjm2thouQXSUJSrKDyMfVGe6ZQRWqCr0UgeSbNKiNi8",
    "y": "8BuGGu519a5xczbArHq1_iVJjGGBSlV5m_FGBJmiFtE"
}
```

#### **Key Rotation**

Relying parties can **rotate** their signing keys in a self-driven manner. To do this with **zero downtime** the Relying party must

* support use of **JWKS URLs** and be onboarded as such
* ensure their replacement signing key has a different key ID (`kid`) to the original key
* ensure their replacement signing key matches the other cryptographic key requirements

To do this with zero downtime, the following procedure should be followed by the Relying Party:

| Time          | Action                                                                                                                                 |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Prep          | Relying party generates a new signing key pair (`K2`) supported for signing.                                                           |
| T0            | Relying party **adds** public key `K2` to its JWKS endpoint (and leaves `K1` on the endpoint)                                          |
| T0 - T+1 hour | Singpass' cache will expire, and re-retrieves the relying party’s published keys from their JWKS endpoint which now includes `K2`      |
| > T+1 hour    | Relying party changes their system to start signing client assertions using the new signing key `K2`                                   |
| Clean Up      | Post-validation, relying party can remove key `K1` from their JWKS endpoint when they are comfortable their new signing key is working |

### **JWK for encryption**

The encryption JWK will be used to [encrypt ID tokens](#id-token-structure) requested from the `/token` endpoint.

Singpass will select the strongest available, supported encryption key from either a **local JWKS**, or **JWKS URL** to encrypt returned ID tokens for those relying parties who require any PII in the ID token.

The encryption JWK must have the following attributes:

* Must have key `use` of value `enc` per [rfc7517#section-4.2](https://tools.ietf.org/html/rfc7517#section-4.2)
* Must contain a key ID in the standard `kid` field per [rfc7517#section-4.5](https://tools.ietf.org/html/rfc7517#section-4.5)
  * The key ID will be specified in the returned JWE header so that clients can pick the right key for decryption
* Must have key type (`kty`) of `EC`
* Must specify the appropriate key encryption `alg` the relying party wants Singpass to use, consistent with the key type/curve (`kty`), and meet the requirements below on allowed `alg`/`curve`/key sizes, consistent with [RFC7518 - JSON Web Algorithm specification](https://tools.ietf.org/html/rfc7518#section-4.1)

| Key Type (`kty`)                 | EC                                                                                                                          |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Status                           | **Required** for new Relying Parties                                                                                        |
| Key encryption algorithm (`alg`) | <p>ECDH-ES+A128KW<br>ECDH-ES+A192KW<br>ECDH-ES+A256KW</p>                                                                   |
| Curve (`crv`)                    | <p>P-256 <em>(NIST, aka secp256r1)</em><br>P-384 <em>(NIST, aka secp384r1)</em><br>P-521 <em>(NIST, aka secp521r1)</em></p> |

Example EC encryption key using P-256 and a timestamped key Id; asking us to encrypt the CEK using ECDH-ES+A128KW

```json
{
    "kty": "EC",
    "use": "enc",
    "kid": "enc-2021-01-15T12:09:06Z",
    "crv": "P-256",
    "x": "xom6kD54yfXRPvMFVYFlVjUKzmNhz7wf0DP_2h9kXtY",
    "y": "lrh8C9c8-SBJTm1FcfqLkj2AnHtaxpnB1qsN6PiFFJE",
    "alg": "ECDH-ES+A128KW"
}
```

#### **Key Preference**

If the relying party exposes *multiple supported encryption keys*, Singpass will select the key to use for encrypting tokens based on the following logic:

1. prefer any EC key (`kty`) matching the above requirements
2. prefer EC keys with stronger `crv` (curve) *above* EC keys with weaker curve
3. prefer EC keys with stronger `alg` key wrapping *above* weaker ones
4. otherwise pick the first compatible key we find

#### **Key Rotation**

Relying parties can **rotate** their encryption keys in a self-driven manner. To do this with **zero downtime** the Relying party must

* support use of **JWKS URLs** and be onboarded as such
* have the ability to decrypt tokens produced with either **one of two** different encryption keys based on either
  * selecting the correct decryption key by its key ID (`kid`)
  * trial-and-error decryption against multiple keys in a collection
* ensure their replacement key matches the other cryptographic key requirements noted above

To do this with zero downtime, the following procedure should be followed by the Relying Party:

| Time          | Action                                                                                                                                                         |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prep          | Relying party generates a new encryption key pair (`K2`) supported for decryption. The existing key pair (`K1`) is still available for decryption of ID tokens |
| T0            | Relying party **removes** public key `K1` and **adds** public key `K2` on its JWKS endpoint                                                                    |
| T0 - T+1 hour | Singpass' caches will expire at any (indeterminate) time within this period, and start encrypting tokens with new encryption key `K2`.                         |
| T0 - T+1 hour | Relying party may be receiving tokens encrypted with either `K1` or `K2` keys throughout this period; and must be able to decrypt either.                      |
| > T+1 hour    | Relying party can remove support for decrypting with the previous `K1` key                                                                                     |

### **JWKS URL Service Level Expectations**

Singpass requires that any JWKS is published on an endpoint that

* is served behind HTTPS on port `443` using a TLS server certificate issued by a standard *publicly verifiable CA issuer* (no private CAs), with *complete cert chain* presented by the server
* is publicly accessible (no IP whitelisting, mTLS or other custom HTTP header requirements outside standard HTTP headers such as `Content-Type`, `Accept`)
* is able to respond in a timely fashion with respect to the below configuration

| Per try timeout                   | 3s |
| --------------------------------- | -- |
| Max attempts                      | 3  |
| Cache duration for retrieved JWKS | 1h |

| Note | While the above is a technical requirement; the user experience of your users may be affected if we are unable to retrieve your JWKS in a timely fashion upon our cache expiry due to slower token exchanges with your backend. We recommend aiming for this response to be as fast as possible based on an in-memory cache; or simple static asset retrieval. |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

If Singpass fails to retrieve a valid JWKS from the provided URL after cache expiry, the relying party’s token exchange will fail with an OAuth2/OIDC `invalid_client` error in these circumstances:

* if *client assertions* are used, and we are unable to validate the relying party’s assertion using their signing key
* if *encryption of returned ID tokens* is required, and we are unable to retrieve the relying party’s preferred encryption key
