Using an AgeKey
Overview
The Use AgeKey flow allows users with an existing AgeKey to quickly verify their age. This guide walks through implementing this flow step-by-step.
This guide uses the OpenID Connect implicit flow (response_type=id_token). If you already know this flow, you mainly need the AgeKey-specific piece: the claims parameter format (see the Use AgeKey API Reference). The rest follows the standard OIDC pattern to spec.
Time to implement: ~2 hours
High-level flow overview
Before diving into code, it's important to understand what happens during the Use AgeKey flow from start to finish.
The user's journey
1. User Needs Age Verification Imagine a user visiting your app and needs to prove they're old enough to access certain content (for example, 13+ or 18+ content). Instead of filling out forms or uploading IDs, they see a "Verify with AgeKey" button.
2. Redirect to AgeKey Service When they click the button, your app redirects their browser to the AgeKey service. In the URL, you include information about what age thresholds you need to check (such as "Is this person 13 or older?" and "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. It creates a signed token containing simple yes or no answers: "Yes, they're 13+" and "No, they're not 18+," for example.
5. Return to Your App The user's browser is redirected back to your app with this signed token in the URL. Your app receives the token but hasn't verified it yet - it's just a claim at this point.
6. Server validation Your server receives the 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. Grant Access Once validated, your app can trust the age verification results. If the user passed the required thresholds, you grant them access to the appropriate content. If not, you show them age-restricted content notices.
Visual flow diagram
Key technical concepts
OpenID Implicit Flow
This flow uses the OpenID Connect "implicit flow," which means the age verification result comes back as a signed token (id_token) directly in the URL, rather than requiring a separate API call to exchange a code for a token.
Age Thresholds, Not Exact Age
For privacy, you only learn yes or no answers to age questions. If you ask "Is this person 18 or older?," you get back true or false, never the person's actual age or date of birth.
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.
What you'll need to build
- Front end Component: A button/link that redirects users to AgeKey with appropriate parameters
- Callback Page: A page that receives the user when they return from AgeKey
- Server validation: Server-side code to verify the token and extract age threshold results
- Access Control Logic: Code to grant or deny access based on the verified age thresholds
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 | openid |
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 13 or older AND 18 or older
{
"age_thresholds": [13, 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):
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_state', state);
sessionStorage.setItem('agekey_nonce', nonce);
Code Example (Python):
import secrets
state = secrets.token_urlsafe(16)
nonce = secrets.token_urlsafe(16)
# Store both state and nonce in session (example using Flask)
session['agekey_state'] = state
session['agekey_nonce'] = nonce
Step 3: Build the authorization URL
Construct the URL to redirect the user to AgeKey.
If you're embedding the Use 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">
</iframe>
Base URL:
https://api.agekey.org/v1/oidc/use
Full Example URL:
https://api.agekey.org/v1/oidc/use?
scope=openid&
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%5B13%2C18%5D%7D
Code Example (JavaScript):
const params = new URLSearchParams({
scope: 'openid',
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: [13, 18] })
});
const authUrl = `https://api.agekey.org/v1/oidc/use?${params.toString()}`;
// Redirect user
window.location.href = authUrl;
Code Example (Python / Flask):
from urllib.parse import urlencode
params = {
'scope': 'openid',
'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': [13, 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 result in query string parameters.
Example Callback URL:
https://yourapp.com/agekey/callback?
state=abc123xyz789&
id_token=eyJhbGc...long-jwt-string...
Extract the token (front end)
Code Example (JavaScript):
// On your callback page
function handleAgeKeyCallback() {
// Get fragment parameters
const fragment = window.location.search;
const params = new URLSearchParams(fragment);
const idToken = params.get('id_token');
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_state');
if (returnedState !== storedState) {
console.error('State mismatch - possible CSRF attack');
return;
}
// Send token to server for validation
fetch('/api/validate-agekey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_token: idToken })
})
.then(response => response.json())
.then(data => {
if (data.verified) {
// Age verification successful!
console.log('Age thresholds passed:', data.age_thresholds);
}
});
}
// Run when page loads
handleAgeKeyCallback();
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', async (req, res) => {
const { id_token } = req.body;
const nonce = req.session.agekey_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: { "13": true, "18": false }
// Grant appropriate access based on results
if (ageThresholds["13"]) {
// User is 13 or older
}
if (ageThresholds["18"]) {
// User is 18 or older
}
res.json({ verified: true, age_thresholds: ageThresholds });
} 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', methods=['POST'])
def validate_agekey():
id_token = request.json['id_token']
nonce = session.get('agekey_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: { "13": True, "18": False }
return jsonify({
'verified': True,
'age_thresholds': age_thresholds
})
except Exception as e:
print(f'Token validation failed: {e}')
return jsonify({'verified': False}), 400
Step 6: Act on the Results
The age_thresholds claim contains a boolean value for each threshold you requested.
Example Result:
{
"13": true,
"18": false
}
This means:
- User is 13 or older ✅
- User isn't 18 or older ❌
Code Example:
if (ageThresholds["13"]) {
// Grant access to 13+ content
allowUserAccess();
}
if (ageThresholds["18"]) {
// Grant access to 18+ content
allowAdultContent();
} else {
// Show age-restricted notice
showRestrictionMessage();
}
This completes the Use AgeKey flow. You can now gate content based on verified age thresholds without handling sensitive personal data.