# OIDC signing keys

Documentation owner: product `access`; documentation version: `3.9.1`.
Source: `content/products/access/3.9.1/key-components/modules/oidc/signing-keys.md` at commit `eaf7a020437971baeee95e87eaf963636040ac71`.

This page describes how FortifiedID Access selects OIDC signing keys, publishes keys in JWKS, and supports overlapping key rotation for OIDC tokens.

## Overview

OIDC signing keys are used for:

* signing ID tokens
* signing JWT access tokens when `use_jwt_access_tokens` is enabled
* publishing public key material through the JWKS endpoint

Access supports both:

* a legacy single-key configuration based on `keystore`
* a multi-key configuration based on `sign_ref`

Use `sign_ref` when you want overlapping key rotation.

## Recommended configuration

The recommended way to configure OIDC signing keys is `sign_ref`.

Example:

```json
"sign_ref": [
  {
    "keystore": {
      "path": "/fortified_test/keys/oidc-2026-q1.p12",
      "password": "secret",
      "key_password": "secret",
      "alias": "oidc-2026-q1",
      "type": "PKCS12"
    }
  },
  {
    "keystore": {
      "path": "/fortified_test/keys/oidc-2026-q2.p12",
      "password": "secret",
      "key_password": "secret",
      "alias": "oidc-2026-q2",
      "type": "PKCS12"
    }
  }
]
```

Each `sign_ref` entry contains its own keystore configuration. Alias and passwords are resolved from that specific keystore entry.

## Legacy single-key configuration

Legacy single-key configuration is still supported.

Example:

```json
"keystore": {
  "path": "/fortified_test/keystore.p12",
  "password": "secret",
  "type": "PKCS12"
},
"sign_jwt_keystore_alias": "jwt",
"sign_jwt_keystore_password": "secret"
```

This remains useful for environments that do not yet need overlapping rotation.

:::note
If `sign_ref` is configured, Access uses `sign_ref` for OIDC signing keys. The legacy `keystore` configuration is then treated only as backward-compatible fallback.
:::

## Key selection rule

When multiple OIDC signing keys are configured, Access selects the active signing key using certificate validity dates.

The rule is:

* consider only keys that are valid at the current time
* among those keys, choose the key with the earliest `notBefore`
* if multiple keys still tie, use a deterministic tie-breaker based on alias
* if a tie still remains, use `kid`
* if no configured key is currently valid, use the first configured signing key in `sign_ref`

This makes key selection deterministic while still providing a predictable fallback when no certificate is currently valid.

## Runtime rotation

OIDC signing key selection happens at signing time, not only during startup.

This means:

* a future-dated key can be published in JWKS before it becomes active
* Access can continue signing with the current key during the overlap period
* Access keeps signing with the older still-valid key during overlap
* Access automatically moves to the next key only when the older key is no longer valid
* if no configured key is currently valid, Access falls back to the first configured key
* no restart is required for the actual switch

## JWKS behavior

The JWKS endpoint publishes all configured OIDC signing keys from `sign_ref`.

This allows:

* clients to verify tokens signed with the current key
* clients to continue verifying tokens signed with the previous key while those tokens are still valid
* administrators to pre-publish the next signing key before it starts being used
* administrators to decide when an old key should be removed from the configuration

For JWKS details, see [JWKS endpoint](/products/access/key-components/modules/oidc/jwks-endpoint/).

## `kid` behavior

Access sets the JWT header `kid` from the signing key that was actually used for that token.

This applies to:

* ID tokens
* JWT access tokens

The `kid` value is expected to match one of the keys published in JWKS.

## Startup validation

Access validates OIDC signing keys at startup.

Startup fails when:

* the configured alias, keystore password, or key password does not resolve usable key material
* `sign_ref` is configured but no signing key can be resolved from it

Startup does not fail only because all configured certificates are expired or future-dated.

This preserves runtime rotation and the configured first-key fallback while still failing clearly for unusable key material.

## Operational guidance

For safe rotation:

* add the next signing key to `sign_ref`
* keep the previous key in `sign_ref` during the overlap period
* wait at least `max token lifetime + JWKS cache TTL` before removing the old key
* remove the old key only when you no longer need clients to verify tokens signed with it

## Example overlap

Assume:

* Key A: `notBefore=2026-06-01`, `notAfter=2027-06-01`
* Key B: `notBefore=2026-08-01`, `notAfter=2027-08-01`

Behavior:

* on `2026-07-15`, Access signs with Key A
* on `2026-08-10`, both keys are valid, and Access still signs with Key A because it is the oldest certificate that remains valid
* after `2027-06-01`, Key A is no longer valid, and Access signs with Key B
* before `2026-06-01`, neither key is valid yet, and Access falls back to the first configured signing key

Both keys may still be published in JWKS during the overlap period.
