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

# Okta & Auth0 Identity Verification

> Delegate identity verification to Didit from Okta Identity Engine (BYO IDV) or Auth0, using OpenID Connect with Pushed Authorization Requests.

Didit exposes an OpenID Connect authorization server purpose-built for delegated identity verification.
It implements the contract Okta's "bring your own identity verification vendor" (BYO IDV) framework expects: Pushed Authorization Requests (PAR, RFC 9126), the authorization code flow with PKCE, and an ID token carrying an OpenID Connect `verified_claims` object.

The same endpoints work for any relying party that speaks this profile, including Auth0 post-login Actions.

## Endpoints

Every endpoint lives under the identity verification bridge base URL.

| Environment | Base URL                                |
| ----------- | --------------------------------------- |
| Production  | `https://apx.didit.me/auth/idv`         |
| Sandbox     | `https://apx.staging.didit.me/auth/idv` |

The discovery document at `{base}/.well-known/openid-configuration` advertises all of them:

| Purpose                      | Endpoint                                      |
| ---------------------------- | --------------------------------------------- |
| Pushed Authorization Request | `POST {base}/par`                             |
| Authorization                | `GET {base}/authorize`                        |
| Token                        | `POST {base}/token`                           |
| JWKS                         | `GET {base}/jwks`                             |
| Discovery                    | `GET {base}/.well-known/openid-configuration` |

Client authentication uses `client_secret_basic` or `client_secret_post`.
The authorization code flow requires PKCE with `code_challenge_method=S256`.
ID tokens are signed with RS256; verify them against the JWKS endpoint.

## Flow

1. The relying party pushes the authorization request to `POST /par`, including the PKCE challenge, `state`, `nonce`, its registered `redirect_uri`, the scopes `openid profile identity_assurance`, and the claims it wants verified.
   Didit returns a single-use `request_uri` that is valid for 60 seconds.
2. The relying party redirects the user's browser to `GET /authorize?request_uri=...`.
   Didit creates a verification session and sends the user into the hosted verification flow.
3. When the user finishes, Didit evaluates the verification decision, compares the extracted identity against the claims that were pushed, and returns the user to the relying party's `redirect_uri` with a one-time `code` and the original `state`.
4. The relying party exchanges the code at `POST /token` with its `code_verifier` and receives a signed ID token.

## Requesting claims

Send the claims to compare inside the standard OpenID Connect `claims` parameter of the PAR request.
Both `given_name` and `family_name` are supported, and both are always compared fuzzily.
`verified_claims` is an array, matching Okta's BYO IDV profile and the ID token response below.

```json theme={null}
{
  "id_token": {
    "verified_claims": [
      {
        "verification": {
          "trust_framework": { "value": "IDV-DELEGATED" },
          "assurance_level": { "value": "VERIFIED" }
        },
        "claims": {
          "given_name": { "value": "Lucas", "fuzzy": true },
          "family_name": { "value": "Fraggioli", "fuzzy": true }
        }
      }
    ]
  }
}
```

## The ID token

The ID token carries a `verified_claims` array. `assurance_level` is `VERIFIED` only when the verification session was approved **and** every requested claim matched the verified document.
Otherwise it is `FAILED`.

```json theme={null}
{
  "iss": "https://apx.didit.me/auth/idv",
  "sub": "eea9c5ca-63e6-437b-8e94-131c33f5dce8",
  "aud": "<your client id>",
  "nonce": "<your nonce>",
  "verified_claims": [
    {
      "verification": {
        "trust_framework": "IDV-DELEGATED",
        "assurance_level": "VERIFIED",
        "time": "2026-07-09T11:59:33Z",
        "verification_process": "eea9c5ca-63e6-437b-8e94-131c33f5dce8"
      },
      "claims": {
        "given_name": { "value": "Lucas Agustin", "fuzzy": true },
        "family_name": { "value": "Fraggioli", "fuzzy": true }
      }
    }
  ]
}
```

`verification_process` is the Didit session id, so you can look the verification up in the console or through the sessions API.

<Note>
  Treat the user as verified only when `trust_framework` is `IDV-DELEGATED` **and** `assurance_level` is `VERIFIED`.
  Any other combination means the verification did not succeed.
</Note>

## Configure Okta

In the Okta Admin Console, add a custom identity verification vendor and fill in the values below.
Okta creates the integration as an identity provider of type `IDV_STANDARD` with protocol `ID_PROOFING`, and you then reference it from an Account Management Policy rule.

| Field                     | Value                                     |
| ------------------------- | ----------------------------------------- |
| Issuer                    | `https://apx.didit.me/auth/idv`           |
| PAR request URL           | `https://apx.didit.me/auth/idv/par`       |
| Authorize URL             | `https://apx.didit.me/auth/idv/authorize` |
| Token URL                 | `https://apx.didit.me/auth/idv/token`     |
| JWKS URL                  | `https://apx.didit.me/auth/idv/jwks`      |
| Scopes                    | `openid`, `profile`, `identity_assurance` |
| Client ID / Client secret | Provided by Didit                         |

Give Didit your callback URL so it can be registered on your client.
Okta builds it as `{yourOktaOrgUrl}/idp/identity-verification/callback`.

## Configure Auth0

Auth0 has no native IDV vendor slot, so verification runs from a post-login Action that redirects the user into the same flow.
Store `DIDIT_IDV_BASE`, `DIDIT_CLIENT_ID` and `DIDIT_CLIENT_SECRET` as Action secrets, and register `https://{yourAuth0Domain}/continue` as the redirect URI with Didit.

<Note>
  Auth0 appends its own `state` to the `/authorize` redirect so it can resume the login. Didit adopts that `state` and echoes it back to `/continue`, which is what lets Auth0 complete the transaction - you don't thread Auth0's state through the PAR body (it isn't generated until the redirect).
</Note>

```javascript theme={null}
const crypto = require("crypto");

// Correlate each login's PKCE verifier with its own transaction so a retried
// or overlapping login can't overwrite it (which would fail the token exchange).
const txKey = (event) =>
  event.transaction?.id || event.request?.query?.state || "default";

const basicAuth = (event) =>
  Buffer.from(
    `${event.secrets.DIDIT_CLIENT_ID}:${event.secrets.DIDIT_CLIENT_SECRET}`
  ).toString("base64");

exports.onExecutePostLogin = async (event, api) => {
  if (event.user.app_metadata?.idv_status === "verified") return;

  const verifier = crypto.randomBytes(48).toString("base64url");
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");

  const pending = { ...event.user.app_metadata?.idv_pkce };
  pending[txKey(event)] = verifier;
  api.user.setAppMetadata("idv_pkce", pending);

  const params = {
    response_type: "code",
    client_id: event.secrets.DIDIT_CLIENT_ID,
    redirect_uri: `https://${event.request.hostname}/continue`,
    scope: "openid profile identity_assurance",
    code_challenge: challenge,
    code_challenge_method: "S256",
    login_hint: event.user.email ?? "",
  };

  // Ask Didit to match the scanned document against this user's known name, so an
  // approved session for a different person can't mark this account verified.
  if (event.user.given_name && event.user.family_name) {
    params.claims = JSON.stringify({
      id_token: {
        verified_claims: [
          {
            verification: { trust_framework: { value: "IDV-DELEGATED" } },
            claims: {
              given_name: { value: event.user.given_name, fuzzy: true },
              family_name: { value: event.user.family_name, fuzzy: true },
            },
          },
        ],
      },
    });
  }

  const res = await fetch(`${event.secrets.DIDIT_IDV_BASE}/par`, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${basicAuth(event)}`,
    },
    body: new URLSearchParams(params).toString(),
  });
  if (!res.ok) return api.access.deny("Identity verification is unavailable.");

  const { request_uri } = await res.json();
  api.redirect.sendUserTo(`${event.secrets.DIDIT_IDV_BASE}/authorize`, {
    query: { request_uri, client_id: event.secrets.DIDIT_CLIENT_ID },
  });
};

exports.onContinuePostLogin = async (event, api) => {
  const code = event.request.query.code;
  const pending = { ...event.user.app_metadata?.idv_pkce };
  const key = txKey(event);
  const verifier = pending[key];

  // Consume the verifier so it can't be reused.
  if (key in pending) {
    delete pending[key];
    api.user.setAppMetadata("idv_pkce", pending);
  }
  if (!code || !verifier) return api.access.deny("Identity verification was not completed.");

  const res = await fetch(`${event.secrets.DIDIT_IDV_BASE}/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${basicAuth(event)}`,
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      code_verifier: verifier,
      redirect_uri: `https://${event.request.hostname}/continue`,
    }).toString(),
  });
  if (!res.ok) return api.access.deny("Identity verification could not be confirmed.");

  const { id_token } = await res.json();
  // This token comes directly from Didit's token endpoint over TLS with client
  // authentication (the trusted back-channel), so decoding is sufficient here.
  // For defense-in-depth, verify the RS256 signature against the JWKS endpoint
  // (plus iss / aud / exp / nonce) before trusting it.
  const payload = JSON.parse(Buffer.from(id_token.split(".")[1], "base64url").toString());
  const verification = payload.verified_claims?.[0]?.verification ?? {};

  if (verification.trust_framework === "IDV-DELEGATED" && verification.assurance_level === "VERIFIED") {
    api.user.setAppMetadata("idv_status", "verified");
    api.user.setAppMetadata("idv_session", verification.verification_process ?? null);
  } else {
    api.access.deny("Identity verification failed.");
  }
};
```

<Note>
  Store the PKCE `code_verifier` per login transaction (keyed by `event.transaction.id`), not in a single `app_metadata` field.
  If a user retries or runs an overlapping login, a single shared field gets overwritten and the token exchange fails with `invalid_grant`.
</Note>

## Errors

Errors follow RFC 6749 and RFC 9126.
Failed client authentication returns `401` with `{"error": "invalid_client"}`; an expired or replayed `request_uri` or authorization code returns `400` with `invalid_request` or `invalid_grant`.

Authorization codes, `request_uri` values, and verification sessions are all single use.
