Security
Security best practices for AgeOnce integration
Security
Recommendations and best practices for secure AgeOnce integration.
Credential Protection
Client Secret
NEVER publish Client Secret in:
- Frontend code (JavaScript, HTML)
- Public repositories (GitHub, GitLab)
- Client mobile applications
- Logs or error messages
Proper storage:
# .env file (add to .gitignore!)
AGEONCE_CLIENT_ID=cl_abc123
AGEONCE_CLIENT_SECRET=cs_secret456// Usage in code
const clientSecret = process.env.AGEONCE_CLIENT_SECRET;Key Rotation
If you suspect Client Secret compromise:
- Immediately generate a new key in the Dashboard
- Update configuration on all servers
- The old key will be automatically deactivated
CSRF Protection
State Parameter
Always use the state parameter for CSRF attack protection:
// Generate
const state = crypto.randomBytes(32).toString('hex');
session.oauthState = state;
// Verify on callback
if (req.query.state !== session.oauthState) {
throw new Error('CSRF attack detected');
}Without state verification, an attacker can force a user to authorize on behalf of another account.
Token Validation
Required Checks
When validating age token, verify:
function validateToken(payload) {
// 1. Verify issuer
if (payload.iss !== 'ageonce') {
throw new Error('Invalid issuer');
}
// 2. Verify expiration
if (payload.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
// 3. Verify client_id
if (payload.client_id !== process.env.AGEONCE_CLIENT_ID) {
throw new Error('Token not issued for this client');
}
// 4. Verify verification
if (!payload.age_verified) {
throw new Error('Age not verified');
}
return true;
}Cryptographic Validation
Tokens are signed with RS256. Verify the signature:
const jwt = require('jsonwebtoken');
// Get public key from JWKS
const publicKey = await getPublicKeyFromJWKS();
// Validation with signature verification
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Important! Don't allow other algorithms
issuer: 'ageonce',
});Never disable signature verification! Without it, an attacker can forge a token.
HTTPS
Requirement
AgeOnce works only over HTTPS. HTTP requests will be rejected.
Ensure that:
- Your site uses an SSL certificate
redirect_uristarts withhttps://- All API calls go to
https://app.ageonce.com
HSTS
We recommend configuring HTTP Strict Transport Security:
Strict-Transport-Security: max-age=31536000; includeSubDomainsRedirect URI
Validation
- Register only exact URLs (no wildcards)
- Avoid
localhostin production - Don't use HTTP URLs
Correct:
https://app.example.com/callback
https://shop.example.com/ageonce/callbackIncorrect:
https://example.com/*
http://example.com/callback
https://localhost:3000/callback (development only)Error Handling
Don't Expose Details
// Bad - exposes internal details
app.get('/callback', async (req, res) => {
try {
// ...
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
});
// Good - generic message
app.get('/callback', async (req, res) => {
try {
// ...
} catch (error) {
console.error('Auth error:', error); // Log for yourself
res.status(500).json({ error: 'Authentication failed' });
}
});Logging
What to Log
// Log events
logger.info('Age verification started', { clientId, timestamp });
logger.info('Age verification success', { clientId, minAge: 18 });
logger.warn('Age verification failed', { clientId, reason: 'expired_code' });What NOT to Log
// Never log
logger.info('Token exchange', {
clientSecret, // NO!
ageToken, // NO! Contains personal data
fullRequest // NO! May contain secrets
});Privacy
Data Minimization
AgeOnce is designed with data minimization principles:
subis always"anonymous"— we don't identify users- Biometric data is not stored
- We only know the fact of verification
GDPR
- You are the controller of verification data
- AgeOnce is the processor
- See our Privacy Policy for details
Security Checklist
- Client Secret is stored in environment variables
- Client Secret is not in the repository
- State parameter is generated and verified
- Tokens are validated with signature verification
- All URLs use HTTPS
- Redirect URI is correctly registered
- Errors don't expose internal details
- Logs don't contain secrets or tokens