TL;DR
Auth0 handles authentication, but security still depends on your implementation. Always validate tokens server-side with proper audience and issuer checks. Use RBAC for authorization instead of client-side checks. Configure callback URLs precisely (no wildcards in production). Keep your client secret actually secret, and rotate it if exposed.
Why Auth0 Security Matters for Vibe Coding
Auth0 is one of the most popular authentication platforms for modern applications. When AI tools generate Auth0 integration code, they often produce working authentication flows but miss critical security validations. The SDK handles the happy path, but security requires attention to edge cases and proper configuration.
Common issues include missing server-side token validation, overly permissive callback URLs, exposed client secrets, and authorization checks that only happen on the frontend.
Configuration Security
Environment Variables
# .env.local (never commit)
AUTH0_SECRET="use-a-32-character-minimum-random-string"
AUTH0_BASE_URL="https://yourdomain.com"
AUTH0_ISSUER_BASE_URL="https://your-tenant.auth0.com"
AUTH0_CLIENT_ID="your-client-id"
AUTH0_CLIENT_SECRET="your-client-secret" # Keep this secret!
# For API token validation
AUTH0_AUDIENCE="https://api.yourdomain.com"
Never Expose Your Client Secret: The AUTH0_CLIENT_SECRET must never be exposed to the browser or committed to version control. It should only exist in server-side environment variables. If it's exposed, rotate it immediately in the Auth0 dashboard.
Callback URL Configuration
In your Auth0 dashboard, configure callback URLs precisely:
# CORRECT: Specific URLs
Allowed Callback URLs:
https://yourdomain.com/api/auth/callback
https://staging.yourdomain.com/api/auth/callback
Allowed Logout URLs:
https://yourdomain.com
https://staging.yourdomain.com
# DANGEROUS: Wildcards (never use in production)
Allowed Callback URLs:
https://*.yourdomain.com/* # Attackers can register subdomains!
http://localhost:*/* # Development only, remove for production
Wildcard Callbacks Enable Attacks: Wildcard callback URLs allow attackers to redirect authentication to malicious endpoints. If an attacker can create a subdomain or find an open redirect on your site, they can steal tokens. Always use exact URLs in production.
Token Validation
Every API route must validate tokens server-side. Never trust tokens without verification.
Next.js API Route Protection
// middleware.ts - Protect all API routes
import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge';
export default withMiddlewareAuthRequired();
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
};
Manual Token Validation
// For custom validation or non-Next.js apps
import { jwtVerify } from 'jose';
async function validateToken(token: string) {
const JWKS = jose.createRemoteJWKSet(
new URL(`${process.env.AUTH0_ISSUER_BASE_URL}/.well-known/jwks.json`)
);
try {
const { payload } = await jwtVerify(token, JWKS, {
// CRITICAL: Verify these claims
issuer: process.env.AUTH0_ISSUER_BASE_URL + '/',
audience: process.env.AUTH0_AUDIENCE,
});
// Additional validation
if (!payload.sub) {
throw new Error('Missing subject claim');
}
return payload;
} catch (error) {
console.error('Token validation failed:', error);
throw new Error('Invalid token');
}
}
// Usage in API route
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return Response.json({ error: 'Missing token' }, { status: 401 });
}
const token = authHeader.slice(7);
try {
const payload = await validateToken(token);
// Token is valid, proceed with request
return Response.json({ userId: payload.sub });
} catch {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
}
Role-Based Access Control (RBAC)
Auth0's RBAC allows you to define permissions and assign them to users via roles. Always verify permissions server-side.
Setting Up RBAC in Auth0
- Go to Dashboard > Applications > APIs > Your API
- Enable "Enable RBAC" and "Add Permissions in the Access Token"
- Define permissions (e.g.,
read:documents,write:documents,admin:users) - Create roles and assign permissions
- Assign roles to users
Checking Permissions Server-Side
// utils/permissions.ts
import { getSession } from '@auth0/nextjs-auth0';
export async function hasPermission(permission: string): Promise<boolean> {
const session = await getSession();
if (!session?.accessToken) {
return false;
}
// Decode the access token to get permissions
const payload = JSON.parse(
Buffer.from(session.accessToken.split('.')[1], 'base64').toString()
);
const permissions = payload.permissions || [];
return permissions.includes(permission);
}
// API route with permission check
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
// First check authentication
const session = await getSession();
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Then check authorization (permission)
if (!await hasPermission('delete:documents')) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
// User is authenticated AND authorized
await deleteDocument(params.id);
return Response.json({ success: true });
}
Never Rely on Frontend Permission Checks: Frontend checks should only be used for UX (hiding buttons, etc.). Always verify permissions on the server before performing any action. Users can modify frontend code or call APIs directly.
Secure Session Management
// auth0.ts - Configure session security
import { initAuth0 } from '@auth0/nextjs-auth0';
export default initAuth0({
secret: process.env.AUTH0_SECRET,
issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
baseURL: process.env.AUTH0_BASE_URL,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
session: {
// Use rolling sessions
rolling: true,
// Absolute timeout
absoluteDuration: 60 * 60 * 24 * 7, // 7 days
// Inactivity timeout
rollingDuration: 60 * 60 * 24, // 1 day of inactivity
cookie: {
// Secure in production
secure: process.env.NODE_ENV === 'production',
// Strict same-site policy
sameSite: 'lax',
// HTTP-only prevents XSS access
httpOnly: true,
}
},
authorizationParams: {
// Request specific scopes
scope: 'openid profile email',
// Include audience for API access
audience: process.env.AUTH0_AUDIENCE,
}
});
Protecting Against Common Attacks
CSRF Protection
Auth0's SDK includes CSRF protection, but verify it's working:
// The SDK automatically adds state parameter
// Verify it's present in your callback handling
// If implementing custom callback:
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const state = searchParams.get('state');
const code = searchParams.get('code');
if (!state) {
return Response.json({ error: 'Missing state parameter' }, { status: 400 });
}
// Verify state matches what was sent
const savedState = cookies().get('auth_state')?.value;
if (state !== savedState) {
return Response.json({ error: 'State mismatch' }, { status: 400 });
}
// Continue with token exchange...
}
Token Storage
// CORRECT: Let Auth0 SDK handle token storage (HTTP-only cookies)
// The SDK stores tokens securely server-side
// DANGEROUS: Storing tokens in localStorage
localStorage.setItem('access_token', token); // XSS can steal this!
// DANGEROUS: Storing tokens in sessionStorage
sessionStorage.setItem('access_token', token); // XSS can steal this!
// If you must pass tokens to the frontend (for SPA APIs):
// Use short-lived tokens and refresh via server-side route
Multi-Factor Authentication
Enable and enforce MFA for sensitive operations:
// Auth0 Rule or Action to enforce MFA
exports.onExecutePostLogin = async (event, api) => {
// Require MFA for admin users
const roles = event.authorization?.roles || [];
if (roles.includes('admin')) {
// Check if MFA was used in this session
if (event.authentication?.methods) {
const mfaUsed = event.authentication.methods.some(
m => m.name === 'mfa'
);
if (!mfaUsed) {
// Trigger MFA challenge
api.multifactor.enable('any');
}
}
}
};
Auth0 Security Checklist
- Client secret stored in server-side environment variables only
- Callback URLs are exact matches (no wildcards in production)
- All API routes validate tokens server-side
- Token validation checks issuer AND audience
- RBAC permissions verified on server before actions
- Sessions use HTTP-only, secure, SameSite cookies
- Session timeouts configured appropriately
- MFA enabled for admin accounts
- Tokens never stored in localStorage/sessionStorage
- CSRF protection enabled (state parameter)
- Refresh tokens rotated on use
- Auth0 tenant has brute force protection enabled
Logging and Monitoring
Auth0 provides extensive logging. Use it for security monitoring:
// Set up Auth0 Log Streaming to your SIEM
// Or use the Management API to fetch logs
import { ManagementClient } from 'auth0';
const management = new ManagementClient({
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_MANAGEMENT_CLIENT_ID,
clientSecret: process.env.AUTH0_MANAGEMENT_CLIENT_SECRET,
});
// Fetch recent security events
const logs = await management.logs.getAll({
q: 'type:f OR type:fu OR type:fp', // Failed logins
sort: 'date:-1',
per_page: 100,
});
// Alert on suspicious patterns
const failedAttempts = logs.filter(log =>
log.type === 'f' && // Failed login
log.ip === suspiciousIP
);
if (failedAttempts.length > 10) {
await alertSecurityTeam('Brute force attempt detected');
}
Should I validate tokens on every API request?
Yes. Every API request should validate the token. Auth0's middleware and SDK do this automatically, but if you're implementing custom routes, ensure validation happens. Token validation is fast (using cached JWKS) and essential for security.
What's the difference between ID tokens and access tokens?
ID tokens contain user information (name, email) and are meant for your application to identify the user. Access tokens are for authorizing API requests and should contain permissions/scopes. Never send ID tokens to external APIs.
How do I handle token expiration?
Use refresh tokens to get new access tokens before they expire. Auth0's SDK handles this automatically. For SPAs, use silent authentication or refresh token rotation. Never extend access token lifetimes beyond what's necessary.
Can I trust the user's roles from the frontend?
No. Roles and permissions shown on the frontend are for UX only. Always verify roles/permissions server-side by checking the access token's claims. Users can modify frontend code to show any role they want.
What CheckYourVibe Detects
When scanning your Auth0-integrated project, CheckYourVibe identifies:
- Client secrets exposed in frontend code
- Missing server-side token validation
- API routes without authentication middleware
- Wildcard callback URLs in configuration
- Tokens stored in localStorage/sessionStorage
- Missing audience validation in token checks
- Frontend-only permission checks
Run npx checkyourvibe scan to catch these issues before they reach production.
Scan Your Auth0 Integration
Find token validation issues, exposed secrets, and configuration problems before they become security incidents.
Start Free Scan