auth0-express
Auth0 integration with Express.js
What this skill does
# Auth0 Express Skill
Patterns for integrating Auth0 with Express.js applications.
## Setup
### Installation
```bash
npm install express-oauth2-jwt-bearer
# or for session-based
npm install express-openid-connect
```
### Configuration
```typescript
// Environment variables
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=your-api-audience
AUTH0_CLIENT_ID=your-client-id
AUTH0_CLIENT_SECRET=your-client-secret
AUTH0_BASE_URL=http://localhost:3000
AUTH0_SECRET=session-encryption-secret
```
## JWT Authentication (API)
### Basic Setup
```typescript
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer'
import express from 'express'
const app = express()
// JWT validation middleware
const jwtCheck = auth({
audience: process.env.AUTH0_AUDIENCE,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}/`,
tokenSigningAlg: 'RS256',
})
// Public route
app.get('/api/public', (req, res) => {
res.json({ message: 'Public endpoint' })
})
// Protected route
app.get('/api/private', jwtCheck, (req, res) => {
res.json({
message: 'Protected endpoint',
user: req.auth?.payload,
})
})
// Route requiring specific scope
app.get(
'/api/admin',
jwtCheck,
requiredScopes('admin:read'),
(req, res) => {
res.json({ message: 'Admin data' })
}
)
```
### Custom Claims Access
```typescript
interface Auth0Payload {
sub: string
'https://myapp.com/roles': string[]
'https://myapp.com/org_id'?: string
permissions?: string[]
}
declare global {
namespace Express {
interface Request {
auth?: {
payload: Auth0Payload
token: string
}
}
}
}
app.get('/api/user-info', jwtCheck, (req, res) => {
const userId = req.auth?.payload.sub
const roles = req.auth?.payload['https://myapp.com/roles'] || []
const orgId = req.auth?.payload['https://myapp.com/org_id']
res.json({ userId, roles, orgId })
})
```
### Permission Middleware
```typescript
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
const permissions = req.auth?.payload.permissions || []
if (!permissions.includes(permission)) {
return res.status(403).json({
error: 'insufficient_permissions',
message: `Missing permission: ${permission}`,
})
}
next()
}
}
// Usage
app.delete(
'/api/users/:id',
jwtCheck,
requirePermission('delete:users'),
async (req, res) => {
await deleteUser(req.params.id)
res.status(204).send()
}
)
```
### Role-Based Access
```typescript
function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.auth?.payload['https://myapp.com/roles'] || []
const hasRole = roles.some(role => userRoles.includes(role))
if (!hasRole) {
return res.status(403).json({
error: 'insufficient_role',
message: `Requires one of: ${roles.join(', ')}`,
})
}
next()
}
}
app.get('/api/admin/stats', jwtCheck, requireRole('admin'), getStats)
```
## Session-Based Authentication
### Setup with express-openid-connect
```typescript
import { auth, requiresAuth } from 'express-openid-connect'
const app = express()
app.use(
auth({
authRequired: false,
auth0Logout: true,
secret: process.env.AUTH0_SECRET,
baseURL: process.env.AUTH0_BASE_URL,
clientID: process.env.AUTH0_CLIENT_ID,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
})
)
// Public route
app.get('/', (req, res) => {
res.send(req.oidc.isAuthenticated() ? 'Logged in' : 'Logged out')
})
// Protected route
app.get('/profile', requiresAuth(), (req, res) => {
res.json(req.oidc.user)
})
// Login/logout routes are automatic:
// /login
// /logout
// /callback
```
### Custom Login Options
```typescript
import { auth, requiresAuth } from 'express-openid-connect'
app.use(
auth({
authRequired: false,
auth0Logout: true,
secret: process.env.AUTH0_SECRET,
baseURL: process.env.AUTH0_BASE_URL,
clientID: process.env.AUTH0_CLIENT_ID,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
// Custom routes
routes: {
login: '/auth/login',
logout: '/auth/logout',
callback: '/auth/callback',
},
// Authorization params
authorizationParams: {
response_type: 'code',
scope: 'openid profile email',
audience: process.env.AUTH0_AUDIENCE,
},
// Session configuration
session: {
absoluteDuration: 60 * 60 * 24, // 24 hours
rolling: true,
},
})
)
```
### Getting Access Token
```typescript
app.get('/api/external', requiresAuth(), async (req, res) => {
try {
const { access_token } = await req.oidc.accessToken.refresh()
const response = await fetch(`${process.env.API_URL}/data`, {
headers: {
Authorization: `Bearer ${access_token}`,
},
})
res.json(await response.json())
} catch (error) {
res.status(500).json({ error: 'Failed to fetch data' })
}
})
```
## Error Handling
```typescript
import {
InvalidTokenError,
UnauthorizedError,
InsufficientScopeError,
} from 'express-oauth2-jwt-bearer'
// Error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof InvalidTokenError) {
return res.status(401).json({
error: 'invalid_token',
message: 'The access token is invalid',
})
}
if (err instanceof UnauthorizedError) {
return res.status(401).json({
error: 'unauthorized',
message: 'No authorization token was found',
})
}
if (err instanceof InsufficientScopeError) {
return res.status(403).json({
error: 'insufficient_scope',
message: `Required scope: ${err.requiredScopes}`,
})
}
console.error(err)
res.status(500).json({ error: 'Internal server error' })
})
```
## Multi-Tenancy
### Organization Validation
```typescript
function requireOrganization() {
return (req: Request, res: Response, next: NextFunction) => {
const orgId = req.auth?.payload['https://myapp.com/org_id']
if (!orgId) {
return res.status(403).json({
error: 'no_organization',
message: 'User must belong to an organization',
})
}
// Add to request for use in handlers
req.organizationId = orgId
next()
}
}
// Usage
app.get('/api/org/data', jwtCheck, requireOrganization(), async (req, res) => {
const data = await getOrgData(req.organizationId)
res.json(data)
})
```
### Scoped Data Access
```typescript
app.get('/api/resources', jwtCheck, async (req, res) => {
const userId = req.auth?.payload.sub
const orgId = req.auth?.payload['https://myapp.com/org_id']
// Query scoped by organization
const resources = await db.resources.findMany({
where: {
organizationId: orgId,
// Optionally filter by user
...(req.query.mine && { userId }),
},
})
res.json(resources)
})
```
## Testing
### Mock JWT Middleware
```typescript
// test/helpers/auth.ts
export function mockAuth(payload: Partial<Auth0Payload> = {}) {
return (req: Request, res: Response, next: NextFunction) => {
req.auth = {
payload: {
sub: 'auth0|test123',
'https://myapp.com/roles': ['user'],
permissions: [],
...payload,
},
token: 'mock-token',
}
next()
}
}
// In tests
jest.mock('express-oauth2-jwt-bearer', () => ({
auth: () => mockAuth({ permissions: ['read:data'] }),
}))
```
## Integration
Used by:
- `backend-developer` agent
- `fullstack-developer` agent
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.