Backend services need to verify JWT tokens from authentication cookies and authorize requests based on user roles stored in DynamoDB.
The @zooly/auth-srv package provides JWT verification utilities:
import { verifyIdToken } from '@zooly/auth-srv';
async function handler(request: Request) {
// Extract token from cookie
const token = request.cookies.get('id_token')?.value;
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
try {
// Verify JWT signature and claims
const payload = await verifyIdToken(token);
// payload contains:
// - sub: Cognito user ID
// - email: User email (source of truth)
// - exp: Expiration timestamp
// - iss: Issuer
// - aud: Audience (client ID)
return payload;
} catch (error) {
return new Response('Invalid token', { status: 401 });
}
}
If you need to verify tokens manually:
import { CognitoJwtVerifier } from 'aws-jwt-verify';
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!,
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID!,
});
async function verifyToken(token: string) {
try {
const payload = await verifier.verify(token);
return payload;
} catch (error) {
throw new Error('Token verification failed');
}
}
The verifier checks:
iss): Matches Cognito User Poolexp): Token not expiredaud): Matches Cognito App Client IDAfter verifying the JWT, look up the user profile from DynamoDB:
import { getIdentity } from '@zooly/auth-db';
async function getUserProfile(sub: string) {
const identity = await getIdentity(sub);
if (!identity) {
throw new Error('User not found');
}
return {
id: identity.user_id,
email: payload.email, // From JWT, not DynamoDB
name: identity.display_name,
image: identity.avatar_url,
roles: identity.roles || [],
};
}
Here's a complete example of a /api/me endpoint:
import { verifyIdToken } from '@zooly/auth-srv';
import { getIdentity } from '@zooly/auth-db';
export async function GET(request: Request) {
// Extract token from cookie
const token = request.cookies.get('id_token')?.value;
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// Verify JWT
const payload = await verifyIdToken(token);
// Get user profile from DynamoDB
const identity = await getIdentity(payload.sub);
if (!identity) {
return Response.json({ error: 'User not found' }, { status: 404 });
}
// Return unified profile
return Response.json({
id: identity.user_id,
email: payload.email, // From JWT (source of truth)
name: identity.display_name,
image: identity.avatar_url,
roles: identity.roles || [],
});
} catch (error) {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
}
Check user roles for authorization:
import { verifyIdToken } from '@zooly/auth-srv';
import { getIdentity } from '@zooly/auth-db';
async function requireRole(request: Request, requiredRole: string) {
const token = request.cookies.get('id_token')?.value;
if (!token) {
throw new Error('Unauthorized');
}
const payload = await verifyIdToken(token);
const identity = await getIdentity(payload.sub);
if (!identity) {
throw new Error('User not found');
}
const roles = identity.roles || [];
if (!roles.includes(requiredRole)) {
throw new Error('Forbidden');
}
return { user: identity, payload };
}
// Usage
async function adminHandler(request: Request) {
const { user } = await requireRole(request, 'admin');
// User has admin role
}
Create middleware for protected routes:
import { verifyIdToken } from '@zooly/auth-srv';
import { getIdentity } from '@zooly/auth-db';
export async function authMiddleware(request: Request) {
const token = request.cookies.get('id_token')?.value;
if (!token) {
return {
error: 'Unauthorized',
status: 401,
};
}
try {
const payload = await verifyIdToken(token);
const identity = await getIdentity(payload.sub);
if (!identity) {
return {
error: 'User not found',
status: 404,
};
}
return {
user: {
id: identity.user_id,
email: payload.email,
name: identity.display_name,
image: identity.avatar_url,
roles: identity.roles || [],
},
payload,
};
} catch (error) {
return {
error: 'Invalid token',
status: 401,
};
}
}
// Usage in route handler
export async function GET(request: Request) {
const auth = await authMiddleware(request);
if (auth.error) {
return Response.json({ error: auth.error }, { status: auth.status });
}
const { user } = auth;
// Use authenticated user
}
For unauthenticated requests, redirect to the auth app:
export async function authMiddleware(request: Request) {
const token = request.cookies.get('id_token')?.value;
if (!token) {
const url = new URL(request.url);
const returnTo = encodeURIComponent(url.pathname + url.search);
const authUrl = `https://auth.zooly.ai?returnTo=${returnTo}`;
return Response.redirect(authUrl, 302);
}
// Continue with verification...
}
For guest checkout endpoints, use getOrCreateByEmail:
import { getOrCreateByEmail } from '@zooly/auth-db';
export async function POST(request: Request) {
const { email, name } = await request.json();
// Get or create guest identity
const identity = await getOrCreateByEmail(email, name);
// Use identity.user_id for purchase association
return Response.json({ userId: identity.user_id });
}
Required environment variables:
# Cognito
COGNITO_USER_POOL_ID=us-east-1_tvlpQoJAn
COGNITO_CLIENT_ID=2i28c7h1cpm6od29o7vatlncv9
COGNITO_REGION=us-east-1
# DynamoDB
DYNAMODB_IDENTITIES_TABLE=zooly-auth-identities
DYNAMODB_REGION=us-east-1
Handle common errors:
try {
const payload = await verifyIdToken(token);
} catch (error) {
if (error instanceof TokenExpiredError) {
// Token expired - redirect to login
return Response.redirect('/auth/login', 302);
}
if (error instanceof TokenInvalidError) {
// Invalid token - clear cookie and redirect
return Response.redirect('/auth/login', 302);
}
// Other errors
return Response.json({ error: 'Authentication failed' }, { status: 500 });
}