Skip to main content

Upgrading an AgeKey

Overview

The Upgrade AgeKey flow allows users to upgrade their existing AgeKey with additional verification when age thresholds aren't met. This guide walks through implementing this flow step-by-step.

Already familiar with OIDC?

This guide uses the OpenID Connect implicit flow (response_type=id_token) with the agekey-upgrade scope. When all age thresholds are false, both a code and an id_token are returned. The id_token contains the age thresholds, and the code is exchanged for an access token used to submit upgrade verification data. If you already know these flows, you mainly need the AgeKey-specific pieces: the agekey-upgrade scope and the upgrade endpoint flow.

Time to implement: ~3 hours


High-level flow overview

Before diving into code, it's important to understand what happens during the Upgrade AgeKey flow from start to finish.

The user's journey

1. User Needs Age Verification A user with an existing AgeKey attempts to access age-restricted content. They click "Verify with AgeKey" to verify their age.

2. Redirect to AgeKey Service with Upgrade Scope When they click the button, your app redirects their browser to the AgeKey service with the agekey-upgrade scope. In the URL, you include information about what age thresholds you need to check (such as "Is this person 18 or older?").

3. AgeKey Authentication The AgeKey service shows the user a prompt to authenticate with their passkey (similar to Touch ID, Face ID, or Windows Hello). This happens entirely on their device - no passwords needed, and the actual age information never leaves their device.

4. Age Threshold Check Once authenticated, AgeKey checks the user's saved age information against the thresholds you requested. If all thresholds are false, it creates both a signed token containing the age thresholds and an authorization code.

5. Return to Your App with Code and Token When all age thresholds are false, the user's browser is redirected back to your app with both a code parameter and an id_token in the URL. The id_token contains the age threshold results, and the code can be exchanged for an access token.

6. Server validation Your server receives the id_token and performs critical security checks:

  • Is the signature valid? (Proves it really came from AgeKey)
  • Is it expired? (Prevents replay attacks)
  • Does the audience match? (Ensures it was meant for your app)
  • Do the security tokens match? (Prevents CSRF attacks)

7. Extract Age Thresholds Once validated, your app extracts the age thresholds from the token. If all thresholds are false, you can offer the user the option to upgrade their AgeKey.

8. Exchange Code for Access Token If the user wants to upgrade, your server exchanges the code for an access token by using Basic authentication with your client credentials.

9. Perform Additional Verification You perform another verification method (ID scan, credit card verification, or other valid methods) to gather additional age verification data.

10. Submit Upgrade Request Your server sends the upgrade verification data to AgeKey's upgrade endpoint by using the access token for authorization.

11. Confirm Success Your app confirms the upgrade was successful and shows the user an appropriate message.

Visual flow diagram

Key technical concepts

OpenID Implicit Flow with Upgrade Scope This flow uses the OpenID Connect "implicit flow," but with the agekey-upgrade scope added. When all age thresholds are false, both a code and an id_token are returned in the URL.

Age Thresholds in Token The id_token contains the age threshold results as boolean values. When all requested thresholds are false, this triggers the upgrade flow.

Code Exchange Flow When all age thresholds are false, AgeKey returns a code parameter in addition to the id_token. This code must be exchanged for an access token by using Basic authentication with your client credentials before submitting the upgrade request.

Two-Step Verification The upgrade flow requires two separate verification steps:

  1. Initial AgeKey verification that determines thresholds aren't met (shown in the id_token)
  2. Additional verification method (ID scan, credit card, or other valid methods) to gather new age data for the upgrade

Security Tokens

  • State: A random value you generate to prevent CSRF attacks. You check that the same value comes back.
  • Nonce: Another random value to prevent replay attacks. It's embedded in the signed token.

JWT Validation The id_token is a JSON Web Token (JWT) signed by AgeKey. Your server must verify this signature by using AgeKey's public keys to ensure it wasn't tampered with.

Server-to-server communication The code exchange and upgrade request happen entirely server-to-server. The user's browser is never involved in transmitting authentication tokens or verification details.

What you'll need to build

  1. Front end Component: A button/link that redirects users to AgeKey with agekey-upgrade scope
  2. Callback Page: A page that receives the user when they return from AgeKey with code and token
  3. Server validation: Server-side code to verify the token and extract age threshold results
  4. Code Exchange Handler: Server-side code to exchange the code for an access token
  5. Additional Verification: Logic to perform a second verification method after obtaining the token
  6. Upgrade Request Handler: Server-side code to submit upgrade data to AgeKey's upgrade endpoint
  7. Access Control Logic: Code to offer upgrade when all thresholds are false
  8. User Experience Flow: UI to offer AgeKey upgrade and show confirmation messages

When to offer AgeKey upgrade

Best Practices
  • After failed threshold check: Offer when id_token shows all thresholds as false
  • Make it optional: Never force users to upgrade their AgeKey
  • Explain the benefit: "Upgrade your AgeKey to meet the age requirement for this content"
  • Have verification method ready: Ensure you have an additional verification method available before offering upgrade
  • Don't offer unnecessarily: Only offer when age thresholds are actually not met

Now you can implement each piece step by step.


Step 1: Prepare your request parameters

Before redirecting the user, you need to prepare several parameters. These are sent as query parameters to the AgeKey authorization endpoint.

Required parameters

ParameterWhat it'sExample
client_idYour AgeKey client ID (provided by AgeKey)your-app-id-123
redirect_uriWhere users return after verification (must be pre-registered)https://yourapp.com/agekey/callback
scopeAlways set to openid agekey-upgradeopenid agekey-upgrade
response_typeAlways set to id_token (implicit flow)id_token
stateRandom token you generate for CSRF protectionabc123xyz789
nonceRandom value for replay protectionnonce456def
claimsURL encoded JSON string specifying age thresholds to checkSee below

The claims parameter

The claims parameter tells AgeKey which age thresholds to verify. It must be a URL-encoded JSON object. For complete details on the claims structure, see the Use AgeKey API Reference.

Example 1 - Check if user is 18 or older

{
"age_thresholds": [18]
}

Example 2 - Check if user is 18 or older by an ID scan with face match scan performed

{
"age_thresholds": [18],
"allowed_methods": ["id_doc_scan"],
"overrides": {
"id_doc_scan": {
"attributes": {
"face_match_performed": [true]
}
}
}
}

Step 2: Generate security tokens

Using an OIDC Library?

If you're using an OIDC library (recommended), it can typically handle state and nonce generation and validation automatically. Check your library's documentation to confirm it handles these security tokens - you might not need to implement this step manually.

If you're implementing the OIDC flow manually (without a library), you need to generate random values for state and nonce. Store both values in your session so you can verify them when the user returns.

Code Example:

function generateRandomString(length) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}

const state = generateRandomString(16);
const nonce = generateRandomString(16);

// Store both state and nonce in session
sessionStorage.setItem('agekey_upgrade_state', state);
sessionStorage.setItem('agekey_upgrade_nonce', nonce);

Step 3: Build the authorization URL

Construct the URL to redirect the user to AgeKey.

Using an iframe?

If you're embedding the Upgrade AgeKey flow in an iframe, you must include the publickey-credentials-get permission in the allow attribute:

<iframe src="https://api.agekey.org/v1/oidc/use" 
allow="publickey-credentials-get"
/>

Base URL:

https://api.agekey.org/v1/oidc/use

Full Example URL:

https://api.agekey.org/v1/oidc/use?
scope=openid%20agekey-upgrade&
response_type=id_token&
client_id=your-client-id&
redirect_uri=https%3A%2F%2Fyourapp.com%2Fagekey%2Fcallback&
state=abc123xyz789&
nonce=nonce456def&
claims=%7B%22age_thresholds%22%3A%5B18%5D%7D

Code Example:

const params = new URLSearchParams({
scope: 'openid agekey-upgrade',
response_type: 'id_token',
client_id: 'your-client-id',
redirect_uri: 'https://yourapp.com/agekey/callback',
state: state,
nonce: nonce,
claims: JSON.stringify({ age_thresholds: [18] })
});

const authUrl = `https://api.agekey.org/v1/oidc/use?${params.toString()}`;

// Redirect user
window.location.href = authUrl;

Step 4: Handle the Callback

After the user completes verification, they'll be redirected back to your redirect_uri with results in the URL fragment.

When all thresholds are false

If all age thresholds are false, the callback includes both a code and an id_token:

Example Callback URL:

https://yourapp.com/agekey/callback#
code=abc123xyz789&
id_token=eyJhbGc...long-jwt-string...&
state=def456uvw012

The id_token contains the age thresholds in its claims, and the code is used to obtain an access token for the upgrade request.

When thresholds are met

If any threshold is met, only the id_token is returned (standard Use AgeKey flow):

Example Callback URL:

https://yourapp.com/agekey/callback#
id_token=eyJhbGc...long-jwt-string...&
state=def456uvw012

Extract the token and code (front end)

Code Example (JavaScript):

// On your callback page
function handleAgeKeyUpgradeCallback() {
// Get fragment parameters
const fragment = window.location.hash.substring(1);
const params = new URLSearchParams(fragment);

const idToken = params.get('id_token');
const code = params.get('code');
const returnedState = params.get('state');
const error = params.get('error');

// Check for errors
if (error) {
console.error('AgeKey error:', error);
// Handle error appropriately
return;
}

// Verify state matches
const storedState = sessionStorage.getItem('agekey_upgrade_state');
if (returnedState !== storedState) {
console.error('State mismatch - possible CSRF attack');
return;
}

// Send token and code to server for validation
fetch('/api/validate-agekey-upgrade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id_token: idToken,
code: code // May be null if thresholds are met
})
})
.then(response => response.json())
.then(data => {
if (data.verified) {
// Check if upgrade is needed (all thresholds false)
if (data.requires_upgrade && data.code) {
// Offer upgrade option
showUpgradeOption(data.age_thresholds, data.code);
} else {
// Age verification successful!
console.log('Age thresholds passed:', data.age_thresholds);
}
}
});
}

// Run when page loads
handleAgeKeyUpgradeCallback();

Step 5: Validate the ID token (server)

Critical Security Requirement

Always validate the id_token in your server, never trust client-side validation alone.

What you need to validate

  1. Signature: Verify the JWT is signed by AgeKey
  2. Issuer: Check iss claim equals https://api.agekey.org/v1/oidc/use
  3. Audience: Check aud claim contains your client_id
  4. Expiration: Check exp claim (token not expired)
  5. Nonce: Check nonce matches what you sent
Best Practice

Using a certified OIDC library handles all the complex JWT validation automatically and reduces the risk of security vulnerabilities.

const { Issuer, generators } = require('openid-client');

// One-time setup
const ageKeyIssuer = await Issuer.discover('https://api.agekey.org/v1/oidc/use');
const client = new ageKeyIssuer.Client({
client_id: 'your-client-id',
redirect_uris: ['https://yourapp.com/agekey/callback'],
response_types: ['id_token'],
});

// Validate the token
app.post('/api/validate-agekey-upgrade', async (req, res) => {
const { id_token, code } = req.body;
const nonce = req.session.agekey_upgrade_nonce; // Retrieve stored nonce

try {
const tokenSet = await client.callback(
'https://yourapp.com/agekey/callback',
{ id_token },
{ nonce }
);

const claims = tokenSet.claims();

// Extract age threshold results
const ageThresholds = claims.age_thresholds;
// Example: { "18": false }

// Check if all thresholds are false (requires upgrade)
const allThresholdsFalse = Object.values(ageThresholds).every(value => value === false);

if (allThresholdsFalse && code) {
// Store code for later use in upgrade flow
req.session.agekey_upgrade_code = code;

return res.json({
verified: true,
age_thresholds: ageThresholds,
requires_upgrade: true,
code: code
});
}

// Thresholds met, standard flow
res.json({
verified: true,
age_thresholds: ageThresholds,
requires_upgrade: false
});

} catch (error) {
console.error('Token validation failed:', error);
res.status(400).json({ verified: false });
}
});

Step 6: Check if upgrade is needed

After validating the token, check if all age thresholds are false. If they're, and a code was provided, offer the user the option to upgrade their AgeKey.

Code Example (JavaScript):

function showUpgradeOption(ageThresholds, code) {
// Show user-friendly message
const message = `Your AgeKey doesn't meet the age requirements. ` +
`Would you like to upgrade it with additional verification?`;

const userWantsToUpgrade = confirm(message);

if (userWantsToUpgrade) {
// Initiate upgrade flow
initiateUpgrade(code);
} else {
// User declined, show alternative verification options
showAlternativeVerification();
}
}

Step 7: Exchange code for access token

If the user wants to upgrade their AgeKey, exchange the authorization code for an access token. This token can be used to authorize the upgrade request.

Endpoint

POST https://api.agekey.org/v1/oidc/use/token

Headers

Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

Form fields

FieldValue
grant_typeauthorization_code
codeThe code received from the callback
const axios = require('axios');
const qs = require('querystring');

async function exchangeCodeForToken(code) {
const clientId = process.env.AGEKEY_CLIENT_ID;
const clientSecret = process.env.AGEKEY_CLIENT_SECRET;

// Create Basic auth header
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

const formData = {
grant_type: 'authorization_code',
code: code
};

try {
const response = await axios.post(
'https://api.agekey.org/v1/oidc/use/token',
qs.stringify(formData),
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

return response.data;
// Returns: { access_token: "...", token_type: "Bearer", expires_in: 3600 }

} catch (error) {
console.error('Token exchange failed:', error.response?.data);
throw error;
}
}

// Usage in your upgrade endpoint
app.post('/api/initiate-upgrade', async (req, res) => {
const code = req.session.agekey_upgrade_code;

if (!code) {
return res.status(400).json({ error: 'No authorization code found' });
}

try {
const tokenResponse = await exchangeCodeForToken(code);

// Store token for upgrade request
req.session.agekey_upgrade_token = tokenResponse.access_token;

// Clear code from session
delete req.session.agekey_upgrade_code;

res.json({
success: true,
message: 'Ready for additional verification'
});

} catch (error) {
console.error('Failed to exchange code for token:', error);
res.status(500).json({ error: 'Failed to initiate upgrade' });
}
});

Response

On success, you'll receive:

{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600
}
warning

Important: The access_token expires in 3600 seconds (1 hour). You should use it promptly for the upgrade request.


Step 8: Perform additional verification

At this point, you should perform another verification method to gather the age verification data that can be used to upgrade the AgeKey. This can be done before or after exchanging the code, depending on your UX flow.

For example:

  • If you already performed ID verification before initiating the upgrade, use that verification data
  • If you haven't yet, prompt the user to complete ID verification now

After successful verification, you should have:

  • Verification method used (for example, id_doc_scan, payment_card_network)
  • Age information (date of birth, age in years, or minimum age)
  • Timestamp of verification
  • Unique verification ID (you generate this)

Build authorization details

The authorization details is a JSON array containing your verification result. This is what you'll send to AgeKey for the upgrade. For complete details on the authorization details structure, see the Create AgeKey PAR API Reference.


Step 9: Submit upgrade request

Now submit the upgrade request to AgeKey's upgrade endpoint by using the access token you obtained.

Endpoint

POST https://api.agekey.org/v1/agekey/upgrade

Headers

Authorization: Basic <base64(access_token:)>
Content-Type: application/json

Body

The request body should contain the authorization_details in the same format as the Create AgeKey flow:

{
"authorization_details": [
{
"type": "age_verification",
"method": "<verification_method>",
"age": { <age_object> },
"verified_at": "<timestamp>",
"verification_id": "<your_unique_id>",
"attributes": { <optional_method_specific_data> }
}
]
}
const axios = require('axios');

async function submitUpgradeRequest(accessToken, verificationData) {
// Build authorization details
const authDetails = buildAuthorizationDetails(verificationData);

// Create Basic auth header with token
const credentials = Buffer.from(`${accessToken}:`).toString('base64');

try {
const response = await axios.post(
'https://api.agekey.org/v1/agekey/upgrade',
{
authorization_details: authDetails
},
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
}
);

return response.data;
// Returns: { "status": "success" } or similar

} catch (error) {
console.error('Upgrade request failed:', error.response?.data);
throw error;
}
}

// Usage in your upgrade submit endpoint
app.post('/api/upgrade-agekey', async (req, res) => {
const accessToken = req.session.agekey_upgrade_token;
const verificationData = req.body; // Contains your additional verification data

if (!accessToken) {
return res.status(400).json({ error: 'No access token found' });
}

try {
const result = await submitUpgradeRequest(accessToken, verificationData);

// Clear token from session
delete req.session.agekey_upgrade_token;

res.json({
success: true,
message: 'AgeKey upgraded successfully'
});

} catch (error) {
console.error('Failed to submit upgrade request:', error);
res.status(500).json({ error: 'Failed to upgrade AgeKey' });
}
});

Response

On success, you'll receive a response indicating the upgrade was successful:

{
"status": "success"
}

Step 10: Show confirmation to user

After handling the upgrade request, show an appropriate message to the user.

Front end Code Example:

async function initiateUpgrade(code) {
try {
// Exchange code for token
const exchangeResponse = await fetch('/api/initiate-upgrade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: code })
});

if (!exchangeResponse.ok) {
throw new Error('Failed to initiate upgrade');
}

// Perform additional verification (ID scan, etc.)
const verificationData = await performAdditionalVerification();

// Submit upgrade request
const upgradeResponse = await fetch('/api/upgrade-agekey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(verificationData)
});

const result = await upgradeResponse.json();

if (result.success) {
showSuccessMessage(
'AgeKey Upgraded!',
'Your AgeKey has been successfully upgraded. You can now use it for age-restricted content.'
);
} else {
showErrorMessage(
'Unable to upgrade AgeKey',
'Something went wrong during the upgrade process. Please try again later.'
);
}

} catch (error) {
console.error('Upgrade failed:', error);
showErrorMessage(
'Upgrade Failed',
'Unable to complete the upgrade. Please try again.'
);
}
}

This completes the Upgrade AgeKey flow. Users can now upgrade their AgeKey with additional verification when their existing AgeKey doesn't meet required age thresholds.