
# FaceVerify

> **📢 Upgrading from older versions?** See the [Migration Guides](docs/README.md#migration-guides) for breaking changes.

This project contains Datachecker's FaceVerify tool, that captures images of faces to be used in liveness detection. The tool only takes a capture once the trigger mechanism is fired.

To perform liveness detection, two slightly different images of the same person are required. For example, when a person moves his/her head slightly this will generate a different image. Therefore, the tool checks difference in movement between frames.

The tool features user challenge-response, namely head pose estimation, in order to prevent video injection attacks.

The tool will be run in the browser and is therefore written in JavaScript.

## Trigger mechanism

The tool performs the following checks:

- Is the environment not too dark (under exposure)?
- Is there a face?
- Is the face occluded?
- Is the detected face not too far?
- Is the detected face not too close?
- Is the face centered?
- Is the image sharp?
- Is there movement?

The movement check will only be used for the second picture. Since the first picture has no other picture to compare to.

## Prerequisites

Please visit [Datachecker API documentation](https://developer.datachecker.nl/).

- Datachecker [OAuth Token](#oauth-token)
- Datachecker [SDK Token](#sdk-token)

## Compatibility

The SDK requires a browser that supports at least ECMAScript 12 (ES12). It is highly recommended to use the latest version of your preferred browser to ensure compatibility and access to the latest features and security updates.

Using the latest browser versions will ensure that all modern JavaScript features required by the SDK are supported.

### Supported browsers and devices

The SDK runs the face landmark model in a Web Worker (preferred) and falls back to the main thread when the worker path is unavailable. The decision is made at runtime by a WebGL2 capability probe, so support is decided by what the device can actually do — not by parsing the User-Agent.

| Platform                                                | Full support (worker, ~30 FPS) | Degraded mode (main thread, throttled) | Refused                        |
| ------------------------------------------------------- | ------------------------------ | -------------------------------------- | ------------------------------ |
| Android Chrome                                          | Last 2 major versions          | Older devices that pass the probe      | No WebGL2 in worker or main    |
| Desktop Chrome / Edge                                   | Last 2 major versions          | —                                      | —                              |
| Desktop Firefox                                         | Last 2 ESR + current           | —                                      | —                              |
| iOS Safari (and all iOS browsers — they all use WebKit) | iOS 17.0 +                     | iOS 16 (CPU delegate, ~10 FPS)         | Device fails the runtime probe |
| macOS Safari                                            | 17.0 +                         | 16.x                                   | Device fails the runtime probe |

**Degraded mode** runs `FaceLandmarker.detectForVideo()` synchronously on the main thread. The flow still completes but the camera preview is less smooth — `detectForVideo` blocks the UI thread between frames. This is the only path that works on iOS 16 (iPhone 8 / 8 Plus / X), where Safari does not support WebGL2 inside Web Workers (added in Safari 17.0).

**Refused** devices receive `device_error:7001` (`DEVICE_UNSUPPORTED`) on the `onError` callback **before** the detection loop starts — see [Error Codes](#error-codes). Integrators should map this to a clear "device not supported" message rather than offering a retry.

## Steps

1. Request [OAUTH Token](#oauth-token)
2. Put OAuth in header
3. SDK [configuration](#configuration) (add SDK token)
4. Run SDK

## OAuth Token

Datachecker uses OAuth authorization. In order to request the [SDK token](#sdk-token) you will need to provide a valid OAuth token in the header.

Example header:

```javascript
header = {'Authorization': `Bearer ${response.accessToken}`}
```

This OAuth token can be retrieved with the [Datachecker OAuth Token API](https://developer.datachecker.nl/?urls.primaryName=v2#/ProductApi/ProductApi_OAuthToken). The scope `"productapi.sdk.read"` needs to be present to make use of the [SDK token](#sdk-token). If this scope is missing you will not be able to retrieve an SDK token.

FaceVerify also requires the other following scopes to send and receive results: `"productapi.faceverify.write", "productapi.poll.read", "productapi.result.read"`

Example OAuth:

```javascript
fetch(<BASE_ENDPOINT>+"/oauth/token", {
    method: 'POST',
    body: JSON.stringify({
        "clientId": <CLIENTID>,
        "clientSecret": <CLIENTSECRET>,
        "scopes": [
            "productapi.sdk.read",
            "productapi.faceverify.write",
            "productapi.poll.read",
            "productapi.result.read",
        ]
    })
})
.then(response => response.json())
```

Note: Contact Datachecker for *client_id* and *client_secret*.

## SDK Token

The SDK is locked. In order to use the SDK in production a *token* is required. The application can only be started with a valid token. This token is a `base64` string. The token can be generated by calling the [Datachecker SDK Token API](https://developer.datachecker.nl/?urls.primaryName=v2#/ProductApi/ProductApi_SdkToken).

Example:

```javascript
fetch(<BASE_ENDPOINT>+"/sdk/token?number_of_challenges=2&customer_reference=<CUSTOMER>&validateWatermark=true&services=FACE_VERIFY", {
    method: 'GET',
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': `Bearer <ACCESSTOKEN>`
    }
})
.then(response => response.json())
```

## Configuration

To run this tool, you will need initialise with the following variables.

| **ATTRIBUTE**      | **FORMAT**              | **DEFAULT VALUE**                      | **EXAMPLE**                               | **NOTES**                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------ | ----------------------- | -------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASSETS_FOLDER`    | string                  | `""`                                   | `"../"`                                   | **optional**<br> Specifies location of **locally hosted** assets folder. (see [Asset Fetching Configuration](#asset-fetching-configuration))                                                                                                                                                                                                                                                                |
| `ASSETS_MODE`      | string                  | `"CDN"`                                | `"LOCAL"`                                 | **optional**<br> Specifies mode of asset fetching, either through CDN or locally hosted assets. (see [Asset Fetching Configuration](#asset-fetching-configuration))                                                                                                                                                                                                                                         |
| `BACKGROUND_COLOR` | string (Hex color code) | `"#1d3461"`                            | `"#1d3461"`                               | **optional**<br> Specifies the background color using a hex color code.                                                                                                                                                                                                                                                                                                                                     |
| `CONTAINER_ID`     | string                  |                                        | `"FV_mount"`                              | **required**<br> *div id* to mount tool on.                                                                                                                                                                                                                                                                                                                                                                 |
| `DEBUG`            | bool                    | `false`                                | `false`                                   | **optional**<br> When debug is `true` more detailed logs will be visible.                                                                                                                                                                                                                                                                                                                                   |
| `DESKTOP_MODE`     | bool                    | `false`                                | `false`                                   | **optional**<br> Enables all cameras for testing/development purposes. **FOR TESTING ONLY - DO NOT USE IN PRODUCTION.** This mode bypasses camera filtering to allow testing on desktop devices, including virtual cameras. Production environments should always use `false` to ensure only front-facing cameras (user) are available, preventing accidental use of back-facing cameras on mobile devices. |
| `LANGUAGE`         | string                  | `"nl"`                                 | `"nl"`                                    | **required**<br> Notifications in specific language.                                                                                                                                                                                                                                                                                                                                                        |
| `onComplete`       | javascript function     |                                        | `function(data) {console.log(data)}`      | **required**<br> Callback function on *complete*.                                                                                                                                                                                                                                                                                                                                                           |
| `onError`          | javascript function     | `function(error) {console.log(error)}` | `function(error) {console.log(error)}`    | **required**<br> Callback function on *error*.                                                                                                                                                                                                                                                                                                                                                              |
| `onUserExit`       | javascript function     | `function(error) {console.log(error)}` | `function(error) {window.history.back()}` | **required**<br> Callback function on *user exit*.                                                                                                                                                                                                                                                                                                                                                          |
| `TOKEN`            | string                  |                                        | see [SDK Token](#sdk-token)               | **required**<br> Datachecker SDK token.                                                                                                                                                                                                                                                                                                                                                                     |

## Asset fetching Configuration

FaceVerify requires fetching assets, which can be done either through a CDN or by hosting them locally. Configure this in the tool settings as follows:

### CDN Configuration

```javascript
// configuration
{
    ASSETS_MODE: "CDN",
    // other configurations
}
```

### Locally Hosting Configuration

To host assets locally, first copy them to your desired location:

```bash
cp -r dist/assets/ path/to/hosted/assets/
```

Then, configure the tool to use these local assets:

```javascript
// configuration
{
    ASSETS_MODE: "LOCAL",
    ASSETS_FOLDER: "path/to/hosted/assets/",
    // other configurations
}
```

For comprehensive integration examples, please refer to our [Integration Examples](examples/README.md).

### Version Control

To ensure compatibility:

- **Separate Asset Versioning**: The assets directory contains a version file, separate from the main file's version.
- **Compatibility Check**: The main file will perform a version check and throw an error if the versions are incompatible.

## Content Security Policy (CSP)

FaceVerify is designed to work with Content Security Policy (CSP) enabled. The SDK requires specific CSP directives to load assets (e.g., scripts, models, images) and execute WebAssembly for face detection.

### Required CSP Directives

Ensure your CSP policy includes the following directives. Adjust domains based on your environment (e.g., use `https://developer.datachecker.nl` for Datachecker production api).

- `default-src 'self';`
- `script-src 'self' https://cdn.jsdelivr.net 'wasm-unsafe-eval' 'unsafe-inline' blob:;`
- `style-src 'self' 'unsafe-inline';`
- `connect-src 'self' https://developer.datachecker.nl https://cdn.jsdelivr.net data:;`
- `img-src 'self' data: blob: https://cdn.jsdelivr.net;`
- `worker-src 'self' blob:;`
- `object-src 'self' blob:;`
- `frame-src 'self' blob:;`
- `base-uri 'none';`

## Handling callbacks

Within the application, you can take advantage of three callback functions to enhance the user experience and manage the flow of your process.

Note: When integrating the application into Native Apps using web views, it's essential to adapt and utilize these callback functions according to the conventions and requirements of the native platforms (e.g., iOS, Android). Native app development environments may have specific ways of handling JavaScript callbacks, and you should ensure seamless communication between the web view and the native code.

Example Web (JS):

```javascript
let FV = new FaceVerify();
FV.init({
    CONTAINER_ID: 'FV_mount',
    LANGUAGE: 'en',
    TOKEN: '<SDK_TOKEN>',
    onComplete: function(data) {
        console.log(data);
    },
    onError: function(error) {
        console.error(error.code, error.stack)
    },
    onUserExit: function(error) {
        console.log(error);
        window.history.back()
    }
});
```

| **ATTRIBUTE** | **FORMAT**          | **DEFAULT VALUE**                           | **EXAMPLE**                                 | **NOTES**                                                                                                               |
| ------------- | ------------------- | ------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `onComplete`  | javascript function |                                             | `function(data) {console.log(data)}`        | **required**<br> Callback that fires when all interactive tasks in the workflow have been completed.                    |
| `onError`     | javascript function | `function(error) {console.log(error.code)}` | `function(error) {console.log(error.code)}` | **required**<br> Callback that fires when an error occurs. Receives `{ code, stack }`. See [Error Codes](#error-codes). |
| `onUserExit`  | javascript function | `function(error) {console.log(error)}`      | `function(error) {window.history.back()}`   | **required**<br> Callback that fires when the user exits the flow without completing it.                                |

### onComplete

This callback function will be called once all the tasks within the workflow succesfully have been completed. This callback function is **required**. The `data` parameter within the function represents the [output](#output) of the completed process. You can customize this function to handle and display the data as needed.

Example Web (JS):

Within the example below we are logging the output (`data`) to console.

```javascript
let FV = new FaceVerify();
FV.init({
    ...,
    onComplete: function(data) {
        console.log(data);
        // FV.stop() is called automatically - no need to call it here
    }
});
```

### onError

This callback fires when an error occurs during the SDK lifecycle. This callback function is **required**. The `error` parameter is an object with two properties:

- **`code`** — A structured error code (e.g., `capture_error:4004`). Use the category prefix to determine the appropriate UI response. See [Error Codes](#error-codes) for the full list of categories.
- **`stack`** — A stack trace string. Include this when reporting issues to support for faster diagnosis.

Example Web (JS):

```javascript
let FV = new FaceVerify();
FV.init({
    ...,
    onError: function(error) {
        console.error(error.code, error.stack);

        if (error.code.startsWith('capture_error')) {
            // Camera issue — show retry UI or prompt for camera permission
        } else if (error.code.startsWith('init_error')) {
            // Initialization failed — prompt user to refresh the page
        } else if (error.code.startsWith('detect_error')) {
            // Face detection failed — prompt user to try again
        }
    }
});
```

### onUserExit

This callback can be used to implement actions like returning users to the previous page or prompting them for confirmation before exiting to ensure they don't lose any unsaved data or work. This callback function is **required**. The `error` parameter within the function contains information about the specific error encountered, allowing you to log or display error messages for debugging or user guidance. The error that is thrown is `"exit"`.

Example Web (JS):

Within the example below we are logging the output (`error`) to console. Finally, we move back one page in the session history with `window.history.back()`.

```javascript
let FV = new FaceVerify();
FV.init({
    ...,
    onUserExit: function(error) {
        console.log(error);
        window.history.back()
    }
});
```

## Usage/Examples

The tool first needs to be initialised to load all the models.
Once its initialised, it can be started with the function `FV.start();`

```javascript
let FV = new FaceVerify();
FV.init({
    CONTAINER_ID: ...,
    LANGUAGE: ...,
    TOKEN: ...,
    onComplete: ...,
    onError: ...,
    onUserExit: ...,
}).then(() => {
    FV.start();
});
```

### Cleanup and Removal

The SDK automatically cleans up resources (stops the camera and removes UI elements) when the `onComplete`, `onError`, or `onUserExit` callbacks are triggered.

If you need to completely remove the SDK instance and its container from the DOM (e.g., when unmounting a component or navigating away), use the `remove()` method:

```javascript
FV.remove();
```

This will stop all SDK processes and remove the entire container from the page. This is particularly useful in Single Page Applications (SPAs) or when you need to reinitialize the SDK later.

Example below:

```javascript
let FV = new FaceVerify();
FV.init({
    CONTAINER_ID: 'FV_mount',
    LANGUAGE: 'nl',
    TOKEN: '<SDK_TOKEN>',
    onComplete: function(data) {
        console.log(data);
    },
    onError: function(error) {
        console.error(error.code, error.stack)
    },
    onUserExit: function(error) {
        console.log(error);
        window.history.back();
    },
});
```

## Importing SDK

Import the SDK with one of the three methods: Script tag, ES6 or CommonJS.

### Script Tag

Easily add FaceVerify to your HTML files using the Script Tag method.

```html
<!-- Add FaceVerify directly in your HTML -->
<script src="dist/faceverify.obf.js"></script>
```

### NPM

For projects using NPM and a module bundler like Webpack or Rollup, you can import FaceVerify as an ES6 module or with CommonJS require syntax.

```js
// Import FaceVerify in your JavaScript file

// ES6 style import
import FaceVerify from '@datachecker/faceverify';

// CommonJS style require
let FaceVerify = require('@datachecker/faceverify')
```

## Demo

```html
<!DOCTYPE html>
<html>
<head>
<title>FaceVerify</title>
</head>

<body>
    <div id="FV_mount" style="height:100vh">
    </div>
</body>

<script src="faceverify.obf.js" type="text/javascript"></script>
<script>
    let FV = new FaceVerify();
    FV.init({
        CONTAINER_ID: 'FV_mount',
        LANGUAGE: 'en',
        TOKEN: '<SDK_TOKEN>',
        onComplete: function(data) {
            console.log(data)
        },
        onError: function(error) {
            console.log(error)
        },
        onUserExit: function(error) {
            console.log(error);
            window.history.back();
        },
    });    
</script>

</html>
```

## Languages

There are two ways in which notifications can be loaded: from file, from object (json).

### File

The languages can be found in `assets/language/`. The current support languages are `en` and `nl`. More languages could be created.

The notifications can be loaded in `configuration` like the following:

```javascript
let FV = new FaceVerify();
FV.init({
    LANGUAGE: 'en',
    ...
```

To create support for a new language, a js file needs to be created with specific keys.
The keys can be derived from the current language js files (`assets/language/en.js`).

Example:

```javascript
var LANGUAGE = {
    "start_prompt": "Tap to start",
    "no_face": "No face detected",
    "nod_head": "Please nod your head",
    "face_thresh": "Face covered",
    "face_far": "Move closer",
    "face_close": "Move back",
    "exp_dark": "Image is too dark",
    "blur": "Hold still",
    "capture_error": "Camera access is required",
    "challenge_0": "Center your face",
    "challenge_out": "Face too far",
    "detect_error": "Face detection failed. Please try again.",
    "init_error": "Initialization failed. Please refresh the page.",
    "model_error": "Failed to load required resources. Please check your connection.",
    "opencv_error": "A required component failed to load. Please refresh the page.",
    "settings_error": "Configuration error. Please contact support.",
    "token_error": "Authorization failed. Please try again later.",
    "challenge_1": "Look up",
    "challenge_2": "Look right",
    "challenge_3": "Look down",
    "challenge_4": "Look left",
    "tutorial": "Follow the instructions",
    "continue": "Continue"
}
```

### Object (json)

Notifications can also be loaded as a json object like the following:

```javascript
let FV = new FaceVerify();
FV.init({
    LANGUAGE: JSON.stringify(
        {
            "start_prompt": "Tap to start",
            "no_face": "No face detected",
            "nod_head": "Please nod your head",
            "face_thresh": "Face covered",
            "face_far": "Move closer",
            "face_close": "Move back",
            "exp_dark": "Image is too dark",
            "blur": "Hold still",
            "capture_error": "Camera access is required",
            "challenge_0": "Center your face",
            "challenge_out": "Face too far",
            "challenge_1": "Look up",
            "challenge_2": "Look right",
            "challenge_3": "Look down",
            "challenge_4": "Look left",
            "tutorial": "Follow the instructions",
            "continue": "Continue"
        }
    ),
    ...
```

## Error Codes

The `onError` callback receives an object `{ code, stack }`. The `code` follows the format `category:NNNN` (e.g., `capture_error:4004`). Use the category prefix to determine the type of error and the appropriate response.

| Category         | Description                                                                                        | Recommended Action                                                                  |
| ---------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `capture_error`  | Camera or processing failure                                                                       | Show camera retry UI or prompt for permission                                       |
| `detect_error`   | Face detection failed after repeated attempts                                                      | Prompt user to try again or refresh                                                 |
| `device_error`   | Device cannot run the face landmarker (no WebGL2 in worker or main thread). Surfaced before start. | Show "device not supported" message — do **not** offer a retry; nothing will change |
| `init_error`     | Initialization failed                                                                              | Prompt user to refresh the page                                                     |
| `model_error`    | ML model failed to load                                                                            | Check network connection, retry initialization                                      |
| `opencv_error`   | Required component failed to load                                                                  | Prompt user to refresh or try a different browser                                   |
| `settings_error` | Invalid configuration or version mismatch                                                          | Verify SDK configuration and assets                                                 |
| `token_error`    | Token missing, invalid, or not permitted                                                           | Verify token credentials                                                            |

When reporting issues to support, include both `error.code` and `error.stack` — the numeric identifier in the code and the stack trace allow for precise diagnosis.

The user-facing alert message shown in the SDK overlay is determined by the category prefix, which maps to a key in the [Languages](#languages) dictionary (e.g., `capture_error` maps to the `capture_error` language key). If a custom language file does not include a key for a new category (e.g., `init_error`), the SDK falls back to its built-in English default.

## Models

The tool uses a collection of neural networks located in the `assets/` directory.

**Asset Configuration:** Models are part of the assets and can be fetched via CDN (default) or hosted locally. See [Asset Fetching Configuration](#asset-fetching-configuration) for details.

## Challenges

User challenges are implemented to prevent video injection attacks. These challenges are randomly chosen and thereby, processes are different from one another. The challenges consist of *head pose estimation*. The performed head poses will be compared with the challenges and that result will be returned as bool in `output`. (see [Output](#output))

There are four poses that will be detected:

- up
- right
- down
- left

**Challenges are managed exclusively through the SDK token.** Specify the number of challenges when requesting your SDK token:

```javascript
fetch(BASE_ENDPOINT + "/sdk/token?number_of_challenges=2&customer_reference=<CUSTOMER>&validateWatermark=true&services=FACE_VERIFY", {
    method: 'GET',
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': `Bearer <ACCESSTOKEN>`
    }
})
.then(response => response.json())
```

The challenges are embedded in the `TOKEN` and are not directly configurable in the SDK initialization.

## Output

The SDK will output in the following structure:

```json
{   
    "images": [
        {
            "data":"<BASE64_IMG>", 
            "type":"LIVE"
        },
        "..."
    ],
    "meta": [
        {
            "x":"", 
            "y":"", 
            "width":"", 
            "height":"", 
            "device":""
        },
        "..."
    ],
    "token": "<SDK_TOKEN>",
    "transactionId": "<TRANSACTION_ID>",
    "valid_challenges": "true|false"
}
```

Example:

```json
{   
    "images": [
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }, 
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }, 
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }
    ],
    "meta": [
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }, 
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }, 
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }
    ],
    "token": "<SDK_TOKEN>",
    "transactionId": "<TRANSACTION_ID>",
    "valid_challenges": true
}
```

### FaceVerify API call

If you want to send the images to the [Datachecker FaceVerify API](https://developer.datachecker.nl/?urls.primaryName=v2#/ProductApi/ProductApi_FaceVerify) you **must** add a *comparison image*. This *comparison image* can either be a portrait picture from an identity card or a selfie. To add this image, you need to use `type: "COMPARE"`.

Example JS:

```javascript
let faceverify_output = {   
    "images": [
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }, 
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }, 
        {
            "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", 
            "type":"LIVE"
        }
    ],
    "meta": [
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }, 
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }, 
        {
            "x": 33, 
            "y": 182, 
            "width": 265, 
            "height": 354, 
            "device": "Front camera"
        }
    ],
    "token": "<SDK_TOKEN>",
    "transactionId": "<TRANSACTION_ID>",
    "valid_challenges": true
}
let images = faceverify_output.images
let portrait_image = {"data":"/9j/4AAQSkZJRgABAQAAAQABAAD/...", "type":"COMPARE"}

images.unshift(portrait_image)
let data = {"images": images, "transaction_id":faceverify_output.transactionId}


fetch(<BASE_ENDPOINT>+"/faceverify", {
        method: 'POST',
        headers: <HEADERS>,
        body: JSON.stringify(data)
        })
        .then(response => response.json())
```
