FaceAssure On-Browser | Getting Started

Integrate seamlessly with our browser-ready SDKs and build privacy-protected web experiences your users will love. Start coding, see results, and unlock the possibilities today.

AgeAssure Fully-Managed On-Browser Release Notes

Version:v1.8.1 (3)

Endpoints:

Target browsers: Chrome (mobile and desktop alike)

This is a documentation for the browser-based experience of Privately’s facial age estimation technology. The UI elements are fully managed by Privately, enabling you to only focus on the communication between your system and Privately’s.

Changelog from Previous Production Version

Modes of Execution

Demo mode

    Cross-Origin Communication with session id/password pairs

Integration mode - Using JWT

    Standard Claims

    Showing preparation instructions: shi Claim

    Execution Behavior: age and liv Claims

    Return Format: rtf Claim

    Return Behavior: rtb and rdr Claims

    Sample JWT Request Generation

The Response Payload

Querying Transaction Results Outside the Execution Time

Recommended Age Gates

User Experience with QR Codes

Current Behavior and Error codes

Additional Considerations

Changelog from Previous Production Version

  • Improved UX and analytics
  • Added the description for the claim ufi in the response payload
  • Fixed the certificates issue in generating QR codes

Modes of Execution

 Integration mode - uses signed JWT tokens  
  • Add ?token=... to activate this mode

  • The end-user will see "Age Check Complete. TX id....". Whether the age check succeeds or not depends on the claims in the request payload.

  • The user will be redirected based on the parameters specified in the JWT token

 Demo mode  
  • Add the following to activate this mode:

?session_id=<your_api_key>&session_password=<your_api_key>

  • Add stillMode=true to skip liveness checks

  • Add shi=true to display instructions screen to the user.

  • Ideal for external demonstrations

Demo mode

This mode is pretty much plug-and-play. You may supply your session_id and session_password as query string parameters, and test out the system. This option has limited configurability (such as age limits), therefore it might not be suitable for production at large scale.

Cross-Origin Communication with session id/password pairs

It’s possible to set up an iFrame/WebView, embed this page, and ingest the execution output with cross-origin communication via postDocument(), with a structure as follows:

{
    age_identified: "25+",
    gate_identified: 25, // the highest possible age gate from presets (16, 21, 25)
    minAge: 33.5,
    maxAge: 37.2,
    transaction_id: "..." // a unique transaction ID for your records
    status: "AGE_CHECK_COMPLETE", // or the error code if the estimation wasn't done
    score: 1, // the confidence of the interval [minAge, maxAge]
    exp: 1640995200,
    nbf: 1609459200,
    iat: 1609455600
}

In order to receive this message, you should add an event listener to your parent page:

window.addEventListener('message', function(e) { 
    // Get the sent data 
    const data = e.data; 
    console.log(data); 
    /* Surround the event message processing against 
    unexpected messages from sources other than the on-browser page: */
    try
    {
        var t = JSON.parse(data); 
        var age_identified = t["age_identified"],
            gate_identified = t["gate_identified"],
            min_age = t["minAge"],
            max_age = t["maxAge"],
            execution_status = t["status"];
        //alert(t)
        //alert("age_identified = ", age_identified)
        /* Your execution logic goes here */
    }
    catch(err)
    {
        //alert("not processing: ", data) // OR the one below
        alert("I skip a message, because: ", err)
    } 
});

We nevertheless recommend instead a JWT-based communication when you deploy with the “Integration mode” - see below.

Integration mode - Using JWT

In integration mode, we assume that you pass a JWT request token as a base-64 query string parameter called token. The payload of this token, once parsed, should resemble to the following structure:

{
  "iss": "https://your.url.where.we.find/your/public/key",
  "sub": "9e39aa249a259c1779a5209f4fe1b57b"
  "aud": "https://api.privately.swiss"
  "exp": 1640995200,
  "nbf": 1609459200,
  "iat": 1609455600,
  "jti": "<your-custom-transaction-id>",
  "rdr": "https://your.custom.url/your-custom-route",
  "age": 21,
  "cfd": 0.9,
  "liv": false,
  "rtf": "query",
  "rtb": "redirect",
  "shi": true
}

Standard Claims

iss, sub, aud, exp, nbf, iat and jti are part of the standard JWT claims.

  • iss is the URL where we can query your public key you used to sign your claims.
  • sub should contain a value generated from your side. It will be helpful in our shared analytics. While Privately can be completely abstracted from the logic with which this value is generated, it will be helpful for you to distinguish transactions for unique clients, deployment environments, etc.
  • jti should indicate a unique transaction ID. Its value should be generated from your side, so that you can avoid replay attacks or fake transaction payloads.
  • exp (expires-at),  nbf (not-before) and iat (issued-at) dates indicate the validity of the request payload. Our system will reject requests if the processing time does not comply the constraints of these dates.

Showing preparation instructions: shi Claim

shi (default value: false), shall define whether we show the initial instructions screen to the user (see below). When shi is set to true, This screen allows the user to prepare themselves, select a camera for the test, or scan a QR code to continue the process on a mobile device.

Execution Behavior: age and liv Claims

  • age shall contain the target age in years that Privately should use for estimation. If we observe the test subject’s age to be higher than the confidence level identified in the cfd claim, then we will return a positive response. See Recommended Age Gates
  • liv enables or disables liveness checks. When it is set for true, the system will employ a variety of techniques to detect anomalous user behavior age checks (presentation and injection attacks). Upon detection of anomalies, the system shall halt the age estimation process and return an appropriate error signals (see Current Behavior and Error codes). For your initial experiments, we recommend that you set this to false.

Return Format: rtf Claim

rtf shall define the return format in the rlt claim in our response token once the age check is complete. Options shall be:

  • “query” [DEFAULT OPTION]: Given your age, cfd , and liv requirements, the system will return true or false in the rlt response field.
  • “interval”: rlt response field shall include sub-fields minAge, maxAge, score and gate. These will let you run your future queries on the output credentials without having the user redo their age checks until the expiry of the response. For adulthood detection, gate will be an integer that gives the highest gate that we can guarantee from our list of preset gates (16, 21, 25). If there are any issues in the execution, we will return these values as 0 (see Current Behavior and Error codes).

Return Behavior: rtb and rdr Claims

rtb shall define the return behavior once the age check is complete. Options shall be:

  • “redirect” [DEFAULT OPTION]: The user shall be redirected to the URL you’ve defined in rdr , with our response JWT payload as a query string parameter token :  https://your.custom.url/your-custom-route?token=<ourJWTtoken>
  • “message”: The token will be sent via postDocument() to the parent page that embeds our page. The browser page will not navigate anywhere.
  • Just like the demo mode, you can still listen to cross-document message events. However, this time the payload will be structured as follows:
{
    token: "{encodedHeaders}.{encodedPlayload}.{encodedSignature}"
}
If you use the “callback” option,
  1. We recommend that you keep track of valid JWT IDs that you’ve sent within the jti claim to avoid fake responses with unknown JWT IDs.
  2. Please indicate if you’d like to use additional authentication mechanisms for the webhook you’ll register.
  3. If you want to register a single webhook for multiple assets (subclients, deployments, endpoints, experiments, etc.), distinguish these assets via separate values in the sub claim for better analytics tracking.

Sample JWT Request Generation

In order to generate a “compliant” but unsigned JWT request payload, you use a snippet similar to the one below. In order to run the snippet below, you can paste it into a file called jwtGeneration.js and then run node jwtGeneration.js :

const crypto = require("crypto");
function getRandomHexID() {
    return crypto.randomBytes(32).toString("hex")
  }
function generateClaimsForJWT(args)
{
  let timestampGenuine = Date.now() / 1000; // the number of milliseconds since January 1, 1970 00:00:00.
  timestampGenuine = ~~timestampGenuine;
  let claims = {
    iss: "client-issuer-url", // the URL where we'll check the public keys
    sub: "9e39aa249a259c1779a5209f4fe1b57b",
    aud: "https://api.privately.swiss", // the URL where we'd supply the publich keys
    exp: timestampGenuine + 100, // Expiration time -- assuming genuinely 100 seconds after issued-at
    nbf: timestampGenuine, // not-before, which is right now
    iat: timestampGenuine - 2, // issued-at -- assuming 2 seconds before
    jti: getRandomHexID().replaceAll('-', ''),
    rdr: "https://privately.eu", // redirection URL
    age: 18,
    cfd: 0.9
  }
  return claims;
}
function buildJwt(claims) {
    const header = {
      alg: "HS256",
      typ: "JWT",
    };
    const HMACSHA256 = (stringToSign, secret) => "not_implemented";
    const encodedHeaders = encodeURIComponent(btoa(JSON.stringify(header)));
    const encodedPlayload = encodeURIComponent(btoa(JSON.stringify(claims)));
    const signature = HMACSHA256(`${encodedHeaders}.${encodedPlayload}`, "mysecret");
    const encodedSignature = encodeURIComponent(btoa(signature));
    const myjwt = `${encodedHeaders}.${encodedPlayload}.${encodedSignature}`;
    //console.log(myjwt);
    return myjwt;
  }
function demo(args)
{
    let claims = generateClaimsForJWT(args);
    let myJWT = buildJwt(claims);
    console.log(myJWT);
}
const args = process.argv.slice(2);
demo(args);

We will require you to:

  • Sign your payloads
  • Point to the public key that we can use to verify your JWT request
  • Give the name of the algorithm 'RS256', 'ES256', etc. that you've used for encryption.
  • We strongly recommend you to use off-the-shelf packages such as npm: jsonwebtoken to sign and verify JWT tokens in production.

The Response Payload

The response payload’s claims shall have the following structure:

{
  "iss": "https://api.privately.swiss",
  "sub": "9e39aa249a259c1779a5209f4fe1b57b"
  "aud": "https://your.url.where.we.find/your/public/key",
  "exp": 1640995200,
  "nbf": 1609459200,
  "iat": 1609455600,
  "age": <mirrors the age claim from the request payload>,
  "liv": <whether liveness check was enabled or not for this TX>,
  "jti": "<your-custom-transaction-id>",
  "rlt": true | {"minAge":..., "maxAge": ..., "score": ..., "gate": ...},
  "rsn": "AGE_CHECK_COMPLETE",
  "ufi": []
}
  • Our custom field rlt contains information based on the return format claim of the original request: Return Format is true if the end-user satisfies the constraints laid out in the request payload’s age, cfd, liv claims.
  • We added another custom field rsn , which might give more information if we deem necessary, such as complete_transaction, user_did_not_follow_instructions, etc.
  • Yet another claim, ufi, shall indicate a list of instructions that the user hasn’t completed before the end of the age check. Possible values that can be inserted to this list are: NO_FACE , STAY_STILL , LOOK_STRAIGHT, CENTRE_FACE, TURN_LEFT, TURN_RIGHT, ALIGN_YOUR_FACE_WITH_THE_CAMERA_UP,  ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWN, SLIGHTLY_TILT_YOUR_HEAD_LEFT, SLIGHTLY_TILT_YOUR_HEAD_RIGHT, OPEN_YOUR_MOUTH, KEEP_YOUR_MOUTH_OPEN, CLOSE_YOUR_MOUTH, SLOWLY_COME_CLOSER_TO_THE_CAMERA, SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA, TOO_DARK

Given a response payload claims, here’s a snippet that verbosely demonstrates what we do to sign our response JWT:

buildJwt(claims){
    const header = {
    "alg": "RS256",
    "typ": "JWT"
    }
    const RSA256 = (stringToSign, secret) => "library_implementation"
    const encodedHeaders = encodeURIComponent(btoa(JSON.stringify(header)))
    const encodedPlayload = encodeURIComponent(btoa(JSON.stringify(claims)))
    const signature = RSA256(`${encodedHeaders}.${encodedPlayload}`, "mysecret")
    const encodedSignature = encodeURIComponent(btoa(signature))
    const myjwt = `${encodedHeaders}.${encodedPlayload}.${encodedSignature}`;
    return myjwt
}

The output, myjwt , would be added as a query string parameter (token) to the redirection URL obtained from the request payload.

Querying Transaction Results Outside the Execution Time

You may encounter cases where you will need to query the transaction results outside the execution time, due to network availability and/or in order to reuse an existing age check result. In this case, you may send an HTTP POST request to the following endpoint:

JWT result query endpoint: https://wycmdhq5c2.execute-api.eu-west-1.amazonaws.com/default/d-privately-age-services

Sample payload (raw/JSON):

{
    "request_type": "query_jwt_result",
    "api_key": "<your_api_key>",
    "transaction_id": "<value_set_in_jti_claim>"
}

Success case: If such a transaction has already happened, The output of this request will be a base64 string of our signed JWT payload, which follows the same format described above in “The Response Payload”.

When we issue our JWT response token, we set its exp (“expiration”) claim 10 minutes from its issuance date. Therefore, depending on the time in which you query the results, you may receive a token that has already expired and therefore cannot be validated. However, the raw payload will still be included for your inspections at any time.

Fail case: If such a transaction doesn’t exist with your api key, you will receive a response with the HTTP status 400:

{
    "request_not_complete": "<the_invalid_transaction_id>"
}

Recommended Age Gates

Age display to the end-user
  • "Age Check Complete." in integration mode (i.e., the prediction won't be revealed to the end-user)

  • "<16", "16-21", "21+" in Demo mode.

Target age gates  
  • The system will accept target age gate inputs as, or will convert them to, 16, 21 and 25.

These age gates are determined by the inputs we have gathered from regulators, the market, our internal benchmarking, and our public EAL3 certifications:

  • Bearing in mind the EU regulations for parental consent, 16 is a good buffer age to determine whether a given user can ask for verified parental consent.
  • 25 comes from the Challenge-25, where businesses are obliged to check for an ID for stronger age verification if the user appears to be below the age of 25. This restriction is regardless of the observed accuracy numbers of the system that might permit a lower age gate such as 21.
  • 21 is a buffer age gate that might be applied for jurisdictions with relaxed constraints which will allow a higher number of adults to pass without friction.

User Experience with QR Codes

When a user opts to scan a QR code and continue their tests on another device, the following effects unfold:

  • It is understood that the user has already read the instructions from their first device, thus QR codes lead to URLs that have &shi=false in the query string parameters.
  • Upon the successful completion of the test in the second device, the user will not be able to submit an age check from the first device.
  • If you have chosen to embed our page as an iFrame to your page, you should make sure that you’ve set rtb:callback, and terminate our iFrame as soon as you receive the results from your webhook.

The QR code is intended to be used just once, therefore it is deleted:

  • As soon as it is scanned
  • As soon as an age check is completed (exceptions: if user denies camera, or their devices are deemed incompatible to start the execution).
  • If more than a minute has passed since its issuance.

Current Behavior and Error codes

 Incident  Behavior
 User completes the transaction  User is redirected to the url defined in rdr, with the result encoded in rlt as true or false. The field rsn will be filled with complete_transaction
 A previously consumed JWT token is presented  
  • Alert message: "Age check token has already been used. Please verify with your game publisher"

  • Reload the page without any communication back to client's systems

 JWT token includes sub (user id) that previously did an age check  
  • Allow the user to perform an age check

 No token is presented, or the JWT token does not parse with the parseJwt function  
  • Alert message: "Age check publisher credentials are invalid. Please verify with your game publisher"

  • Since JWT (and hence redirection URL) might be tampered, no redirection happens. However, the transaction will be logged in Privately servers with the signal INVALID_JWT

 User does not follow the instructions, and thus a timeout occurs  
  • Alert message: "Your age test has exceeded the time limit before we could sample a good number of images. Please follow the instructions to align your face and complete the test."

  • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="USER_DID_NOT_FOLLOW_INSTRUCTIONS".

 User closes the window without finishing the check  
  • Redirection not possible, but we will log this transaction with the signal "USER_PREMATURELY_CLOSED"

 User denies camera access to our interface  
  • Redirection with the the rsn claim = CAMERA_PERMISSIONS_NOT_GRANTED 

 A presentation attack is detected (printed photo / pre-recorded video / face masks are physically presented to the camera)  
  • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="FACE_SWAP DETECTED"

 Redirection URL is not valid  
  • If the JWT token is structurally valid, the redirection will happen, even if the link is broken

  • If JWT is invalid, then no redirection will happen.

Additional Considerations

  • Before large scale deployments please consult us for optimized scaling solutions.
  • For customised frontend, contact us for a specific version of our "white-label" product option, e.g. integrating your visual assets
  • When possible, Privately stores the transaction outcomes also in its databases. Upon request, we can open an endpoint to you so that they can query these records. Additional logs can be made available based on mutually agreed design choices.