cedar-authorize
v1.0.9
Published
Express.js/Next.js authorization middleware for Amazon Verified Permissions with Cedar policies. Per-tenant policy store routing, can() helper, and fail-closed error handling.
Maintainers
Readme
cedar-authorize
Cedar authorization middleware for Express.js and Next.js with multi-tenant policy store routing, powered by Amazon Verified Permissions.
This package provides what the existing AWS Cedar packages do not: multi-tenant support with per-tenant policy store routing, Next.js middleware, and field-level authorization. It handles JWT extraction, tenant context injection, entity mapping, and fail-closed error handling in a single integration.
Why This Exists
AWS provides excellent building blocks for Cedar-based authorization:
| Package | What It Does |
|---------|--------------|
| @aws-sdk/client-verifiedpermissions | Raw API client for Amazon Verified Permissions |
| @cedar-policy/authorization-for-expressjs | Basic Express middleware for Cedar |
| @verifiedpermissions/authorization-clients-js | Authorization client abstraction |
The problem: the raw AWS SDK requires 50-80 lines of setup per service, and even the higher-level Express middleware requires wiring two packages together with no support for multi-tenant routing, Next.js, or field-level authorization. Every team that needs these capabilities builds them from scratch.
This SDK solves that. One import, one config object, done.
What This Adds Over the Underlying Packages
| Feature | Raw AWS Packages | cedar-authorize |
|---------|-----------------|---------------------|
| Express.js middleware | Manual setup | cedarAuthorize(config) - one line |
| Next.js middleware | Not provided | cedarMiddleware(config) - one line |
| Per-tenant policy store routing | Not provided (single store per client) | Dynamic resolver per request |
| JWT extraction | You write it | Automatic from Authorization: Bearer header |
| Multi-tenant context | You wire it | Auto-injects custom:tenantId into every AVP request |
| HTTP to Cedar entity mapping | You define it | GET /docs/123 maps to Action::"GET /docs/123" automatically |
| Field-level authorization | Not provided | can(user, action, resource) helper |
| Fail-closed error handling | You implement it | Default deny on any AVP error (configurable) |
| Observability | Not provided | Event emitter for denied and error events |
| Custom mappers | N/A | Pluggable identityExtractor, actionMapper, resourceMapper |
| Zero-config defaults | N/A | Works out of the box with Cognito + AVP |
Install
npm install cedar-authorizeOr with pnpm:
pnpm add cedar-authorizeOr with yarn:
yarn add cedar-authorizePeer dependency: express (optional, only needed if using Express middleware)
Verify Installation
node -e "const { cedarAuthorize, can } = require('cedar-authorize'); console.log('cedarAuthorize:', typeof cedarAuthorize); console.log('can:', typeof can)"Expected output:
cedarAuthorize: function
can: functionUninstall
npm uninstall cedar-authorizeQuick Start
Express.js: Protect All Routes
import express from 'express';
import { cedarAuthorize } from 'cedar-authorize';
const app = express();
app.use(cedarAuthorize({
policyStoreId: 'PSxxxxxxxxxxxxxxx',
region: 'us-east-1',
}));
app.get('/documents', (req, res) => {
// Only reaches here if Cedar policy permits
res.json({ documents: [] });
});
app.listen(3000);Next.js: Middleware
// middleware.ts
import { cedarMiddleware } from 'cedar-authorize/nextjs';
export default cedarMiddleware({
policyStoreId: 'PSxxxxxxxxxxxxxxx',
region: 'us-east-1',
skipPaths: ['/_next', '/api/health'],
});
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*'],
};Field-Level Authorization: can() Helper
import { can } from 'cedar-authorize';
app.get('/documents/:id', async (req, res) => {
const user = req.cedarAuth.identity;
const doc = await getDocument(req.params.id);
if (await can(user, 'ViewConfidential', { type: 'Document', id: req.params.id }, {
policyStoreId: 'PSxxx',
region: 'us-east-1',
})) {
res.json({ ...doc, notes: doc.confidentialNotes });
} else {
res.json({ ...doc, notes: '[REDACTED]' });
}
});How It Works
Request
1. Extract Bearer token from Authorization header
2. Decode JWT to get principal (sub), tenantId, roles
3. Map HTTP method + path to Cedar Action
4. Map request path to Cedar Resource
5. Inject tenantId into AVP context
6. Call AVP IsAuthorizedWithToken
7. ALLOW: attach cedarAuth to req, call next()
DENY: emit 'denied' event, return 403
ERROR: fail-closed (403), emit 'error' eventConfiguration
app.use(cedarAuthorize({
// Required
policyStoreId: 'PSxxxxxxxxxxxxxxx',
region: 'us-east-1',
// Optional: custom identity extraction
identityExtractor: (req) => ({
principal: { type: 'User', id: req.user.id },
claims: req.user,
tenantId: req.user.orgId,
}),
// Optional: custom action mapping
actionMapper: (req) => ({
type: 'Action',
id: `${req.method}:${req.route.path}`,
}),
// Optional: custom resource mapping
resourceMapper: (req) => ({
type: 'Document',
id: req.params.id || '/',
}),
// Optional: behavior on AVP errors (default: 'deny')
onError: 'deny', // or 'allow'
// Optional: event emitter for observability
emitter: myEventEmitter,
}));Multi-Tenant Support
cedar-authorize provides two levels of multi-tenant support:
1. Tenant Context Injection (automatic)
If the JWT contains a custom:tenantId claim, the middleware automatically includes it in the AVP context on every request. Write tenant-scoped Cedar policies:
permit(
principal,
action,
resource
) when {
principal has tenantId &&
resource has tenantId &&
principal.tenantId == resource.tenantId
};No extra code needed. The SDK handles the plumbing.
2. Per-Tenant Policy Store Routing
For SaaS applications that use separate AVP policy stores per tenant, pass a resolver function instead of a static string:
app.use(cedarAuthorize({
policyStoreId: async (identity) => {
// identity.tenantId is extracted from the JWT automatically
const storeId = await lookupPolicyStoreForTenant(identity.tenantId);
return storeId;
},
region: 'us-east-1',
}));The resolver receives the decoded UserIdentity (including tenantId) and returns the policy store ID to use for that request. This works with cedarAuthorize(), cedarMiddleware(), and can().
Example with a static mapping:
const TENANT_STORES: Record<string, string> = {
'tenant-1': 'PS-store-for-tenant-1',
'tenant-2': 'PS-store-for-tenant-2',
};
app.use(cedarAuthorize({
policyStoreId: (identity) => TENANT_STORES[identity.tenantId] || 'PS-default',
region: 'us-east-1',
}));Example with DynamoDB lookup:
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
const ddb = new DynamoDBClient({ region: 'us-east-1' });
app.use(cedarAuthorize({
policyStoreId: async (identity) => {
const result = await ddb.send(new GetItemCommand({
TableName: 'TenantConfig',
Key: { tenantId: { S: identity.tenantId } },
}));
return result.Item?.policyStoreId?.S || 'PS-default';
},
region: 'us-east-1',
}));The resolved policy store ID is available on req.cedarAuth.policyStoreId after authorization succeeds.
Observability
import { cedarAuthorize } from 'cedar-authorize';
import { EventEmitter } from 'events';
const emitter = new EventEmitter();
emitter.on('denied', ({ identity, action, resource }) => {
console.log(`DENIED: ${identity.principal.id} on ${resource.id}`);
});
emitter.on('error', (err) => {
alertOps('Authorization service error', err);
});
app.use(cedarAuthorize({ policyStoreId: 'PSxxx', region: 'us-east-1', emitter }));API Reference
cedarAuthorize(config): ExpressMiddleware
Express.js middleware. On ALLOW, attaches req.cedarAuth with { identity, decision, determiningPolicies }.
cedarMiddleware(config): NextMiddleware
Next.js middleware. Returns undefined (continue) on ALLOW, Response(403) on DENY.
can(user, action, resource, config): Promise<boolean>
Standalone authorization check. Returns true if permitted, false on deny or error (fail-closed). Use inside route handlers for field-level access control.
Prerequisites
- An Amazon Verified Permissions policy store
- A Cognito User Pool (or any OIDC provider) configured as an identity source in AVP
- Cedar policies deployed to the policy store
- AWS credentials configured (IAM role with
verifiedpermissions:IsAuthorizedWithTokenpermission) - Node.js 18+
Example: Cedar Policy Management Platform
The Cedar Policy Management Platform was built using this SDK to demonstrate how it works in action. It includes:
- A deployed Express.js API using
cedar-authorizewith all three features (static store, per-tenant routing, field-level auth) - A visual Cedar policy editor with real-time authorization testing
- CDK infrastructure for Cognito, API Gateway, AVP, and Lambda
- CI/CD pipeline for Cedar policy deployment
The Express demo endpoints show each feature in isolation:
/sdk-demo/api/static/documents- static policy store authorization/sdk-demo/api/multi-tenant/documents- per-tenant policy store routing/sdk-demo/api/documents/:id- field-level authorization withcan()
License
Apache-2.0
Author
Param - [email protected]
Built on top of Amazon Verified Permissions and the Cedar policy language.
