> ## 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 신원 확인 연동

> Okta Identity Engine(BYO IDV) 또는 Auth0에서 Didit에 신원 확인을 위임합니다. Pushed Authorization Requests를 적용한 OpenID Connect를 사용합니다.

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 Action도 포함됩니다.

## 엔드포인트

모든 엔드포인트는 신원 확인 브리지 기본 URL 아래에 있습니다.

| 환경 | 기본 URL |
| - | - |
| 프로덕션 | `https://apx.didit.me/auth/idv` |
| 샌드박스 | `https://apx.didit.me/auth/idv` |

프로덕션과 샌드박스는 같은 엔드포인트를 사용합니다.
클라이언트 자격 증명과 연결된 애플리케이션이 환경을 결정합니다.
각 환경마다 해당 애플리케이션에 속한 별도의 클라이언트 자격 증명과 워크플로를 사용하세요.
시뮬레이션된 인증 결과는 [Sandbox & Test Data](https://docs.didit.me/ko/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 challenge, `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 설정

Didit에 프로덕션 및 샌드박스 애플리케이션의 신원 확인 기능을 활성화하고, 각 애플리케이션을 해당 인증 워크플로와 연결하며, 각 Okta 테넌트의 정확한 콜백 URL을 등록해 달라고 요청하세요.
기존 애플리케이션이나 API 키만으로는 이 연동이 활성화되지 않습니다.

Okta Admin Console에서 사용자 지정 신원 확인 벤더를 추가하고 아래 값을 입력하세요.
Okta는 `IDV_STANDARD` 유형과 `ID_PROOFING` 프로토콜을 가진 아이덴티티 공급자로 연동을 생성하며, 이후 Account Management Policy 규칙에서 이를 참조합니다.

| 필드 | 값 |
| - | - |
| Issuer | `https://apx.didit.me/auth/idv` |
| PAR 요청 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` |
| 스코프 | `openid`, `profile`, `identity_assurance` |
| Client ID | 해당 Didit 애플리케이션의 클라이언트 ID |
| Client secret | 해당 애플리케이션의 기본 API 키. Didit Console의 **Developers > API keys**에서 확인할 수 있습니다 |

프로덕션과 샌드박스는 각각의 자격 증명을 사용하는 별도의 신원 확인 연동으로 구성하세요.
두 환경 모두 위의 엔드포인트를 사용합니다.
이 흐름은 PKCE를 사용하지만 클라이언트 시크릿은 여전히 필요합니다.
이름을 지정해 만든 API 키는 이 연동에서 애플리케이션의 기본 API 키를 대신할 수 없습니다.

Didit가 클라이언트에 등록할 수 있도록 콜백 URL을 전달하세요.
Okta는 이를 `{yourOktaOrgUrl}/idp/identity-verification/callback` 형식으로 구성합니다.

## Auth0 설정

Auth0에는 기본 제공되는 IDV 벤더 슬롯이 없으므로, 인증은 사용자를 같은 흐름으로 보내는 post-login Action에서 실행됩니다.
`DIDIT_IDV_BASE`, `DIDIT_CLIENT_ID`, `DIDIT_CLIENT_SECRET`을 Action 시크릿으로 저장하고, `https://{yourAuth0Domain}/continue`를 Didit에 리디렉션 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`는 단일 `app_metadata` 필드가 아니라 로그인 트랜잭션별로(`event.transaction.id`를 키로 사용하여) 저장하세요.
> 사용자가 재시도하거나 로그인이 겹치면 공유 필드 하나가 덮어쓰여 토큰 교환이 `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.