This example demonstrates how to protect Next.js API routes using StrongDM ID authentication with Edge Middleware.
- JWT signature verification using JWKS
- Edge middleware for low-latency auth
- Scope-based access control
- DPoP (Demonstrating Proof of Possession) support
- Token claims passed to route handlers via headers
npm installCreate a .env.local file:
STRONGDM_ISSUER=https://id.strongdm.ai
STRONGDM_AUDIENCE=my-api # Optionalnpm run dev# Get an access token (you'll need valid client credentials)
TOKEN=$(curl -s -X POST https://id.strongdm.ai/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=openid email" | jq -r '.access_token')
# Call a protected endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/protected├── lib/
│ └── strongdm-auth.ts # Auth library
├── middleware.ts # Edge middleware configuration
└── app/
└── api/
├── health/ # Public health check
├── protected/ # Basic auth required
├── agent-info/ # Shows token claims
└── admin/ # Requires admin
Edit middleware.ts to configure protected routes:
// Define your protected routes
const protectedRoutes: Record<string, RouteConfig> = {
"/api/protected": {}, // Any valid token
"/api/admin": { scopes: ["admin"] }, // Admin only
"/api/optional": { optional: true }, // Auth optional
};
// Public routes (skip authentication)
const publicRoutes = ["/api/health", "/api/public"];The middleware adds claims to request headers:
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
// Get individual claims
const subject = request.headers.get("x-strongdm-subject");
const scopes = request.headers.get("x-strongdm-scopes")?.split(" ");
// Or get full claims object
const claimsHeader = request.headers.get("x-strongdm-claims");
const claims = claimsHeader ? JSON.parse(claimsHeader) : null;
return NextResponse.json({ subject, scopes });
}For more control, use the library in API routes:
import { NextRequest, NextResponse } from "next/server";
import { StrongDMAuth, StrongDMAuthError } from "@/lib/strongdm-auth";
const auth = new StrongDMAuth();
export async function GET(request: NextRequest) {
try {
const claims = await auth.verifyRequest(
request.headers.get("authorization"),
request.headers.get("dpop"),
request.method,
request.url
);
// Check specific scopes
auth.checkScopes(claims, ["admin"]);
return NextResponse.json({ subject: claims.sub });
} catch (error) {
if (error instanceof StrongDMAuthError) {
return NextResponse.json(
{ error: error.message },
{ status: error.statusCode }
);
}
throw error;
}
}| Variable | Default | Description |
|---|---|---|
STRONGDM_ISSUER |
https://id.strongdm.ai |
Token issuer URL |
STRONGDM_AUDIENCE |
- | Expected audience claim |
STRONGDM_INTROSPECTION_ENABLED |
false |
Enable token introspection |
STRONGDM_CLIENT_ID |
- | Client ID (for introspection) |
STRONGDM_CLIENT_SECRET |
- | Client secret (for introspection) |
| Endpoint | Auth | Scopes | Description |
|---|---|---|---|
/api/health |
No | - | Health check |
/api/protected |
Yes | Any | Basic protected endpoint |
/api/agent-info |
Yes | Any | Returns token claims |
/api/admin |
Yes | admin | Admin operations |
| Status | Meaning |
|---|---|
401 |
Missing/invalid/expired token |
403 |
Valid token but insufficient scope |
Example error:
{
"error": "Missing required scopes: admin"
}For sender-constrained tokens:
curl -X GET http://localhost:3000/api/protected \
-H "Authorization: DPoP $DPOP_TOKEN" \
-H "DPoP: $DPOP_PROOF"- The middleware runs on the Edge runtime for low latency
- JWKS is cached for 15 minutes
- For high-security endpoints, enable introspection to check token revocation
- Always use HTTPS in production