Protect an API
This guide explains how to verify Aero2 access tokens in your backend API to protect your endpoints.
Overview
When a user signs in through Aero2, your frontend receives an access token. Your backend API should verify this token on every request to ensure the user is authenticated and authorized.
Steps
Fetch the JWKS
Aero2 publishes a JSON Web Key Set (JWKS) at a well-known URL. Start by discovering it from the OpenID Connect discovery endpoint:
GET https://your-app.aero2.dev/.well-known/openid-configurationThe response includes a jwks_uri field pointing to the JWKS endpoint (typically https://your-app.aero2.dev/oauth2/jwks.json).
The JWKS contains the public keys used to verify token signatures. Most libraries cache these keys automatically.
Verify the token
Use the jose library to verify tokens. It handles JWKS fetching, caching, and key rotation automatically.
npm install joseimport * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(
new URL('https://your-app.aero2.dev/oauth2/jwks.json')
);
async function verifyToken(token: string) {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: 'https://your-app.aero2.dev',
audience: 'your-client-id',
});
return payload;
}Extract user info
The verified payload contains the user's claims:
const payload = await verifyToken(accessToken);
const userId = payload.sub; // User ID
const email = payload.email; // Email address
const roles = payload.roles; // Assigned rolesExpress Middleware Example
Here is a complete middleware pattern for Express/Node.js:
import * as jose from 'jose';
import { Request, Response, NextFunction } from 'express';
const JWKS = jose.createRemoteJWKSet(
new URL('https://your-app.aero2.dev/oauth2/jwks.json')
);
async function authMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.slice(7);
try {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: 'https://your-app.aero2.dev',
audience: 'your-client-id',
});
// Attach user info to the request
req.user = {
id: payload.sub,
email: payload.email as string,
roles: payload.roles as string[],
};
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Use the middleware on protected routes
app.get('/api/protected', authMiddleware, (req, res) => {
res.json({ message: `Hello, ${req.user.email}` });
});Role-Based Authorization
You can extend the middleware to check for specific roles:
function requireRole(role: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user?.roles?.includes(role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Only admins can access this route
app.delete('/api/users/:id', authMiddleware, requireRole('admin'), handler);Security Checklist
- Always verify the token signature using the JWKS endpoint
- Check the
iss(issuer) claim matches your Aero2 application URL - Check the
aud(audience) claim matches your client ID - Check the
exp(expiration) claim to ensure the token has not expired - Use the JWKS endpoint for key discovery — never hardcode public keys
- Handle token verification errors gracefully and return appropriate HTTP status codes