> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xkey.io/llms.txt
> Use this file to discover all available pages before exploring further.

# OIDC Federation

> Register your own OpenID Connect Identity Provider as a trusted issuer so your existing users can log in and bind identities to 0xkey sub-organizations — with every token cryptographically re-verified inside the enclave.

**OIDC Federation** lets a parent organization register its *own* OIDC Identity Provider (an internal auth service, an Auth0/Cognito tenant, or any standards-compliant IdP) as a trusted issuer for its sub-organization users. It's the recommended path for customers who already have a user system and want to keep their IdP as the source of truth for identity while still getting cryptographically-verified, non-custodial logins into 0xkey.

<Note>
  This page is the task-oriented integration guide. For the conceptual background and how federation compares to the built-in social providers (Google, Apple, …), see [Social logins → Custom OIDC providers](/authentication/social-logins#custom-oidc-providers-your-own-identity-provider). The mechanics below reuse the same enclave verification path described there.
</Note>

## When to use federation

<CardGroup cols={2}>
  <Card title="Use OIDC Federation" icon="building-lock">
    You operate your own IdP (or an Auth0/Cognito/Okta tenant) and want it to be the identity source of truth. You want to onboard *existing* users into embedded wallets without asking them to re-authenticate through a third-party social button.
  </Card>

  <Card title="Use built-in social logins" icon="google">
    You just want "Sign in with Google/Apple/…". Those issuers are trusted by default — no registration needed. See [Social logins](/authentication/social-logins).
  </Card>
</CardGroup>

Federation and built-in social logins are not mutually exclusive: a single sub-organization user can have both a Google identity and your own IdP identity bound to them.

## How it fits together

Trust is established once, at the **organization** level; identity binding and login happen per **user**.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Admin as Root-quorum admin
    participant IdP as Your IdP
    participant App as Your app / backend
    participant 0xkey

    Note over Admin,0xkey: One-time, org-level trust setup
    Admin->>0xkey: CREATE_OIDC_PROVIDER (issuer, audiences)
    0xkey->>Admin: providerId

    Note over IdP,0xkey: Per-user, at login time
    App->>IdP: Authenticate end-user (nonce = sha256(publicKey))
    IdP->>App: id_token (OIDC JWT)
    App->>0xkey: oauth_login / CREATE_SUB_ORGANIZATION / CREATE_OAUTH_PROVIDERS
    0xkey->>0xkey: Enclave fetches issuer JWKS live, verifies sig + iss/aud/exp/nonce
    0xkey->>App: session JWT / bound identity
```

The enclave never trusts the token at face value: on **every** login or binding it independently fetches the issuer's discovery document and JWKS over HTTPS (through the [TLS Fetcher](/authentication/social-logins#oidc-token-verification)) and re-verifies the signature, `iss`, `aud`, `exp`, and `nonce`.

## Prerequisites for your IdP

Your Identity Provider must expose the standard OIDC discovery surface:

* **OIDC Discovery** at `https://<your-issuer>/.well-known/openid-configuration`, returning at minimum `issuer` and `jwks_uri`.
* **JWKS** served live over HTTPS at the discovered `jwks_uri`. 0xkey enforces full public-CA TLS validation — there is **no** self-signed / plain-HTTP / private-CA bypass, even in dev.
* **Same-host `jwks_uri`.** For self-hosted (non-built-in) issuers, the `jwks_uri` host must match the issuer host. A mismatch is rejected (see [JWKS host binding](#jwks-host-does-not-match-issuer-host)).
* **RS256 or ES256** signatures. Other algorithms are rejected.
* **Stable `sub`.** The `sub` claim must be a stable, non-reassignable per-user identifier — `(iss, sub)` is 0xkey's identity primary key. If your IdP allows account merging or `sub` reassignment, review it carefully (see the [Auth0 caveat](/authentication/social-logins#auth0)).
* **Key rotation grace period.** When you rotate signing keys, publish the new key *and* keep the old key in the JWKS until in-flight tokens expire. 0xkey simply trusts whatever the JWKS currently serves.

## Integration guide

<Steps>
  <Step title="Register your issuer (one-time, root-quorum)">
    Registering an issuer establishes trust for the whole parent organization. It is a **root-quorum-only** operation.

    From the **Dashboard**: go to **Wallet Kit → OIDC Providers → Add provider**, fill in a name, the issuer URL, and at least one audience. The UI wraps the same activity behind a passkey-gated flow.

    Or via the API with [`CREATE_OIDC_PROVIDER`](/api-reference/activities/create-oidc-provider):

    ```bash title="cURL — CreateOidcProvider" theme={"theme":{"light":"github-light","dark":"github-dark"}}
    curl --request POST \
      --url https://api.0xkey.io/public/v1/submit/create_oidc_provider \
      --header 'Content-Type: application/json' \
      --header "X-Stamp: <root-quorum signature — see Authorizations>" \
      --data '{
        "type": "ACTIVITY_TYPE_CREATE_OIDC_PROVIDER",
        "timestampMs": "1746736509954",
        "organizationId": "<your-parent-organization-id>",
        "parameters": {
          "providerName": "Acme Corp IdP",
          "issuer": "https://auth.acme.com",
          "audiences": ["acme-0xkey-client-id"]
        }
      }'
    ```

    * `issuer` must **exactly** match the `iss` claim of your IdP's tokens, and is **immutable** — delete and re-create if it ever changes.
    * `audiences` is a **required, non-empty** allowlist of `aud` values (your OAuth2 client IDs). There is no wildcard — see [fail-closed audiences](/authentication/social-logins#fail-closed-no-wildcard-trust).
  </Step>

  <Step title="Mint an id_token bound to the session key">
    Before starting your IdP login flow, the frontend generates an API keypair (this becomes the session key). When it authenticates the user against your IdP, it sets the OIDC `nonce` to `sha256(publicKey)` so the resulting `id_token` is cryptographically bound to that key.

    If your IdP cannot customize `nonce`, 0xkey also accepts a `tknonce` claim — only one of the two needs to equal `sha256(publicKey)`. Registration-only flows (`CREATE_OIDC_PROVIDER`, `CREATE_SUB_ORGANIZATION`) don't carry a `publicKey`, so the nonce binding is not enforced there.
  </Step>

  <Step title="Onboard or log the user in">
    With the `id_token` and the API `publicKey`, your backend:

    1. Looks up the sub-organization(s) associated with that identity.
    2. If none exists, creates one with [`CREATE_SUB_ORGANIZATION`](/api-reference/organizations/create-sub-organization), seeding the user's OAuth provider from the token.
    3. Calls [`oauth_login`](/api-reference/activities/login-with-oauth) with the `id_token` + `publicKey` to obtain a session JWT.

    Once your issuer is registered, this flow is **identical** to a built-in provider — `oauth_login` doesn't care whether the issuer is Google or your own IdP.
  </Step>

  <Step title="Bind additional identities to an existing user (optional)">
    To add your IdP as a login method for a user who already exists (e.g. they signed up with a passkey), submit [`CREATE_OAUTH_PROVIDERS`](/api-reference/activities/create-oauth-providers) with the raw `id_token`:

    ```bash title="cURL — CreateOauthProviders" theme={"theme":{"light":"github-light","dark":"github-dark"}}
    curl --request POST \
      --url https://api.0xkey.io/public/v1/submit/create_oauth_providers \
      --header 'Content-Type: application/json' \
      --header "X-Stamp: <see Authorizations>" \
      --data '{
        "type": "ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS_V2",
        "timestampMs": "1746736509954",
        "organizationId": "<organization-id>",
        "parameters": {
          "userId": "<user-id>",
          "oauthProviders": [
            {
              "providerName": "acme-idp",
              "oidcToken": "<raw id_token JWT>"
            }
          ]
        }
      }'
    ```

    <Warning>
      Send `oidcToken` as a **flat string field** inside each `oauthProviders` entry, exactly as shown. Do not nest it under `tokenOrClaims`. The enclave re-verifies this token end-to-end before binding the identity. See [missing field `oidcToken`](#missing-field-oidctoken) if you hit a deserialization error.
    </Warning>
  </Step>
</Steps>

## Managing the registry

| Operation         | Activity / Query                                                         | Notes                                                                                                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List providers    | [`GET_OIDC_PROVIDERS`](/api-reference/queries/get-oidc-providers)        | All registered issuers for the organization.                                                                                                                                                               |
| Update a provider | [`UPDATE_OIDC_PROVIDER`](/api-reference/activities/update-oidc-provider) | Change `providerName`, `audiences`, or `enabled`. The `issuer` is immutable.                                                                                                                               |
| Remove a provider | [`DELETE_OIDC_PROVIDER`](/api-reference/activities/delete-oidc-provider) | Future logins for that issuer fail immediately. Existing per-user bindings are **not** retroactively deleted — use [`DELETE_OAUTH_PROVIDERS`](/api-reference/activities/delete-oauth-providers) for those. |

All three write operations are root-quorum-only.

## Security model

<Warning>
  Registering an issuer is a **trust decision**, not a convenience setting. Anyone who can mint a validly-signed token for a registered `(issuer, audience)` pair can log in as — or bind an identity to — any matching sub-organization user. Keep the audience list as narrow as possible and restrict `CREATE_/UPDATE_/DELETE_OIDC_PROVIDER` to root-quorum admins.
</Warning>

* **Fail-closed audiences.** An empty audience list is rejected outright; there is no "trust this issuer for any audience" option.
* **JWKS host binding.** For federated (non-built-in) issuers, the JWKS fetch host must match the issuer host — this blocks a tampered discovery document from redirecting key-fetching to an attacker-controlled host.
* **Live verification.** Signature, `iss`, `aud`, `exp`, `nbf` (with 60s skew), and `nonce` are checked inside the enclave against the issuer's live JWKS on every login/binding — nothing is cached as "already trusted".

## Troubleshooting

The verification errors below surface from the enclave during `oauth_login`, `CREATE_SUB_ORGANIZATION`, or `CREATE_OAUTH_PROVIDERS`. They are all **fail-closed** — a rejected token never results in a session or a bound identity.

### OIDC issuer '…' is not a registered/enabled trusted IdP for this organization

**Cause:** the token's `iss` doesn't match any enabled provider registered for the organization.

**Fix:**

* Confirm you ran [`CREATE_OIDC_PROVIDER`](/api-reference/activities/create-oidc-provider) for this issuer against the **correct parent organization**, and that the provider is `enabled`.
* The registered `issuer` must be byte-for-byte equal to the token's `iss` (watch for a trailing slash, `http` vs `https`, or a tenant path). The issuer is immutable — if it's wrong, delete and re-create.
* Verify the registry with [`GET_OIDC_PROVIDERS`](/api-reference/queries/get-oidc-providers).

### JWT 'iss' mismatch: expected '…', got '…'

**Cause:** the token's issuer differs from the provider's configured issuer.

**Fix:** align your IdP's `iss` claim with the exact string you registered. These must match character-for-character.

### JWT 'aud' mismatch: expected one of \[…]

**Cause:** the token's `aud` isn't in the registered audience allowlist.

**Fix:** add the OAuth2 client ID your app uses to the provider's `audiences` via [`UPDATE_OIDC_PROVIDER`](/api-reference/activities/update-oidc-provider). If you use different client IDs per platform (web/iOS/Android), register each one you intend to accept — or standardize on a single client ID (see [the `aud` note](/authentication/social-logins#what-does-0xkey-use-from-the-oidc-tokens-to-prove-identity)).

### JWT 'nonce' does not match sha256(public\_key)

**Cause:** the token's `nonce` (or `tknonce`) isn't the SHA-256 of the API `publicKey` used in `oauth_login`.

**Fix:** set the OIDC `nonce` to `sha256(publicKey)` when you start the IdP login flow, using the **same** keypair you later pass to `oauth_login`. This binding is enforced only during authentication, not during registration-only flows.

### JWT has expired

**Cause:** the `id_token`'s `exp` is in the past by the time the enclave verifies it.

**Fix:** OIDC `id_token`s are short-lived (often \~1 hour). Mint a fresh token immediately before the 0xkey call rather than reusing a stale one.

### JWKS host does not match issuer host

Full message: `OIDC JWKS fetch URL host does not match issuer host: issuer='…', jwks_url='…'`.

**Cause:** for a federated issuer, the `jwks_uri` in your discovery document points at a different host than the issuer.

**Fix:** serve your JWKS on the same host as your issuer. (Built-in social providers like Google are exempt because they legitimately publish JWKS on a separate host; this check applies only to your own registered issuers.)

### Unsupported JWT algorithm / JWK not found for kid

**Cause:** the token is signed with an unsupported algorithm (only `RS256` and `ES256` are accepted), or its `kid` isn't present in the currently-served JWKS.

**Fix:** sign tokens with RS256 or ES256, and keep every currently-valid signing key published in your JWKS. During rotation, retain the previous key until all tokens signed with it have expired.

### invalid OIDC token

**Cause:** malformed JWT (not three `.`-separated parts) or missing a required claim (`aud`, `sub`, `iss`).

**Fix:** see [`invalid OIDC token`](/developer-reference/api-overview/errors#invalid-oidc-token) in the error reference.

### missing field `oidcToken`

**Cause:** a `CREATE_OAUTH_PROVIDERS` request whose `oauthProviders[]` entries don't carry a top-level `oidcToken` string.

**Fix:** put `oidcToken` as a flat field on each provider object (as in the [binding example](#integration-guide) above), not nested under `tokenOrClaims`. The enclave expects the raw `id_token` string per provider entry.

## Related

<CardGroup cols={2}>
  <Card title="Social logins" icon="right-to-bracket" href="/authentication/social-logins">
    Built-in providers, OAuth 2.0-only wrapper flow, and the full OIDC verification model.
  </Card>

  <Card title="Create OIDC provider" icon="code" href="/api-reference/activities/create-oidc-provider">
    API reference for registering an issuer.
  </Card>

  <Card title="Create OAuth providers" icon="code" href="/api-reference/activities/create-oauth-providers">
    API reference for binding an identity to a user.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/developer-reference/api-overview/errors">
    Full error-code reference and troubleshooting hub.
  </Card>
</CardGroup>
