Auth0 Post-Login Action

Integrate Incode identity verification into your Auth0 tenant using a post-login Action. When a user logs in, they are automatically 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

Before you begin, ensure you have:

  • An Auth0 tenant with Actions enabled
  • Access to the Integrations page in Dashboard. Contact your Incode Representative if you do not see the Integrations page.
  • An Incode OIDC Client ID and Client Secret — see Find Integration Details
  • Your Incode auth server URL (https://auth.demo.incode.com for demo, https://auth.incode.com for production)

How It Works

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 Guide

Step 1: Register the Auth0 Redirect URI in Incode

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 navigation, 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.

Step 2: Create the Post-Login Action in Auth0

  1. In your Auth0 Dashboard, navigate to Actions > Library.
  2. Click Build Custom Action.
  3. Give it 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.
  7. Click Save Draft.
/**
 * 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,
  });
};

Step 3: Configure Action Secrets

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

Secret keyDescriptionExample value
INCODE_CLIENT_IDYour Incode OIDC Client ID4cacb025...
INCODE_CLIENT_SECRETYour Incode OIDC Client Secretyour-secret
INCODE_SERVER_URLIncode auth server base URLhttps://auth.demo.incode.com
REDIRECT_SECRETRandom 32+ character string for signing session tokensSee note below
AUTH0_DOMAINYour Auth0 tenant domainyour-tenant.us.auth0.com
REVERIFICATION_HOURSHours between re-verifications (0 = never re-verify)720
SCOPESSpace-separated OIDC scopesopenid
BLOCK_ON_FAILURESet to true to deny login if IDV failstrue or false

Generating a REDIRECT_SECRET**: Run the following in your browser console to generate a secure random value:

crypto.getRandomValues(new Uint8Array(32)).reduce((a,b) => a + b.toString(16).padStart(2,'0'), '')

INCODE_SERVER_URL values**: Use https://auth.demo.incode.com for demo and https://auth.incode.com for production.

Step 4: 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.

Step 5: Attach the Action to the Login Flow

  1. In the Auth0 Dashboard, navigate 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.

What Gets Stored

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

{
  "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:

{
  "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.

ValueBehavior
0Never re-verify once completed
24Re-verify every 24 hours
720Re-verify every 30 days (recommended default)
8760Re-verify once per year

PII Claims

By default, the openid scope returns only basic identity identifiers. To receive 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.

ScopeData returned
profileName, date of birth
emailEmail address
phonePhone number
addressPhysical address
id_attestationID document data
identity_assuranceAssurance 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 already 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: 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: 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 the Incode Dashboard and confirm you are using the correct server URL.

Error: 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: 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 will be denied login. Check the incode_idv_error field in app_metadata for the specific error.

OIDC Endpoints Reference

EndpointDemoProduction
Authorizationhttps://auth.demo.incode.com/oauth2/authorizehttps://auth.incode.com/oauth2/authorize
Tokenhttps://auth.demo.incode.com/oauth2/tokenhttps://auth.incode.com/oauth2/token
UserInfohttps://auth.demo.incode.com/userinfohttps://auth.incode.com/userinfo
JWKShttps://auth.demo.incode.com/oauth2/jwkshttps://auth.incode.com/oauth2/jwks
Discoveryhttps://auth.demo.incode.com/.well-known/openid-configurationhttps://auth.incode.com/.well-known/openid-configuration