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
Cross-Origin Communication with session id/password pairs
Showing preparation instructions: shi Claim
Execution Behavior: age and liv Claims
Return Behavior: rtb and rdr Claims
Querying Transaction Results Outside the Execution Time
Current Behavior and Error codes
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
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) andiat
(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.
![](https://cdn.prod.website-files.com/65382a5ea19f13fb51e12ccb/674593a4b425feb20f588d72_6745911f8517baf2fc6f438d_f1fc082e-2334-4b96-8b79-33670c0e904f.png)
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 thecfd
claim, then we will return a positive response. See Recommended Age Gatesliv
enables or disables liveness checks. When it is set fortrue
, 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 tofalse
.
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
, andliv
requirements, the system will returntrue
orfalse
in therlt
response field. - “interval”:
rlt
response field shall include sub-fieldsminAge
,maxAge
,score
andgate
. 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 parametertoken
: 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}"
}
- “callback”: Repeats the behavior in “message” mode, and also make the full results sent from our servers to the webhook you register in
rdr
. In other words, we’ll send HTTP POST to: https://your.custom.url/your-custom-route?token=<ourJWTtoken> The browser page shall not navigate anywhere.
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’sage
,cfd
,liv
claims. - We added another custom field
rsn
, which might give more information if we deem necessary, such ascomplete_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”.
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
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
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.