> ## 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の本人確認（IDV）連携

> Okta Identity Engine（BYO IDV）やAuth0の本人確認をDiditに委任できます。OpenID ConnectとPushed Authorization Requests（PAR）で連携します。

Didit は、委任型の本人確認のために設計されたOpenID Connect認可サーバーを提供しています。
このサーバーは、Oktaの「bring your own identity verification vendor」(BYO IDV)フレームワークが求める仕様を実装しています。具体的には、Pushed Authorization Requests(PAR、RFC 9126)、PKCEを用いた認可コードフロー、そしてOpenID Connectの`verified_claims`オブジェクトを含むIDトークンです。

このプロファイルに対応するリライングパーティであれば、Auth0のpost-loginアクションを含め、同じエンドポイントを利用できます。

## エンドポイント

すべてのエンドポイントは、本人確認ブリッジのベースURLの配下にあります。

| 環境 | ベースURL |
| - | - |
| 本番環境 | `https://apx.didit.me/auth/idv` |
| サンドボックス | `https://apx.didit.me/auth/idv` |

本番環境とサンドボックスは同じエンドポイントを使用します。
環境は、クライアント認証情報に紐づくアプリケーションによって決まります。
環境ごとに別々のクライアント認証情報を使用し、対応するアプリケーションに属するワークフローを使用してください。
検証結果のシミュレーションについては、[サンドボックスとテストデータ](https://docs.didit.me/ja/integration/sandbox-testing.md)を参照してください。

`{base}/.well-known/openid-configuration`のディスカバリードキュメントには、これらすべてが記載されています。

| 用途 | エンドポイント |
| - | - |
| Pushed Authorization Request | `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`を使用します。
認可コードフローでは、`code_challenge_method=S256`によるPKCEが必須です。
IDトークンはRS256で署名されています。JWKSエンドポイントを使って検証してください。

## フロー

1. リライングパーティは、PKCEチャレンジ、`state`、`nonce`、登録済みの`redirect_uri`、スコープ`openid profile identity_assurance`、および検証したいクレームを含めて、認可リクエストを`POST /par`にプッシュします。
   Didit は、60秒間有効な使い捨ての`request_uri`を返します。
2. リライングパーティは、ユーザーのブラウザを`GET /authorize?request_uri=...`にリダイレクトします。
   Didit は検証セッションを作成し、ユーザーをホスト型の検証フローに送ります。
3. ユーザーが検証を完了すると、Didit は検証の判定を行い、抽出した本人情報をプッシュされたクレームと照合したうえで、ワンタイムの`code`と元の`state`を付けて、ユーザーをリライングパーティの`redirect_uri`に戻します。
4. リライングパーティは、`code_verifier`を使って`POST /token`でコードを交換し、署名済みのIDトークンを受け取ります。

## クレームのリクエスト

照合するクレームは、PARリクエストの標準のOpenID Connect `claims`パラメーター内で送信します。
`given_name`と`family_name`の両方に対応しており、どちらも常にあいまい照合されます。
`verified_claims`は配列であり、OktaのBYO IDVプロファイルおよび後述のIDトークンのレスポンスに準拠しています。

```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トークン

IDトークンには`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の設定

本番環境とサンドボックスのアプリケーションで本人確認を有効にし、それぞれを本人確認ワークフローに紐づけ、各Oktaテナントの正確なコールバックURLを登録するよう、Diditに依頼してください。
既存のアプリケーションやAPIキーだけでは、この連携は有効になりません。

Okta Admin Consoleで、カスタムの本人確認ベンダーを追加し、以下の値を入力します。
Oktaは、この連携をタイプ`IDV_STANDARD`、プロトコル`ID_PROOFING`のアイデンティティプロバイダーとして作成します。その後、Account Management Policyのルールからこれを参照します。

| フィールド | 値 |
| - | - |
| 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 | 対応するDiditアプリケーションのクライアントID |
| Client secret | そのアプリケーションのプライマリAPIキー。Didit Consoleの**Developers > API keys**で確認できます |

本番環境とサンドボックスは、それぞれの認証情報を使って別々の本人確認連携として設定してください。
どちらにも上記のエンドポイントを使用します。
このフローではPKCEも使用しますが、クライアントシークレットは必須です。
この連携では、別途作成した名前付きのAPIキーをアプリケーションのプライマリAPIキーの代わりに使用することはできません。

クライアントに登録できるよう、コールバックURLをDiditに伝えてください。
Oktaはこれを`{yourOktaOrgUrl}/idp/identity-verification/callback`として生成します。

## Auth0の設定

Auth0にはネイティブのIDVベンダー枠がないため、検証は、ユーザーを同じフローにリダイレクトするpost-loginアクションから実行されます。
`DIDIT_IDV_BASE`、`DIDIT_CLIENT_ID`、`DIDIT_CLIENT_SECRET`をアクションのシークレットとして保存し、`https://{yourAuth0Domain}/continue`をリダイレクトURIとしてDiditに登録してください。

> [!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`は、単一の`app_metadata`フィールドではなく、ログイントランザクションごとに(`event.transaction.id`をキーとして)保存してください。
> ユーザーが再試行したり、ログインを重複して実行したりすると、共有の単一フィールドが上書きされ、トークン交換が`invalid_grant`で失敗します。

## エラー

エラーはRFC 6749およびRFC 9126に準拠します。
クライアント認証に失敗した場合は`401`と`{"error": "invalid_client"}`が返されます。期限切れまたは再利用された`request_uri`や認可コードの場合は、`400`と`invalid_request`または`invalid_grant`が返されます。

認可コード、`request_uri`の値、検証セッションはいずれも1回限り使用できます。


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