Skip to content

Server

The @arrow-labs/auth-sdk/server subpath is for consuming backends — services that protect their own APIs with ArrowLabs access tokens and/or complete the OAuth flow server-side. It has one runtime dependency, jose, and runs in Node and edge runtimes.

Create one client per issuer at startup — the JWKS is fetched and cached on the instance, so the validation hot path never refetches:

import { createAuthServerClient } from '@arrow-labs/auth-sdk/server';
const auth = createAuthServerClient({
baseUrl: 'https://api.arrowlabs.co.uk',
audience: 'your-client-id', // expected `aud` on incoming tokens
// issuer defaults to baseUrl; jwksUri defaults to `${baseUrl}/.well-known/jwks.json`
});

Tokens are verified offline against the JWKS — the auth API is never on the hot path. verifyAccessToken checks the RS256 signature plus iss, aud, and exp, and returns a discriminated result:

app.use(async (req, res, next) => {
const token = req.headers.authorization?.replace(/^Bearer /, '');
const result = await auth.verifyAccessToken(token ?? '');
if (result.kind === 'invalid') {
// result.reason: 'expired' | 'signature' | 'claims' | 'malformed'
return res.status(401).json({ error: 'invalid_token', reason: result.reason });
}
req.user = result.claims; // { sub, org_id, roles, app_access, email, ... }
next();
});

Validation is purely cryptographic + claim-based. Revocation is handled by the 15-minute access-token lifetime and refresh-token rotation, not by this check.

// Confidential client (has a client_secret):
const tokens = await auth.exchangeCode({
code,
redirectUri: 'https://app.example.com/callback',
clientId: 'your-client-id',
clientSecret: process.env.CLIENT_SECRET,
});
// Public client (PKCE) — pass codeVerifier instead of clientSecret:
const tokens = await auth.exchangeCode({ code, redirectUri, clientId, codeVerifier });
if (tokens.kind === 'success') {
// tokens.accessToken, tokens.refreshToken, tokens.idToken, tokens.expiresIn, tokens.tokenType
} else {
// tokens.error (e.g. 'invalid_grant', 'invalid_client', 'network_error'), tokens.description
}
const refreshed = await auth.refreshTokens({
refreshToken: tokens.refreshToken!,
clientId: 'your-client-id',
clientSecret: process.env.CLIENT_SECRET, // omit for public clients
});

Both token calls return the same TokenResult union, so there’s a single error path — including transport failure, surfaced as error: 'network_error'. No try/catch needed for expected outcomes.