# Auth0 Post-Login Action

You can integrate Incode identity verification (IDV) into your Auth0 tenant using a post-login Action. When a user logs in, they are redirected to Incode to complete identity verification. The results are stored in their Auth0 profile and included in the ID token.

This integration uses the Incode OIDC solution and Auth0 Actions. It supports on-the-fly identity creation, reverification periods, and face authentication for returning users.

***

## Prerequisites

Ensure you have the following before you begin:

- An Auth0 tenant with Actions enabled.
- Access to the Integrations page in Dashboard. Contact your Incode Representative if you do not see it.
- An Incode OIDC Client ID and Client Secret—see [Find Integration Details](https://developer.incode.com/docs/manage-integrations#find-integration-details)​.
- Your Incode auth server URL (`https://auth.demo.incode.com` for demo, `https://auth.incode.com` for production).

***

## Integration Flow by Persona

- **First login (new user)**: The user is redirected to Incode for full identity verification—ID document capture, liveness detection, and face match. Results are stored in `app_metadata` and included in the ID token.
- **Returning user within the reverification window**: IDV is skipped. Existing verification claims are re-stamped onto the ID token.
- **Returning user requiring reverification**: The user is sent back to Incode. Their stored `incode_identity_id` is passed as the `login_hint`, enabling face authentication instead of full IDV.

***

## Set Up the Auth0 Integration

### Register the Auth0 Redirect URI in Dashboard

Before creating the Auth0 action, add Auth0's callback URL as an allowed redirect URI in your Incode OIDC integration.

1. Log in to Dashboard.
2. In the left menu, click **Integrations**.
3. From the Custom tab, open your **OIDC** integration.
4. Add the following as an allowed **Redirect URI**: `https://YOUR_AUTH0_DOMAIN/continue`.
5. Replace `YOUR_AUTH0_DOMAIN` with your Auth0 tenant domain: for example, `your-tenant.us.auth0.com`.

### Create the Post-Login Action in Auth0

1. In your Auth0 Dashboard, go to **Actions** > **Library**.
2. Click **Build Custom Action**.
3. Enter a name: for example, `Incode IDV`.
4. Select **Login/Post Login** as the trigger.
5. Click **Create**.
6. Paste the action code below into the editor, then click **Save Draft**.
   ```javascript
   /**
    * Auth0 Post-Login Action — Incode Identity Verification (OIDC)
    * Trigger: post-login
    */

   const DEFAULT_AUTH_SERVER = "https://auth.demo.incode.com";

   function needsVerification(event, reverificationHours) {
     const meta = event.user.app_metadata || {};
     if (!meta.incode_idv_completed) return true;
     const lastVerified = meta.incode_idv_completed_at;
     if (!lastVerified) return true;
     if (reverificationHours === 0) return false;
     const hoursSince = (Date.now() - new Date(lastVerified).getTime()) / 36e5;
     return hoursSince >= reverificationHours;
   }

   async function exchangeCodeForTokens(code, redirectUri, tokenEndpoint, event) {
     const body = new URLSearchParams({
       grant_type:    "authorization_code",
       code,
       redirect_uri:  redirectUri,
       client_id:     event.secrets.INCODE_CLIENT_ID,
       client_secret: event.secrets.INCODE_CLIENT_SECRET,
     });
     const resp = await fetch(tokenEndpoint, {
       method:  "POST",
       headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" },
       body: body.toString(),
     });
     if (!resp.ok) {
       const err = await resp.text();
       throw new Error(`Incode token exchange failed (${resp.status}): ${err}`);
     }
     return resp.json();
   }

   async function fetchUserInfo(accessToken, userInfoEndpoint) {
     const resp = await fetch(userInfoEndpoint, {
       headers: { Authorization: `Bearer ${accessToken}` },
     });
     if (!resp.ok) {
       const err = await resp.text();
       throw new Error(`Incode userinfo fetch failed (${resp.status}): ${err}`);
     }
     return resp.json();
   }

   exports.onExecutePostLogin = async (event, api) => {
     const authServer          = event.secrets.INCODE_SERVER_URL || DEFAULT_AUTH_SERVER;
     const authEndpoint        = `${authServer}/oauth2/authorize`;
     const reverificationHours = parseFloat(event.secrets.REVERIFICATION_HOURS || "720");
     const scopes              = event.secrets.SCOPES || "openid";
     const auth0Domain         = event.secrets.AUTH0_DOMAIN;
     const blockOnFailure      = (event.secrets.BLOCK_ON_FAILURE || "false").toLowerCase() === "true";

     if (!needsVerification(event, reverificationHours)) {
       const meta = event.user.app_metadata || {};
       if (meta.incode_idv) {
         api.idToken.setCustomClaim("https://incode.com/idv", meta.incode_idv);
       }
       return;
     }

     const redirectUri      = `https://${auth0Domain}/continue`;
     const storedIdentityId = (event.user.app_metadata || {}).incode_identity_id || null;
     const loginHint        = storedIdentityId || event.user.email || null;

     let sessionToken;
     try {
       sessionToken = api.redirect.encodeToken({
         secret:           event.secrets.REDIRECT_SECRET,
         expiresInSeconds: 3600,
         payload: { userId: event.user.user_id },
       });
     } catch (err) {
       console.error("[Incode IDV] encodeToken failed:", err.message);
       if (blockOnFailure) api.access.deny("identity_verification_failed");
       return;
     }

     const authParams = new URLSearchParams({
       response_type: "code",
       client_id:     event.secrets.INCODE_CLIENT_ID,
       redirect_uri:  redirectUri,
       scope:         scopes,
       state:         sessionToken,
       ...(loginHint ? { login_hint: loginHint } : {}),
     });

     api.redirect.sendUserTo(`${authEndpoint}?${authParams.toString()}`);
   };

   exports.onContinuePostLogin = async (event, api) => {
     const authServer       = event.secrets.INCODE_SERVER_URL || DEFAULT_AUTH_SERVER;
     const tokenEndpoint    = `${authServer}/oauth2/token`;
     const userInfoEndpoint = `${authServer}/userinfo`;
     const blockOnFailure   = (event.secrets.BLOCK_ON_FAILURE || "false").toLowerCase() === "true";
     const auth0Domain      = event.secrets.AUTH0_DOMAIN;
     const redirectUri      = `https://${auth0Domain}/continue`;

     const rawCode             = event.request?.query?.code;
     const rawError            = event.request?.query?.error;
     const rawErrorDescription = event.request?.query?.error_description;

     if (!rawCode) {
       console.error("[Incode IDV] No code returned. Error:", rawError, rawErrorDescription);
       api.user.setAppMetadata("incode_idv_completed", false);
       api.user.setAppMetadata("incode_idv_error", rawError
         ? `${rawError}: ${rawErrorDescription}`
         : "no_code_returned"
       );
       if (blockOnFailure) api.access.deny("identity_verification_failed");
       return;
     }

     let tokenResponse;
     try {
       tokenResponse = await exchangeCodeForTokens(rawCode, redirectUri, tokenEndpoint, event);
     } catch (err) {
       console.error("[Incode IDV] Token exchange error:", err.message);
       api.user.setAppMetadata("incode_idv_completed", false);
       api.user.setAppMetadata("incode_idv_error", err.message);
       if (blockOnFailure) api.access.deny("identity_verification_failed");
       return;
     }

     let userInfo = {};
     try {
       userInfo = await fetchUserInfo(tokenResponse.access_token, userInfoEndpoint);
     } catch (err) {
       console.warn("[Incode IDV] UserInfo fetch failed (non-fatal):", err.message);
     }

     let idTokenClaims = {};
     try {
       const idTokenPayload = tokenResponse.id_token.split(".")[1];
       idTokenClaims = JSON.parse(Buffer.from(idTokenPayload, "base64url").toString("utf8"));
     } catch (err) {
       console.warn("[Incode IDV] ID token parse failed (non-fatal):", err.message);
     }

     const now         = new Date().toISOString();
     const interviewId = idTokenClaims.interview_id || userInfo.interview_id || null;
     const identityId  = idTokenClaims.identity_id  || userInfo.identity_id  || userInfo.sub || null;

     const pii = {
       ...(userInfo.name         ? { name:         userInfo.name }         : {}),
       ...(userInfo.given_name   ? { given_name:   userInfo.given_name }   : {}),
       ...(userInfo.family_name  ? { family_name:  userInfo.family_name }  : {}),
       ...(userInfo.birthdate    ? { birthdate:    userInfo.birthdate }    : {}),
       ...(userInfo.email        ? { email:        userInfo.email }        : {}),
       ...(userInfo.phone_number ? { phone_number: userInfo.phone_number } : {}),
       ...(userInfo.address      ? { address:      userInfo.address }      : {}),
     };

     api.user.setAppMetadata("incode_idv", {
       id_token:            tokenResponse.id_token     || null,
       access_token:        tokenResponse.access_token || null,
       token_type:          tokenResponse.token_type   || null,
       expires_in:          tokenResponse.expires_in   || null,
       interview_id:        interviewId,
       identity_id:         identityId,
       auth_overall_score:  idTokenClaims.auth_overall_score  || null,
       auth_overall_status: idTokenClaims.auth_overall_status || null,
       pii,
       userinfo:            userInfo,
     });

     api.user.setAppMetadata("incode_idv_completed",    true);
     api.user.setAppMetadata("incode_idv_completed_at", now);
     api.user.setAppMetadata("incode_idv_error",        null);

     if (interviewId) api.user.setAppMetadata("incode_interview_id", interviewId);
     if (identityId)  api.user.setAppMetadata("incode_identity_id",  identityId);

     api.idToken.setCustomClaim("https://incode.com/idv", {
       completed:           true,
       completed_at:        now,
       interview_id:        interviewId,
       identity_id:         identityId,
       auth_overall_score:  idTokenClaims.auth_overall_score  || null,
       auth_overall_status: idTokenClaims.auth_overall_status || null,
       pii,
     });
   };
   ```

### Configure Action Secrets

In the Action editor, click the key icon in the left sidebar to open the Secrets panel. Add the following secrets:

<Table>
  <thead>
    <tr>
      <th>
        Secret key
      </th>

      <th>
        Description
      </th>

      <th>
        Example value
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `INCODE_CLIENT_ID`
      </td>

      <td>
        Your Incode OIDC Client ID
      </td>

      <td>
        `4cacb025...`
      </td>
    </tr>

    <tr>
      <td>
        `INCODE_CLIENT_SECRET`
      </td>

      <td>
        Your Incode OIDC Client Secret
      </td>

      <td>
        `your-secret`
      </td>
    </tr>

    <tr>
      <td>
        `INCODE_SERVER_URL`
      </td>

      <td>
        Incode auth server base URL
      </td>

      <td>
        Use `https://auth.demo.incode.com` for demo and `https://auth.incode.com` for production.
      </td>
    </tr>

    <tr>
      <td>
        `REDIRECT_SECRET`
      </td>

      <td>
        Random 32+ character string for signing session tokens
      </td>

      <td>
        Run the following in your browser console to generate a secure random value:

        ```javascript
        crypto.getRandomValues(new Uint8Array(32)).reduce((a,b) => a + b.toString(16).padStart(2,'0'), '')
        ```
      </td>
    </tr>

    <tr>
      <td>
        `AUTH0_DOMAIN`
      </td>

      <td>
        Your Auth0 tenant domain
      </td>

      <td>
        `your-tenant.us.auth0.com`
      </td>
    </tr>

    <tr>
      <td>
        `REVERIFICATION_HOURS`
      </td>

      <td>
        Hours between re-verifications (`0` = never re-verify)
      </td>

      <td>
        `720`
      </td>
    </tr>

    <tr>
      <td>
        `SCOPES`
      </td>

      <td>
        Space-separated OIDC scopes
      </td>

      <td>
        `openid`
      </td>
    </tr>

    <tr>
      <td>
        `BLOCK_ON_FAILURE`
      </td>

      <td>
        Set to `true` to deny login if IDV fails
      </td>

      <td>
        `true` or `false`
      </td>
    </tr>
  </tbody>
</Table>

### Deploy the Action

After adding all secrets, click **Deploy** in the top right corner of the Action editor. Saving the draft alone is not sufficient; the Action must be deployed before it will run.

### Attach the Action to the Login Flow

1. In the Auth0 Dashboard, go to **Actions** > **Triggers**.
2. Click **post-login**.
3. In the right sidebar under **Custom**, find your `Incode IDV` action.
4. Drag it into the pipeline between **Start** and **Complete**.
5. Click **Apply** to save the flow.

***

## Stored Data

After a successful verification, the following is written to the user's `app_metadata` in Auth0:

```json
{
  "incode_idv_completed": true,
  "incode_idv_completed_at": "2026-04-07T17:28:25.150Z",
  "incode_interview_id": "69d53e7a...",
  "incode_identity_id": "69d52eca...",
  "incode_idv": {
    "interview_id": "69d53e7a...",
    "identity_id": "69d52eca...",
    "auth_overall_score": "100.0",
    "auth_overall_status": "OK",
    "pii": {},
    "userinfo": { "sub": "69d53e7a..." }
  }
}
```

The following custom claim is also added to the Auth0 ID token under the `https://incode.com/idv` namespace:

```json
{
  "completed": true,
  "completed_at": "2026-04-07T17:28:25.150Z",
  "interview_id": "69d53e7a...",
  "identity_id": "69d52eca...",
  "auth_overall_score": "100.0",
  "auth_overall_status": "OK"
}
```

***

## Reverification Period

The `REVERIFICATION_HOURS` secret controls how often users must re-verify.

| Value  | Behavior                                      |
| ------ | --------------------------------------------- |
| `0`    | Never re-verify after completing once         |
| `24`   | Re-verify every 24 hours                      |
| `720`  | Re-verify every 30 days (recommended default) |
| `8760` | Re-verify once per year                       |

***

## PII Claims

By default, the `openid` scope returns only basic identity identifiers. To receive personally identifiable information (PII) such as name, date of birth, email, and address, contact your Incode Representative to enable additional scopes on your OIDC client, then update the `SCOPES` secret accordingly.

| Scope                | Data returned                           |
| -------------------- | --------------------------------------- |
| `profile`            | Name, date of birth                     |
| `email`              | Email address                           |
| `phone`              | Phone number                            |
| `address`            | Physical address                        |
| `id_attestation`     | ID document data                        |
| `identity_assurance` | Assurance level and verification status |

Example `SCOPES` value with additional scopes enabled:

```
openid profile email id_attestation
```

***

## Troubleshooting

### **IDV Is Skipped for a User I Expect to be Re-Verified**

The user has completed IDV and is within the reverification window. To force re-verification for testing, set `REVERIFICATION_HOURS` to a small value like `1`, or clear `incode_idv_completed` and `incode_idv_completed_at` from the user's `app_metadata` in **Auth0 Dashboard > User Management > Users**.

### **Error:&#x20;**`invalid_scope`

The scope you are requesting is not enabled on your Incode OIDC client. Revert `SCOPES` to `openid` and contact your Incode Representative to enable additional scopes.

### **Error:&#x20;**`invalid_client`

The `INCODE_CLIENT_ID` or `INCODE_CLIENT_SECRET` does not match what is registered in Incode, or `INCODE_SERVER_URL` is pointing to the wrong environment. Verify your credentials in Dashboard and confirm you are using the correct server URL.

### **Error:&#x20;**`identity_verification_failed`

Incode returned an error during the authorization or token exchange step. Check the Auth0 action logs under **Monitoring > Logs** for the specific error. Common causes are an expired authorization code (codes are single-use and expire in approximately 60 seconds) or a redirect URI mismatch.

### **Error:&#x20;**`no_code_returned`

Incode did not return an authorization code. The IDV session may have failed or the user abandoned the flow. If `BLOCK_ON_FAILURE` is `true`, the user is denied login. Check the `incode_idv_error` field in `app_metadata` for the specific error.

***

## OIDC Endpoints Reference

| Endpoint      | Demo                                                            | Production                                                 |
| ------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
| Authorization | `https://auth.demo.incode.com/oauth2/authorize`                 | `https://auth.incode.com/oauth2/authorize`                 |
| Token         | `https://auth.demo.incode.com/oauth2/token`                     | `https://auth.incode.com/oauth2/token`                     |
| UserInfo      | `https://auth.demo.incode.com/userinfo`                         | `https://auth.incode.com/userinfo`                         |
| JWKS          | `https://auth.demo.incode.com/oauth2/jwks`                      | `https://auth.incode.com/oauth2/jwks`                      |
| Discovery     | `https://auth.demo.incode.com/.well-known/openid-configuration` | `https://auth.incode.com/.well-known/openid-configuration` |

<br />
