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:
| Value | Description |
|---|---|
AUTH_SERVER_URL | Base URL of the OIDC authorization server (e.g. https://auth.demo.incode.com). Also used as the issuer value for ID token validation. |
SERVER_URL | Base URL of the Incode API server |
CLIENT_ID | Your registered OIDC client ID |
REDIRECT_URI | The redirect URI registered with your OIDC client |
API_KEY | Your Incode API key |
ADMIN_EMAIL | Admin account email for the Incode Admin API |
ADMIN_PASSWORD | Admin account password for the Incode Admin API |
Keep credentials secureNever expose
ADMIN_EMAIL,ADMIN_PASSWORD, orAPI_KEYin 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:
| Value | How it's generated |
|---|---|
nonce | 16 random bytes, base64url-encoded |
state | 16 random bytes, base64url-encoded |
code_verifier | 32 random bytes, base64url-encoded (must be 43–128 chars, per PKCE spec) |
code_challenge | SHA-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 yourcode_verifierThe
code_verifiermust 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:
- Liveness detection
- ID document capture and validation
- 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:
| Field | Description |
|---|---|
code | A short-lived authorization code to exchange for tokens |
state | The state value you sent — verify this matches what you stored in Step 1 |
response_mode=form_postThe
codeandstateare delivered as aPOSTto yourREDIRECT_URI, not as URL query parameters. Make sure your redirect endpoint can receive a form-encoded POST body.
Validatestateon returnAlways verify that the
statevalue 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
| Parameter | Value |
|---|---|
grant_type | authorization_code |
client_id | YOUR_CLIENT_ID |
redirect_uri | YOUR_REDIRECT_URI |
code | The authorization code received from Step 2 |
code_verifier | The 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:
| Language | Library |
|---|---|
| JavaScript / Node.js | jose, jsonwebtoken |
| Python | python-jose, authlib |
| Java | nimbus-jose-jwt |
| Go | golang-jwt/jwt |
| Ruby | jwt |
Claims to validate:
| Claim | Expected Value | Why |
|---|---|---|
iss | Must exactly match your AUTH_SERVER_URL | Confirms the token was issued by the authorization server you trust |
exp | Must be in the future (compare against server time) | Rejects expired tokens |
nonce | Must match the nonce generated in Step 1 | Confirms the token was issued in response to your specific request — protects against replay attacks |
azp | Must equal CLIENT_ID when multiple audiences are present | Confirms your app is the intended recipient of the token |
kid | Must be present in the JWKS at {AUTH_SERVER_URL}/oauth2/jwks | Used 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-useAuthorization 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]"
}| Field | Description |
|---|---|
sub | The Incode session identifier (interviewId) — store this for Step 5b |
email | The authenticated user's email address |
Store theinterviewIdThe
subvalue 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 referenceFor the complete request schema, authentication options, and error responses, see the official Admin Login API reference.
Server-side onlyThis request must be made from your server. Never expose
ADMIN_EMAIL,ADMIN_PASSWORD, orAPI_KEYin 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>"
}| Field | Description |
|---|---|
token | Short-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 referenceFor 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 Parameter | Required | Description |
|---|---|---|
id | No | The 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:
| Section | Description |
|---|---|
overall | Composite score and pass/fail status across all checks |
idValidation | Document authenticity and quality checks |
liveness | Liveness detection results |
faceRecognition | Selfie-to-ID face match results |
deepsight | Multimodal fraud intelligence signals |
idOcrConfidence | Confidence scores for extracted text fields |
governmentValidation | Government database cross-check results |
watchlistScore | Watchlist screening results |
antifraud | Aggregate fraud risk assessment |
deviceRisk | Device trust signals |
behavioralRisk | Behavioral trust signals |
reasonMsg | Human-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 Code | Description |
|---|---|
400 | Bad request — check that your interviewId, api-version, and auth headers are correct |
Updated about 3 hours ago
