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

| 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)**

```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:**

```undefined
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)**

```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_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**

```undefined
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)**

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

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

```undefined
GET {AUTH_SERVER_URL}/oauth2/jwks
```

**Example (JavaScript using `jose`)**

```javascript
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**

```undefined
GET /userinfo
Authorization: Bearer YOUR_ACCESS_TOKEN
```

**Example (JavaScript)**

```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)**

```json
{
  "sub": "667ce6cd1f9dce127eabe35e",
  "email": "user@example.com"
}
```

| Field | Description | 
|-------|-------------| 
| `sub` | The Incode session identifier (`interviewId`) — store this for Step 5b | 
| `email` | The 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.

**Request**

```undefined
POST /executive/log-in
Content-Type: application/json
api-version: 1.0
x-api-key: YOUR_API_KEY
```

JSON

```json
{
  "email": "YOUR_ADMIN_EMAIL",
  "password": "YOUR_ADMIN_PASSWORD"
}
```

**Response**

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

**Request**

```undefined
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)**

```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 [Fetch Scores API reference](https://developer.incode.com/reference/getscores).
