From monocloud
Use when integrating MonoCloud access-token validation into an Express API — installing or configuring `@monocloud/backend-node/express`, wiring the `protectApi()` middleware factory, validating JWT or opaque (introspection) bearer tokens, enforcing scopes/groups, attaching `claims` to `req` via `AuthenticatedExpressRequest`, or troubleshooting `MONOCLOUD_BACKEND_*` env vars / audience / JWKS / mTLS certificate binding.
How this skill is triggered — by the user, by Claude, or both
Slash command
/monocloud:monocloud-auth-expressThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Backend SDK for validating MonoCloud-issued access tokens in Express APIs. Handles JWT signature verification (via JWKS) and opaque-token introspection automatically based on token format.
@monocloud/backend-node/express)Backend SDK for validating MonoCloud-issued access tokens in Express APIs. Handles JWT signature verification (via JWKS) and opaque-token introspection automatically based on token format.
Use: @monocloud/backend-node with the /express subpath. This is a single npm package that also ships /fastify.
This is not the same SDK as @monocloud/auth-nextjs (frontend, user sessions) or @monocloud/auth-node-core (server-side auth flows). This package is purely for API protection — validating tokens issued elsewhere, not signing users in.
If you see any of these symbols, they belong to a different package or an older SDK — do not use them here:
expressJwt, expressOAuth2BearerToken, passport-* (other libraries)requireAuth, requireScopes as standalone functions (this SDK uses one chained call)@monocloud/backend-node root for Express middleware (use the /express subpath)npm install @monocloud/backend-node
Required:
| Variable | Purpose |
|---|---|
MONOCLOUD_BACKEND_TENANT_DOMAIN | MonoCloud tenant URL, e.g. https://acme.us.monocloud.app |
MONOCLOUD_BACKEND_AUDIENCE | Expected audience claim, e.g. https://api.example.com |
Required only when validating opaque tokens (or when MONOCLOUD_BACKEND_INTROSPECT_JWT_TOKENS=true):
| Variable | Purpose |
|---|---|
MONOCLOUD_BACKEND_CLIENT_ID | Client used to call the introspection endpoint |
MONOCLOUD_BACKEND_CLIENT_SECRET | Client secret |
MONOCLOUD_BACKEND_CLIENT_AUTH_METHOD | One of client_secret_basic, client_secret_post (default), client_secret_jwt, private_key_jwt, tls_client_auth, self_signed_tls_client_auth |
Optional tuning:
| Variable | Default | Purpose |
|---|---|---|
MONOCLOUD_BACKEND_INTROSPECT_JWT_TOKENS | false | If true, skip local JWT validation and always introspect |
MONOCLOUD_BACKEND_CLOCK_SKEW | 0 | Allowed clock drift (seconds) |
MONOCLOUD_BACKEND_CLOCK_TOLERANCE | 300 | Extra tolerance on time-based claims |
MONOCLOUD_BACKEND_GROUPS_CLAIM | — | Claim name that carries group memberships |
MONOCLOUD_BACKEND_GROUPS_MATCH_ALL | false | If true, all listed groups must match |
MONOCLOUD_BACKEND_JWKS_CACHE_DURATION | — | Seconds to cache the JWKS |
MONOCLOUD_BACKEND_METADATA_CACHE_DURATION | — | Seconds to cache the OIDC discovery doc |
import express from 'express';
import {
protectApi,
type AuthenticatedExpressRequest,
} from '@monocloud/backend-node/express';
const app = express();
// Reads MONOCLOUD_BACKEND_* env vars. Build it once and reuse.
const protect = protectApi();
// Bare protection — any valid token works
app.get('/api/me', protect(), (req, res) => {
const { claims } = req as AuthenticatedExpressRequest;
res.json({ sub: claims.sub });
});
// Scope-gated
app.post('/api/posts', protect({ scopes: ['posts:write'] }), (req, res) => {
res.status(201).end();
});
// Group-gated (uses MONOCLOUD_BACKEND_GROUPS_CLAIM or override per call)
app.delete('/api/posts/:id', protect({ groups: ['admin'] }), (req, res) => {
res.status(204).end();
});
app.listen(3000);
Two-call pattern: protectApi() builds a factory once (parses env, loads JWKS lazily); calling the factory with options returns the actual middleware. Build the factory outside the request hook, call the factory inline per route.
protect(options) acceptsoptions (all optional):
interface ProtectOptions {
scopes?: string[]; // require all listed scopes
groups?: string[]; // require group membership (any-of by default)
validateCertificateBinding?: boolean; // mTLS-bound token validation
}
scope claim).MONOCLOUD_BACKEND_GROUPS_MATCH_ALL=true (or per-client groupOptions.matchAll). The claim name comes from MONOCLOUD_BACKEND_GROUPS_CLAIM.cnf.x5t#S256 confirmation claim against the client's TLS cert. Requires a certificateResolver to be wired (see "Advanced" below).new MonoCloudBackendNodeClient(options) accepts the backend-node option shape. Use this when you need a shared client, non-env configuration, or a custom token-claims cache:
interface MonoCloudBackendNodeClientOptions {
tenantDomain: string;
audience: string;
clientId?: string;
clientSecret?: string;
clientAuthMethod?: ClientAuthMethod;
groupOptions?: { groupsClaim?: string; matchAll?: boolean };
clockSkew?: number;
clockTolerance?: number;
jwksCacheDuration?: number;
metadataCacheDuration?: number;
introspectJwtTokens?: boolean;
cache?: ICache;
fetcher?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
}
cache?: ICache is constructor-only; pass it in code to cache validated access-token claims by raw token until the token expires.
Authorization: Bearer <token> header (and no custom tokenResolver): 401 { "message": "unauthorized" }401 { "message": "unauthorized" }403 { "message": "forbidden" }The middleware does not call next(err) — it sends the response directly. If you want custom error responses, wrap the middleware or implement your own using MonoCloudBackendNodeClient.validateAccessToken().
After protect() runs successfully, req.claims is populated with AccessTokenClaims. Cast the request:
import type { AuthenticatedExpressRequest } from '@monocloud/backend-node/express';
app.get('/api/me', protect(), (req, res) => {
const { claims } = req as AuthenticatedExpressRequest;
// claims.sub, claims.scope, claims.exp, claims.iat, etc.
res.json(claims);
});
Alternatively, declare a global Express namespace augmentation if you don't want to cast per-route:
import type { AccessTokenClaims } from '@monocloud/backend-node';
declare global {
namespace Express {
interface Request { claims?: AccessTokenClaims; }
}
}
import {
protectApi,
MonoCloudBackendNodeClient,
type ICache,
} from '@monocloud/backend-node/express';
// Build a client explicitly when you need a cache or non-env config
const client = new MonoCloudBackendNodeClient({
tenantDomain: 'https://acme.us.monocloud.app',
audience: 'https://api.example.com',
cache: redisCache, // your ICache implementation — caches by token until exp
introspectJwtTokens: false,
});
const protect = protectApi(client, {
// Pull token from somewhere other than Authorization: Bearer
tokenResolver: async (req) => req.cookies.access_token,
// Provide the client cert for mTLS-bound tokens (use with validateCertificateBinding)
certificateResolver: async (req) => req.headers['x-client-cert'] as string,
});
app.get('/api/secure', protect({ validateCertificateBinding: true }), (req, res) => {
res.json((req as AuthenticatedExpressRequest).claims);
});
ICache interface (implement for Redis, in-memory, etc.):
interface ICache {
get(token: string): Promise<AccessTokenClaims | null | undefined>;
set(token: string, claims: AccessTokenClaims, expiresAt: number): Promise<void>;
delete(token: string): Promise<void>;
}
Caching is keyed on the raw token string and respects claims.exp minus the configured clock skew/tolerance — short-lived tokens self-expire.
xxx.yyy.zzz) and introspectJwtTokens is false (default): the SDK validates the JWT locally using JWKS fetched from the tenant. No network call per request after the JWKS cache warms.introspectJwtTokens=true): the SDK calls the OIDC introspection endpoint. Requires clientId + clientSecret (or another clientAuthMethod).This means: JWT tokens don't require client credentials. Opaque tokens do. If you see MonoCloudValidationError: clientId is required when receiving opaque tokens, set the introspection env vars.
protectApi must be imported from @monocloud/backend-node/express, not the root. The root only exports the framework-agnostic MonoCloudBackendNodeClient class.MONOCLOUD_BACKEND_AUDIENCE must exactly match the aud claim minted by your authorization server for this API. A trailing slash or http vs https difference will fail validation.protectApi() is meant to be called once at startup. Calling it inside a route handler creates a new client per request and re-fetches the JWKS each time.protect() doesn't read the body, but if your route does, register body parsers normally. Order doesn't matter for the auth middleware.next() after the auth middleware sent a 401/403. The middleware sends the response synchronously on failure and doesn't call next. If you wrap it, check res.headersSent.groups is set but the token doesn't carry the claim named by MONOCLOUD_BACKEND_GROUPS_CLAIM (or the default), the request is forbidden. Configure the claim in the MonoCloud dashboard or set groupsClaim explicitly.MONOCLOUD_BACKEND_INTROSPECT_JWT_TOKENS=true, every JWT request also calls introspection. Usually unwanted; only enable if you specifically need server-side revocation checks.npm install @monocloud/backend-node.MONOCLOUD_BACKEND_TENANT_DOMAIN and MONOCLOUD_BACKEND_AUDIENCE to your env. If you'll accept opaque tokens, also add MONOCLOUD_BACKEND_CLIENT_ID and MONOCLOUD_BACKEND_CLIENT_SECRET.MONOCLOUD_BACKEND_AUDIENCE.const protect = protectApi();app.get('/path', protect({ scopes: [...] }), handler);req to AuthenticatedExpressRequest inside handlers to read claims.Re-exported from @monocloud/auth-core via @monocloud/backend-node:
AccessTokenClaims, JwtClaims, Jwk, Jwks, IssuerMetadata, ClientAuthMethodMonoCloudAuthBaseError, MonoCloudValidationError, MonoCloudOPError, MonoCloudHttpError, MonoCloudTokenErrorA failed scope/group check throws MonoCloudTokenError with the message 'Token is missing required scopes' or 'Token is missing required groups' — these get converted to 403 by the middleware. Other validation failures throw MonoCloudTokenError or MonoCloudValidationError and become 401.
references/api-surface.md — every export from @monocloud/backend-node/express, full type signatures, env-var → option mapping, defaults.references/troubleshooting.md — symptom → cause → fix index for the most common failure modes (audience mismatch, opaque-token introspection config, scope/group enforcement quirks, mTLS binding errors, JWKS thrash).Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub monocloud/agent-skills --plugin monocloud