Protect your API
Verify access tokens offline in Hono, Express or any other framework, read the user, add policies and permissions, accept personal access tokens.
Register a resource app with an audience (e.g. acme-api) and give the web and CLI apps the same audience, so the tokens they get carry it in aud. Verification is offline against cached keys: no call to anchring per request.
Hono
import { Hono } from 'hono';
import { createAuth } from '@anchring/auth/server';
import '@anchring/auth/hono'; // types c.var.user / c.var.resource
const auth = createAuth({ project: 'acme', env: process.env.AUTH_ENV ?? 'test', audience: 'acme-api' });
const app = new Hono();
app.use('/api/*', auth.middleware()); // 401 without a valid token
app.get('/api/reports', (c) => c.json(listReports(c.var.user.sub)));The @anchring/auth/hono import types c.var.user; if your app already types its own user, use new Hono<AuthEnv>() instead. c.var.user has sub, type (session, apiKey or impersonation), scopes, amr, env and all claims.
Express
import express from 'express';
import { createAuth } from '@anchring/auth/server';
import { authMiddleware } from '@anchring/auth/express';
const auth = createAuth({ project: 'acme', env: 'test', audience: 'acme-api' });
const app = express();
app.use(express.json());
app.use('/api', authMiddleware(auth));
app.get('/api/me', (req, res) => res.json(req.user)); // also res.locals.userThe caller IP defaults to req.ip, so set Express trust proxy when you use allowIps behind a proxy.
Anything else
const result = await auth.authenticate(request.headers.get('authorization'), { audience: 'acme-api' });
if (!result.ok) return new Response(JSON.stringify(result.body), { status: result.status, headers: result.headers });
const user = result.user; // AuthUser: sub, type, scopes, amr, env, claims …
// or, for a bare token:
const verified = await auth.verify(token, { audience: 'acme-api' }); // throws AuthErrorauthenticate never throws for bad input: it returns the response to send, with the right WWW-Authenticate header.
Policies
auth.policy({ maxSessionAge: '12h', requireMfaWithin: '1h', allowIps: ['10.0.0.0/8'] });
app.use('/api/billing/*', auth.middleware({ policy: { requireMfaWithin: '15m' } })); // stricter per routeA session too old or without a recent second factor gets the RFC 9470 step-up challenge (401 insufficient_user_authentication); the browser app answers it with signIn({ prompt: 'login', params: { acr_values: 'mfa' } }) and retries.
Permissions
Teams and roles stay in your own database, keyed by sub. One typed definition covers roles, resource rules and token ceilings, evaluated locally:
const perms = auth.permissions({
roles: {
viewer: ['report:read'],
member: ['report:read', 'report:write'],
admin: ['report:*', 'members:manage'],
},
resources: { report: { 'report:write': ({ user, resource }) => resource.ownerId === user.sub } },
tokens: { apiKey: { max: ['report:read'] } }, // CI keys never exceed this
context: async (user) => ({ role: await roleOf(user.sub) }),
});
app.put('/api/reports/:id', perms.guard('report:write', (c) => loadReport(c.req.param('id'))), handler);Personal access tokens
Users create personal access tokens on their account page for scripts and CI. Accept them in the middleware with the resource app's audience; the middleware exchanges each one for a short-lived token and caches it:
const auth = createAuth({ project: 'acme', env: 'prod', audience: 'acme-api', pat: { audience: 'acme-api' } });
app.use('/api/*', auth.middleware()); // accepts JWTs and kh_pat_… alikeEvery option, error and token type: the SDK README.