Authentication & Access Control
Section titled “Authentication & Access Control”Authentication
Section titled “Authentication”Bridge Express evaluates two independent authentication paths on every request:
- User JWT — sent via
Authorization: Bearer <token>. Verified against Bridge’s JWKS endpoint. The standard path for browser-based users. - API token — sent via
x-api-keyas a JWT. Verified via Bridge token introspection (the app never holds the signing secret). The path for server-to-server / programmatic access.
The two paths are evaluated independently: when both an x-api-key and an Authorization: Bearer header are present and valid, both contexts coexist on the request (req.bridgeApiToken and req.bridgeUser are both set).
Accessing user information
Section titled “Accessing user information”After a request authenticates via a user JWT, the verified user is on req.bridgeUser:
import { Router } from 'express';
const router = Router();
router.get('/users/me', (req, res) => { const user = req.bridgeUser!; res.json({ id: user.id, email: user.email, username: user.username, fullName: user.fullName, tenantId: user.tenantId, appId: user.appId, role: user.role, });});
export default router;The BridgeUser interface:
interface BridgeUser { id: string; // User ID (sub claim) email: string; // User's email emailVerified: boolean; username: string; // preferred_username claim fullName: string; // Display name givenName?: string; familyName?: string; locale?: string; onboarded?: boolean; tenantId: string; // Tenant/workspace ID appId?: string; // App ID from the token (aid claim) scope?: string; // OAuth scopes granted to the token role?: string; // User's role within the tenant multiTenantAccess?: boolean;}The user’s privileges claim from the JWT is what the route-rule privilege check (below) evaluates against.
Accessing tenant information
Section titled “Accessing tenant information”The tenant the user is authenticated for is on req.bridgeTenant:
router.get('/workspace', (req, res) => { const user = req.bridgeUser!; const tenant = req.bridgeTenant; res.json({ user: { id: user.id, email: user.email, role: user.role }, tenant: tenant && { id: tenant.id, name: tenant.name, locale: tenant.locale, logo: tenant.logo, onboarded: tenant.onboarded, }, });});The BridgeTenant interface:
interface BridgeTenant { id: string; name: string; locale?: string; logo?: string; onboarded?: boolean;}The raw access token
Section titled “The raw access token”req.bridgeAccessToken holds the raw user JWT string. Use it to forward the token to downstream services (see Frontend Integration) or to open a tenant scope (see Tenant Data):
const tenant = bridge.fromJwt(req.bridgeAccessToken!);Declarative vs per-route protection
Section titled “Declarative vs per-route protection”Declarative guard (recommended)
Section titled “Declarative guard (recommended)”Mount bridge.auth() as application- or router-level middleware. It reads the guard config and applies defaultAccess plus your route rules to every route registered after it:
const bridge = createBridge({ appId: 'YOUR_APP_ID', guard: { defaultAccess: 'protected', rules: [ { path: '/health', privilege: 'ANONYMOUS' }, { path: '/webhooks/*', privilege: 'ANONYMOUS' }, ], },});
app.use(bridge.auth());With the declarative guard mounted, use bridge.public() to mark exceptions next to the handler:
app.get('/health', bridge.public(), (_req, res) => { res.json({ status: 'ok' });});Per-route protection
Section titled “Per-route protection”bridge.protect(options?) always enforces auth on the route it’s attached to, regardless of defaultAccess. It does not consult config route rules — its options are the rule. Use it to protect a single route, or to apply role / privilege / feature-flag / accepted-auth overrides:
// Force auth on one route even if defaultAccess is 'public'app.get('/secret', bridge.protect(), handler);
// Require an ADMIN role (user JWT)app.delete('/admin/users/:id', bridge.protect({ role: 'ADMIN' }), handler);You can mount bridge.protect() on a whole router to protect a group of routes:
import { Router } from 'express';const admin = Router();admin.use(bridge.protect({ role: 'ADMIN' }));admin.get('/dashboard', handler); // all admin routes require ADMINadmin.get('/settings', handler);app.use('/admin', admin);API Token Authentication
Section titled “API Token Authentication”How it works
Section titled “How it works”When an x-api-key header carries a JWT-shaped token, Bridge Express verifies it by POSTing it to the Bridge token-introspection endpoint ({apiBaseUrl}/account/api-token/introspect). The app never holds the HS256 signing secret — verification is a network call to the Bridge, not a local signature check. The Bridge collapses every rejection (forged, tampered, revoked, expired) into { active: false }. On success, the claims are attached to req.bridgeApiToken.
User JWTs bypass the
privilegeoption.bridge.protect({ privilege })enforces the privilege only for API-token callers. User JWTs are governed by route-rule privilege,role, andfeatureFlaginstead. This keeps an endpoint that adds aprivilegeoption for API tokens from breaking user-JWT access.
ApiTokenClaims type
Section titled “ApiTokenClaims type”When an API token verifies, req.bridgeApiToken is set with these claims:
interface ApiTokenClaims { active: boolean; // Whether the token is active (always true once attached) sub: string; // Token subject identifier appId: string; // App ID the token was issued for tenantId: string | null; // Tenant ID (null for app-level tokens) type: 'api'; // Always 'api' for API tokens privileges: string[]; // Privilege strings (e.g. ['USER_READ', 'TENANT_WRITE']) exp?: number; // Expiry (epoch seconds)}Requiring a privilege
Section titled “Requiring a privilege”Pass privilege to bridge.protect(...) to require that an API token carries a specific privilege:
// API tokens must carry USER_READ; user JWTs bypass this check.router.get('/users', bridge.protect({ privilege: 'USER_READ' }), handler);
// API tokens must carry USER_WRITE.router.post('/users', bridge.protect({ privilege: 'USER_WRITE' }), handler);Restricting the accepted auth type
Section titled “Restricting the accepted auth type”acceptAuth restricts which credential types an endpoint accepts:
// Only user JWTs accepted — an API token alone gets 401bridge.protect({ acceptAuth: 'jwt' })
// Only API tokens accepted — a user JWT alone gets 401bridge.protect({ acceptAuth: 'api_token' })
// Both accepted (default when omitted)bridge.protect({ acceptAuth: 'both' })The AuthType is 'jwt' | 'api_token' | 'both'.
When
acceptAuth: 'jwt'and both headers are present (some Bridge frontends always send both), the request is accepted and the JWT path populatesreq.bridgeUser; the API key is treated as informational only. The endpoint is rejected only if the API token is the only credential offered.
Dual-auth endpoints
Section titled “Dual-auth endpoints”Endpoints that accept both user JWTs and API tokens (the default). Branch on which context is present:
router.get('/users', bridge.protect({ privilege: 'USER_READ' }), (req, res) => { if (req.bridgeApiToken) { // Authenticated via API token console.log('API token tenant:', req.bridgeApiToken.tenantId); console.log('API token privileges:', req.bridgeApiToken.privileges); return res.json({ users: [] }); }
// Authenticated via user JWT const user = req.bridgeUser!; return res.json({ users: [], tenantId: user.tenantId });});API-token-only endpoints
Section titled “API-token-only endpoints”Endpoints for machine-to-machine traffic only:
router.post( '/integrations/sync', bridge.protect({ acceptAuth: 'api_token', privilege: 'TENANT_WRITE' }), (req, res) => { const { tenantId, privileges } = req.bridgeApiToken!; res.json({ synced: true, tenantId }); },);JWT-only endpoints
Section titled “JWT-only endpoints”Endpoints that should reject API tokens:
router.get('/account/profile', bridge.protect({ acceptAuth: 'jwt' }), (req, res) => { const user = req.bridgeUser!; res.json({ email: user.email, role: user.role });});Role-Based Access Control
Section titled “Role-Based Access Control”Roles are enforced per route via the role option on bridge.protect(...). Roles are not part of route rules.
import { Router } from 'express';const admin = Router();
// Applies to every route on this routeradmin.use(bridge.protect({ role: 'ADMIN' }));
admin.get('/dashboard', (req, res) => { res.json({ message: 'Admin dashboard', admin: req.bridgeUser!.email });});
// Tighten an individual route to OWNERadmin.get('/settings', bridge.protect({ role: 'OWNER' }), (req, res) => { res.json({ settings: 'sensitive data' });});
app.use('/admin', admin);The role check compares req.bridgeUser.role (from the verified user JWT) against the required role and returns 403 on mismatch. The role option applies only to the user-JWT path; API-token callers are unaffected by it.
A note on GraphQL. Express has no built-in GraphQL execution context. Protect a
/graphqlroute withbridge.protect(...)like any other route. Per-operationgraphqlOperationrules exist in the config type but are not wired in the Express plugin — do not rely on per-operation GraphQL guarding here.