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.
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:
- Initial AgeKey verification that determines thresholds aren't met (shown in the
id_token) - 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
- Front end Component: A button/link that redirects users to AgeKey with
agekey-upgradescope - Callback Page: A page that receives the user when they return from AgeKey with code and token
- Server validation: Server-side code to verify the token and extract age threshold results
- Code Exchange Handler: Server-side code to exchange the code for an access token
- Additional Verification: Logic to perform a second verification method after obtaining the token
- Upgrade Request Handler: Server-side code to submit upgrade data to AgeKey's upgrade endpoint
- Access Control Logic: Code to offer upgrade when all thresholds are false
- User Experience Flow: UI to offer AgeKey upgrade and show confirmation messages
When to offer AgeKey upgrade
- ✅ After failed threshold check: Offer when
id_tokenshows 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
| Parameter | What it's | Example |
|---|---|---|
client_id | Your AgeKey client ID (provided by AgeKey) | your-app-id-123 |
redirect_uri | Where users return after verification (must be pre-registered) | https://yourapp.com/agekey/callback |
scope | Always set to openid agekey-upgrade | openid agekey-upgrade |
response_type | Always set to id_token (implicit flow) | id_token |
state | Random token you generate for CSRF protection | abc123xyz789 |
nonce | Random value for replay protection | nonce456def |
claims | URL encoded JSON string specifying age thresholds to check | See 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
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:
- Javascript
- Python
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);
import secrets
state = secrets.token_urlsafe(16)
nonce = secrets.token_urlsafe(16)
# Store both state and nonce in session (example using Flask)
session['agekey_upgrade_state'] = state
session['agekey_upgrade_nonce'] = nonce
Step 3: Build the authorization URL
Construct the URL to redirect the user to AgeKey.
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:
- Javascript
- Python
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;
from urllib.parse import urlencode
params = {
'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.dumps({'age_thresholds': [18]})
}
auth_url = f"https://api.agekey.org/v1/oidc/use?{urlencode(params)}"
return redirect(auth_url)
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)
Always validate the id_token in your server, never trust client-side validation alone.
What you need to validate
- Signature: Verify the JWT is signed by AgeKey
- Issuer: Check
issclaim equalshttps://api.agekey.org/v1/oidc/use - Audience: Check
audclaim contains yourclient_id - Expiration: Check
expclaim (token not expired) - Nonce: Check
noncematches what you sent
Using an OIDC library (recommended)
Using a certified OIDC library handles all the complex JWT validation automatically and reduces the risk of security vulnerabilities.
- Node.js with openid-client
- Python with python-jose
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 });
}
});
from jose import jwt
import requests
# Fetch JWKS (do this once and cache)
jwks_uri = 'https://api.agekey.org/.well-known/jwks.json'
jwks = requests.get(jwks_uri).json()
@app.route('/api/validate-agekey-upgrade', methods=['POST'])
def validate_agekey_upgrade():
id_token = request.json['id_token']
code = request.json.get('code')
nonce = session.get('agekey_upgrade_nonce')
try:
# Decode and validate
claims = jwt.decode(
id_token,
jwks,
algorithms=['RS256'],
audience='your-client-id',
issuer='https://api.agekey.org/v1/oidc/use',
options={'verify_nonce': True}
)
# Verify nonce
if claims['nonce'] != nonce:
return jsonify({'verified': False}), 400
# Extract age threshold results
age_thresholds = claims['age_thresholds']
# Example: { "18": False }
# Check if all thresholds are false (requires upgrade)
all_thresholds_false = all(value == False for value in age_thresholds.values())
if all_thresholds_false and code:
# Store code for later use in upgrade flow
session['agekey_upgrade_code'] = code
return jsonify({
'verified': True,
'age_thresholds': age_thresholds,
'requires_upgrade': True,
'code': code
})
# Thresholds met, standard flow
return jsonify({
'verified': True,
'age_thresholds': age_thresholds,
'requires_upgrade': False
})
except Exception as e:
print(f'Token validation failed: {e}')
return jsonify({'verified': False}), 400
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
| Field | Value |
|---|---|
grant_type | authorization_code |
code | The code received from the callback |
- Node.js
- Python
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' });
}
});
import requests
import base64
from urllib.parse import urlencode
def exchange_code_for_token(code):
client_id = os.getenv('AGEKEY_CLIENT_ID')
client_secret = os.getenv('AGEKEY_CLIENT_SECRET')
# Create Basic auth header
credentials = base64.b64encode(
f'{client_id}:{client_secret}'.encode()
).decode()
form_data = {
'grant_type': 'authorization_code',
'code': code
}
try:
response = requests.post(
'https://api.agekey.org/v1/oidc/use/token',
data=form_data,
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/x-www-form-urlencoded'
}
)
response.raise_for_status()
return response.json()
# Returns: { "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }
except requests.exceptions.RequestException as e:
print(f'Token exchange failed: {e}')
raise
# Usage in your upgrade endpoint
@app.route('/api/initiate-upgrade', methods=['POST'])
def initiate_upgrade():
code = session.get('agekey_upgrade_code')
if not code:
return jsonify({'error': 'No authorization code found'}), 400
try:
token_response = exchange_code_for_token(code)
# Store token for upgrade request
session['agekey_upgrade_token'] = token_response['access_token']
# Clear code from session
session.pop('agekey_upgrade_code', None)
return jsonify({
'success': True,
'message': 'Ready for additional verification'
})
except Exception as e:
print(f'Failed to exchange code for token: {e}')
return jsonify({'error': 'Failed to initiate upgrade'}), 500
Response
On success, you'll receive:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600
}
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> }
}
]
}
- Node.js
- Python
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' });
}
});
import requests
import base64
import json
def submit_upgrade_request(access_token, verification_data):
# Build authorization details
auth_details = build_authorization_details(verification_data)
# Create Basic auth header with token
credentials = base64.b64encode(
f'{access_token}:'.encode()
).decode()
try:
response = requests.post(
'https://api.agekey.org/v1/agekey/upgrade',
json={
'authorization_details': auth_details
},
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/json'
}
)
response.raise_for_status()
return response.json()
# Returns: { "status": "success" } or similar
except requests.exceptions.RequestException as e:
print(f'Upgrade request failed: {e}')
raise
# Usage in your upgrade submit endpoint
@app.route('/api/upgrade-agekey', methods=['POST'])
def upgrade_agekey():
access_token = session.get('agekey_upgrade_token')
verification_data = request.json # Contains your additional verification data
if not access_token:
return jsonify({'error': 'No access token found'}), 400
try:
result = submit_upgrade_request(access_token, verification_data)
# Clear token from session
session.pop('agekey_upgrade_token', None)
return jsonify({
'success': True,
'message': 'AgeKey upgraded successfully'
})
except Exception as e:
print(f'Failed to submit upgrade request: {e}')
return jsonify({'error': 'Failed to upgrade AgeKey'}), 500
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.