OIDC Automatic Configuration

Face Auth 2.0 combines OpenID Connect (OIDC) with Incode's biometric verification pipeline. The result is a standard OAuth 2.0 authorization code flow — augmented with PKCE — where the authorization server performs liveness detection, ID document validation, and face recognition before issuing tokens.


Prerequisites

Before integrating Face Auth 2.0, you will need the following values from your Incode dashboard and OIDC configuration:

ValueDescription
AUTH_SERVER_URLBase URL of the OIDC authorization server (e.g. https://auth.demo.incode.com). Also used as the issuer value for ID token validation.
SERVER_URLBase URL of the Incode API server
CLIENT_IDYour registered OIDC client ID
REDIRECT_URIThe redirect URI registered with your OIDC client
API_KEYYour Incode API key
ADMIN_EMAILAdmin account email for the Incode Admin API
ADMIN_PASSWORDAdmin account password for the Incode Admin API
🚧

Keep credentials secure

Never expose ADMIN_EMAIL, ADMIN_PASSWORD, or API_KEY in client-side code. All requests using these values must be made from your server.

Step 1: Generate PKCE Parameters

Purpose: Generate all cryptographic values needed for a secure OIDC authorization request.

Before redirecting the user to the authorization server, your application must generate the following values:

ValueHow it's generated
nonce16 random bytes, base64url-encoded
state16 random bytes, base64url-encoded
code_verifier32 random bytes, base64url-encoded (must be 43–128 chars, per PKCE spec)
code_challengeSHA-256 hash of code_verifier, base64url-encoded

Store these values server-side — you will need code_verifier, nonce, and state in later steps.

Example (JavaScript)

const crypto = require('crypto');

function base64UrlEncode(buffer) {
  return buffer.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

const nonce        = base64UrlEncode(crypto.randomBytes(16));
const state        = base64UrlEncode(crypto.randomBytes(16));
const codeVerifier = base64UrlEncode(crypto.randomBytes(32));
const codeChallenge = base64UrlEncode(
  crypto.createHash('sha256').update(codeVerifier).digest()
);

Authorization URL structure:

GET {AUTH_SERVER_URL}/oauth2/authorize
  ?client_id={CLIENT_ID}
  &redirect_uri={REDIRECT_URI}
  &scope=openid
  &response_type=code
  &response_mode=form_post
  &code_challenge_method=S256
  &code_challenge={code_challenge}
  &state={state}
  &nonce={nonce}
🚧

Persist your code_verifier

The code_verifier must be securely stored (e.g. in a server-side session) until the token exchange in Step 3. Do not regenerate or overwrite it between steps.

Step 2: Redirect the User to the Authorization URL

Purpose: Send the user to the authorization server to complete face authentication.

Using the authorization URL built in Step 1, redirect the user's browser to begin the face authentication flow. The authorization server will guide the user through:

  1. Liveness detection
  2. ID document capture and validation
  3. Face recognition (matching selfie to document photo)

Example (JavaScript)

const params = new URLSearchParams({
  client_id:             CLIENT_ID,
  redirect_uri:          REDIRECT_URI,
  scope:                 'openid',
  response_type:         'code',
  response_mode:         'form_post',
  code_challenge_method: 'S256',
  code_challenge:        codeChallenge,
  state:                 state,
  nonce:                 nonce,
});

const authorizeUrl = `${AUTH_SERVER_URL}/oauth2/authorize?${params.toString()}`;

// Redirect the user
window.location.href = authorizeUrl;

On success, the authorization server will POST to your REDIRECT_URI with the following form-encoded fields:

FieldDescription
codeA short-lived authorization code to exchange for tokens
stateThe state value you sent — verify this matches what you stored in Step 1
📘

response_mode=form_post

The code and state are delivered as a POST to your REDIRECT_URI, not as URL query parameters. Make sure your redirect endpoint can receive a form-encoded POST body.

🚧

Validate state on return

Always verify that the state value returned by the server matches the one you generated in Step 1. A mismatch may indicate a CSRF attack.

Step 3: Exchange the Authorization Code for Tokens

Endpoint: POST {AUTH_SERVER_URL}/oauth2/token

Once your redirect endpoint receives the code, your server must exchange it for an access token and ID token.

Request

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
ParameterValue
grant_typeauthorization_code
client_idYOUR_CLIENT_ID
redirect_uriYOUR_REDIRECT_URI
codeThe authorization code received from Step 2
code_verifierThe code_verifier generated in Step 1

Example (JavaScript)

const response = await fetch(`${AUTH_SERVER_URL}/oauth2/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type:    'authorization_code',
    client_id:     CLIENT_ID,
    redirect_uri:  REDIRECT_URI,
    code:          authorizationCode,
    code_verifier: codeVerifier,
  }),
});

const tokens = await response.json();

Response

{
  "access_token": "<JWT>",
  "id_token": "<JWT>",
	"scope": "openid",
  "token_type": "Bearer",
  "expires_in": 3600
}

ID Token Validation

After receiving the ID token, your application is responsible for validating it before trusting any of its claims. Use a well-maintained JWT library rather than implementing validation manually.

Recommended libraries:

LanguageLibrary
JavaScript / Node.jsjose, jsonwebtoken
Pythonpython-jose, authlib
Javanimbus-jose-jwt
Gogolang-jwt/jwt
Rubyjwt

Claims to validate:

ClaimExpected ValueWhy
issMust exactly match your AUTH_SERVER_URLConfirms the token was issued by the authorization server you trust
expMust be in the future (compare against server time)Rejects expired tokens
nonceMust match the nonce generated in Step 1Confirms the token was issued in response to your specific request — protects against replay attacks
azpMust equal CLIENT_ID when multiple audiences are presentConfirms your app is the intended recipient of the token
kidMust be present in the JWKS at {AUTH_SERVER_URL}/oauth2/jwksUsed to verify the token's cryptographic signature

Verifying the token signature (JWKS):

The kid value in the token header identifies which public key to use for signature verification. Fetch Incode's public keys and find the matching key:

GET {AUTH_SERVER_URL}/oauth2/jwks

Example (JavaScript using jose)

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL(`${AUTH_SERVER_URL}/oauth2/jwks`)
);

const { payload } = await jwtVerify(idToken, JWKS, {
  issuer:   AUTH_SERVER_URL,
  audience: CLIENT_ID,
});

// Validate nonce manually
if (payload.nonce !== storedNonce) {
  throw new Error('Nonce mismatch — possible replay attack');
}

Codes are single-use

Authorization codes expire quickly (typically 60 seconds) and can only be exchanged once. If the token exchange fails, the user will need to restart the authentication flow from Step 1.

Step 4: Retrieve the Session / Interview ID

Endpoint: GET {AUTH_SERVER_URL}/userinfo

Once your application has a valid access token, call the OIDC UserInfo endpoint to retrieve the user's session details. The sub claim returned contains the Incode session identifier, referred to as interviewId, which is required to retrieve verification results in Step 5b.

Request

GET /userinfo
Authorization: Bearer YOUR_ACCESS_TOKEN

Example (JavaScript)

const response = await fetch(`${AUTH_SERVER_URL}/userinfo`, {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
  },
});

const userInfo = await response.json();
const interviewId = userInfo.sub;

Response (example)

{
  "sub": "667ce6cd1f9dce127eabe35e",
  "email": "[email protected]"
}
FieldDescription
subThe Incode session identifier (interviewId) — store this for Step 5b
emailThe authenticated user's email address
📘

Store the interviewId

The sub value returned here is your key to retrieving the full verification score. Store it securely on your server before proceeding to Step 5.

Step 5a: Create an Admin Token

Endpoint: POST {SERVER_URL}/executive/log-in

Before retrieving the verification score, your server must authenticate with the Incode Admin API to obtain a short-lived, high-privilege admin token.

📘

Full endpoint reference

For the complete request schema, authentication options, and error responses, see the official Admin Login API reference.

🚧

Server-side only

This request must be made from your server. Never expose ADMIN_EMAIL, ADMIN_PASSWORD, or API_KEY in client-side code.

Request

POST /executive/log-in
Content-Type: application/json
api-version: 1.0
x-api-key: YOUR_API_KEY
{
  "email": "YOUR_ADMIN_EMAIL",
  "password": "YOUR_ADMIN_PASSWORD"
}

Response

{
  "token": "<admin-token>"
}
FieldDescription
tokenShort-lived admin token required for the score retrieval request in Step 5b

Step 5b: Retrieve the Verification Score

Endpoint: GET {SERVER_URL}/omni/get/score

Using the interviewId from Step 4 and the adminToken from Step 5a, your server can now retrieve the full biometric verification result for the session.

📘

Full endpoint reference

For the complete request schema, response fields, and status definitions, see the official Get Scores API reference.

Request

GET /omni/get/score?id=YOUR_INTERVIEW_ID
Content-Type: application/json
api-version: 1.0
x-api-key: YOUR_API_KEY
X-Incode-Hardware-Id: YOUR_ADMIN_TOKEN
Query ParameterRequiredDescription
idNoThe interviewId from Step 4. If omitted, the interview ID is extracted from the authentication token.

Example (JavaScript)

const response = await fetch(
  `${SERVER_URL}/omni/get/score?id=${interviewId}`,
  {
    method: 'GET',
    headers: {
      'Content-Type':         'application/json',
      'api-version':          '1.0',
      'x-api-key':            API_KEY,
      'X-Incode-Hardware-Id': adminToken,
    },
  }
);

const score = await response.json();

Response

A successful response returns a 200 OK with a score object containing the following top-level sections:

SectionDescription
overallComposite score and pass/fail status across all checks
idValidationDocument authenticity and quality checks
livenessLiveness detection results
faceRecognitionSelfie-to-ID face match results
deepsightMultimodal fraud intelligence signals
idOcrConfidenceConfidence scores for extracted text fields
governmentValidationGovernment database cross-check results
watchlistScoreWatchlist screening results
antifraudAggregate fraud risk assessment
deviceRiskDevice trust signals
behavioralRiskBehavioral trust signals
reasonMsgHuman-readable explanation of the overall outcome

For the full field definitions, status values, and schema for each section, refer to the official Get Scores API reference.

Error Responses

Status CodeDescription
400Bad request — check that your interviewId, api-version, and auth headers are correct