> ## 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 身份验证集成

> Didit 可通过 OpenID Connect 和推送授权请求（PAR），为 Okta Identity Engine（BYO IDV）或 Auth0 提供身份验证委托服务。

Didit 提供了一个专为委托式身份验证构建的 OpenID Connect 授权服务器。
它实现了 Okta 的"自带身份验证供应商"（BYO IDV）框架所要求的协议规范：推送授权请求（PAR，RFC 9126）、带 PKCE 的授权码流程，以及携带 OpenID Connect `verified_claims` 对象的 ID token。

只要依赖方遵循该协议规范，同样的 endpoint 即可使用，包括 Auth0 登录后 Action。

## Endpoints

所有 endpoint 都位于身份验证桥接基础 URL 之下。

| 环境 | 基础 URL |
| - | - |
| 生产环境 | `https://apx.didit.me/auth/idv` |
| 沙盒 | `https://apx.didit.me/auth/idv` |

生产环境与沙盒使用相同的 endpoint。
与您的客户端凭证关联的应用决定所属环境。
每个环境请分别使用对应应用的客户端凭证和工作流。
模拟验证结果请参阅[沙盒与测试数据](https://docs.didit.me/zh/integration/sandbox-testing.md)。

位于 `{base}/.well-known/openid-configuration` 的发现文档列出了以下所有 endpoint：

| 用途 | Endpoint |
| - | - |
| 推送授权请求 | `POST {base}/par` |
| 授权 | `GET {base}/authorize` |
| 令牌 | `POST {base}/token` |
| JWKS | `GET {base}/jwks` |
| 发现 | `GET {base}/.well-known/openid-configuration` |

客户端认证使用 `client_secret_basic` 或 `client_secret_post`。
授权码流程要求使用 PKCE，且 `code_challenge_method=S256`。
ID token 使用 RS256 签名；请通过 JWKS endpoint 验证签名。

## 流程

1. 依赖方（relying party）将授权请求推送至 `POST /par`，其中包括 PKCE 挑战值、`state`、`nonce`、其已注册的 `redirect_uri`、作用域 `openid profile identity_assurance`，以及需要验证的声明。
   Didit 返回一个一次性 `request_uri`，有效期为 60 秒。
2. 依赖方将用户的浏览器重定向至 `GET /authorize?request_uri=...`。
   Didit 创建验证会话，并将用户引导至托管验证流程。
3. 用户完成后，Didit 评估验证决策，将提取的身份信息与推送的声明进行比对，并携带一次性 `code` 和原始 `state` 将用户重定向回依赖方的 `redirect_uri`。
4. 依赖方使用其 `code_verifier` 在 `POST /token` 处交换 code，并获得已签名的 ID token。

## 请求声明

需要比对的声明应放在 PAR 请求的标准 OpenID Connect `claims` 参数中发送。
支持 `given_name` 和 `family_name`，且两者始终进行模糊比对。
`verified_claims` 为数组，与 Okta 的 BYO IDV 配置文件以及下文的 ID token 响应格式一致。

```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 }
        }
      }
    ]
  }
}
```

## ID token

ID token 携带一个 `verified_claims` 数组。只有当验证会话已通过**并且**所有请求的声明都与已验证证件匹配时，`assurance_level` 才为 `VERIFIED`。否则为 `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` 即 Didit 会话 ID，因此您可以在控制台中或通过会话 API 查询该验证。

> [!NOTE]
>
> 只有当 `trust_framework` 为 `IDV-DELEGATED` **并且** `assurance_level` 为 `VERIFIED` 时，才能将用户视为已验证。其他任何组合均表示验证未成功。

## 配置 Okta

请联系 Didit 为您的生产环境和沙盒应用启用身份验证，将每个应用与其验证工作流关联，并为每个 Okta 租户注册准确的回调 URL。
仅有现有应用或 API 密钥并不能启用此集成。

在 Okta 管理控制台中，添加自定义身份验证供应商，并填写下表中的值。
Okta 将该集成创建为类型为 `IDV_STANDARD`、协议为 `ID_PROOFING` 的身份提供方，随后您可在账户管理策略规则中引用它。

| 字段 | 值 |
| - | - |
| 签发者 | `https://apx.didit.me/auth/idv` |
| PAR 请求 URL | `https://apx.didit.me/auth/idv/par` |
| 授权 URL | `https://apx.didit.me/auth/idv/authorize` |
| 令牌 URL | `https://apx.didit.me/auth/idv/token` |
| JWKS URL | `https://apx.didit.me/auth/idv/jwks` |
| 作用域 | `openid`, `profile`, `identity_assurance` |
| 客户端 ID | 对应 Didit 应用的客户端 ID |
| 客户端密钥 | 该应用的主 API 密钥，可在 Didit 控制台的 **Developers > API keys** 中找到 |

请将生产环境与沙盒分别配置为独立的身份验证集成，并使用各自的凭证。
两者均使用上述 endpoint。
即使流程同时使用 PKCE，仍需提供客户端密钥。
单独创建的命名 API 密钥不能替代该应用的主 API 密钥用于此集成。

请向 Didit 提供您的回调 URL，以便在您的客户端上注册该 URL。
Okta 生成的回调 URL 格式为 `{yourOktaOrgUrl}/idp/identity-verification/callback`。

## 配置 Auth0

Auth0 没有内置的身份验证供应商入口，因此验证通过登录后 Action 运行，该 Action 会将用户重定向至同一流程。
将 `DIDIT_IDV_BASE`、`DIDIT_CLIENT_ID` 和 `DIDIT_CLIENT_SECRET` 保存为 Action 密钥，并在 Didit 中将 `https://{yourAuth0Domain}/continue` 注册为重定向 URI。

> [!NOTE]
>
> Auth0 会在 `/authorize` 重定向中附加自己的 `state`，以便恢复登录。Didit 采用该 `state` 并将其回传至 `/continue`，正是这一机制使 Auth0 能够完成该事务。您无需将 Auth0 的 state 传入 PAR 请求体（该值要到重定向时才生成）。

```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]
>
> 请按登录事务保存 PKCE `code_verifier`（以 `event.transaction.id` 为键），而不要保存在单一的 `app_metadata` 字段中。如果用户重试或发起同时进行的登录，单一共享字段会被覆盖，令牌交换会因 `invalid_grant` 而失败。

## 错误

错误遵循 RFC 6749 和 RFC 9126。
客户端认证失败时，返回 `401` 和 `{"error": "invalid_client"}`；`request_uri` 或授权码过期或被重放时，返回 `400`，并附带 `invalid_request` 或 `invalid_grant`。

授权码、`request_uri` 值和验证会话均为一次性使用。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.