> ## 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.

# Verifikasi Identitas Okta & Auth0

> Delegasikan verifikasi identitas ke Didit dari Okta (BYO IDV) atau Auth0 lewat OpenID Connect dengan Pushed Authorization Requests.

## Endpoint

Didit menyediakan server otorisasi OpenID Connect yang dibangun khusus untuk verifikasi identitas yang didelegasikan.
Server ini menerapkan kontrak yang diharapkan oleh framework "bring your own identity verification vendor" (BYO IDV) dari Okta: Pushed Authorization Requests (PAR, RFC 9126), authorization code flow dengan PKCE, dan ID token yang membawa objek OpenID Connect `verified_claims`.

Endpoint yang sama dapat digunakan oleh relying party mana pun yang mengikuti profil ini, termasuk Auth0 post-login Actions.

Semua endpoint berada di bawah URL dasar jembatan verifikasi identitas.

| Lingkungan | URL dasar |
| - | - |
| Produksi | `https://apx.didit.me/auth/idv` |
| Sandbox | `https://apx.didit.me/auth/idv` |

Produksi dan sandbox menggunakan endpoint yang sama.
Aplikasi yang terkait dengan kredensial klien Anda menentukan lingkungannya.
Gunakan kredensial klien terpisah dan alur kerja milik aplikasi yang sesuai untuk setiap lingkungan.
Lihat [Sandbox & Test Data](https://docs.didit.me/id/integration/sandbox-testing.md) untuk hasil verifikasi simulasi.

Discovery document di `{base}/.well-known/openid-configuration` mencantumkan seluruh endpoint berikut:

| Tujuan | 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` |

Autentikasi klien menggunakan `client_secret_basic` atau `client_secret_post`.
Authorization code flow mewajibkan PKCE dengan `code_challenge_method=S256`.
ID token ditandatangani dengan RS256; verifikasi menggunakan endpoint JWKS.

## Alur

1. Relying party mengirim permintaan otorisasi ke `POST /par`, termasuk tantangan PKCE, `state`, `nonce`, `redirect_uri` yang terdaftar, scope `openid profile identity_assurance`, dan klaim yang ingin diverifikasi.
   Didit mengembalikan `request_uri` sekali pakai yang berlaku selama 60 detik.
2. Relying party mengarahkan browser pengguna ke `GET /authorize?request_uri=...`.
   Didit membuat sesi verifikasi dan membawa pengguna ke alur verifikasi yang di-hosting.
3. Setelah pengguna menyelesaikan proses, Didit mengevaluasi keputusan verifikasi, membandingkan identitas yang diekstrak dengan klaim yang dikirim, lalu mengembalikan pengguna ke `redirect_uri` relying party dengan `code` sekali pakai dan `state` asli.
4. Relying party menukarkan code di `POST /token` dengan `code_verifier` miliknya dan menerima ID token yang ditandatangani.

## Meminta klaim

Kirim klaim yang akan dibandingkan di dalam parameter `claims` OpenID Connect standar pada permintaan PAR.
Baik `given_name` maupun `family_name` didukung, dan keduanya selalu dibandingkan dengan pencocokan fuzzy.
`verified_claims` berupa array, sesuai dengan profil BYO IDV Okta dan respons ID token di bawah ini.

```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 membawa array `verified_claims`. `assurance_level` bernilai `VERIFIED` hanya jika sesi verifikasi disetujui **dan** setiap klaim yang diminta cocok dengan dokumen yang diverifikasi.
Jika tidak, nilainya adalah `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` adalah ID sesi Didit, sehingga Anda dapat mencari verifikasi tersebut di konsol atau melalui sessions API.

> [!NOTE]
>
> Anggap pengguna telah terverifikasi hanya jika `trust_framework` bernilai `IDV-DELEGATED` **dan** `assurance_level` bernilai `VERIFIED`.
> Kombinasi lain berarti verifikasi tidak berhasil.

## Mengonfigurasi Okta

Minta Didit untuk mengaktifkan verifikasi identitas untuk aplikasi produksi dan sandbox Anda, mengaitkan setiap aplikasi dengan alur kerja verifikasinya, dan mendaftarkan URL callback yang tepat untuk setiap tenant Okta.
Aplikasi atau kunci API yang sudah ada saja tidak mengaktifkan integrasi ini.

Di Okta Admin Console, tambahkan vendor verifikasi identitas khusus lalu isi nilai-nilai di bawah ini.
Okta membuat integrasi ini sebagai identity provider bertipe `IDV_STANDARD` dengan protokol `ID_PROOFING`, lalu Anda merujuknya dari aturan Account Management Policy.

| Kolom | Nilai |
| - | - |
| 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` |
| Scope | `openid`, `profile`, `identity_assurance` |
| Client ID | Client ID dari aplikasi Didit yang sesuai |
| Client secret | kunci API Primary aplikasi tersebut, tersedia di **Developers > API keys** pada Didit Console |

Konfigurasikan produksi dan sandbox sebagai integrasi verifikasi identitas yang terpisah dengan kredensial masing-masing.
Gunakan endpoint di atas untuk keduanya.
Client secret tetap diperlukan meskipun alur ini juga menggunakan PKCE.
Kunci API bernama yang dibuat secara terpisah tidak dapat menggantikan kunci API Primary aplikasi untuk integrasi ini.

Berikan URL callback Anda kepada Didit agar dapat didaftarkan pada klien Anda.
Okta membentuknya sebagai `{yourOktaOrgUrl}/idp/identity-verification/callback`.

## Mengonfigurasi Auth0

Auth0 tidak memiliki slot vendor IDV bawaan, jadi verifikasi dijalankan dari post-login Action yang mengarahkan pengguna ke alur yang sama.
Simpan `DIDIT_IDV_BASE`, `DIDIT_CLIENT_ID` dan `DIDIT_CLIENT_SECRET` sebagai rahasia Action, lalu daftarkan `https://{yourAuth0Domain}/continue` sebagai redirect URI di Didit.

> [!NOTE]
>
> Auth0 menambahkan `state`-nya sendiri ke redirect `/authorize` agar dapat melanjutkan proses login. Didit menggunakan `state` tersebut dan mengembalikannya ke `/continue`, sehingga Auth0 dapat menyelesaikan transaksi - Anda tidak perlu meneruskan state Auth0 di dalam body PAR (state tersebut belum dibuat sampai redirect terjadi).

```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]
>
> Simpan PKCE `code_verifier` per transaksi login (dengan kunci `event.transaction.id`), bukan di satu kolom `app_metadata`.
> Jika pengguna mencoba ulang atau menjalankan login yang berlangsung bersamaan, satu kolom bersama akan tertimpa dan pertukaran token gagal dengan `invalid_grant`.

## Galat

Galat mengikuti RFC 6749 dan RFC 9126.
Autentikasi klien yang gagal mengembalikan `401` dengan `{"error": "invalid_client"}`; `request_uri` atau kode otorisasi yang sudah kedaluwarsa atau digunakan ulang mengembalikan `400` dengan `invalid_request` atau `invalid_grant`.

Kode otorisasi, nilai `request_uri`, dan sesi verifikasi semuanya hanya dapat digunakan sekali.


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