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`});Validate an access token
Section titled “Validate an access token”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.
Exchange an authorization code
Section titled “Exchange an authorization code”// 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}Refresh
Section titled “Refresh”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.