Skip to content

Latest commit

 

History

History

auth

Folders and files

README.md

@cleverbrush/auth

CI License: BSD-3-Clause

Unit coverage

Transport-agnostic authentication and authorization for TypeScript. Ships with JWT and cookie schemes, a fluent policy builder, and a typed Principal value object. Designed for @cleverbrush/server but usable in any Node.js context.

Features

  • Transport-agnostic — authentication schemes receive a plain AuthenticationContext (headers + cookies + items), not a raw HTTP request.
  • Principal<T> — immutable, typed value object exposing .hasRole(), .hasClaim(), .isAuthenticated, and .claims.
  • JWT scheme — jwtScheme() supports HS256/HS384/HS512 and RS256/RS384/RS512; configurable issuer, audience, clock tolerance, and role claim.
  • Cookie scheme — cookieScheme() with a user-supplied async validate() function (session lookup, signature check, etc.).
  • Authorization — PolicyBuilder with .requireRole() / .require(predicate) fluent API; AuthorizationService.authorize() accepts requirements or a named policy.
  • requireRole() helper — convenience factory for single-call role requirements.
  • Cookie utilities — parseCookies() and serializeCookie() for low-level cookie handling.
  • Zero runtime dependencies — uses only Node.js built-in crypto.

Installation

npm install @cleverbrush/auth

Quick Start

import { jwtScheme, Principal } from '@cleverbrush/auth';

type UserClaims = { sub: string; role: string };

const jwt = jwtScheme<UserClaims>({
    secret: process.env.JWT_SECRET!,
    mapClaims: claims => ({
        sub: claims.sub as string,
        role: claims.role as string
    })
});

// Authenticate a request context (built from HTTP headers)
const result = await jwt.authenticate({
    headers: { authorization: 'Bearer <token>' },
    cookies: {},
    items: new Map()
});

if (result.succeeded) {
    const principal: Principal<UserClaims> = result.principal;
    principal.isAuthenticated; // true
    principal.hasRole('admin'); // boolean
    principal.value.sub;       // string
}

JWT Authentication

import { jwtScheme, signJwt } from '@cleverbrush/auth';

// Sign a token (for testing or token issuance)
const token = signJwt({
    sub: 'user-1', role: 'admin',
    iss: 'https://my-app.com', aud: 'my-api',
    exp: Math.floor(Date.now() / 1000) + 3600
}, 'my-secret');

// Verify and authenticate
const scheme = jwtScheme({
    secret: 'my-secret',
    algorithms: ['HS256'],          // default
    issuer: 'https://my-app.com',   // optional iss check
    audience: 'my-api',             // optional aud check
    clockTolerance: 5,              // seconds
    roleClaim: 'role',              // default
    mapClaims: c => ({ sub: c.sub as string, role: c.role as string })
});

Supported algorithms: HS256, HS384, HS512, RS256, RS384, RS512.

Configure a single key family per scheme: HMAC secrets and RSA keys must never share an algorithm allowlist. RSA requires an RSA PEM key; HMAC rejects empty secrets and PEM key material. Use a cryptographically random secret from secure configuration, not the illustrative value above. Unsupported critical JOSE extensions, malformed claim types and tokens at their expiration instant are rejected. Clock tolerance must be finite and non-negative.

See security guidance and the v4.x-to-v5 migration guide.

Cookie Authentication

import { cookieScheme } from '@cleverbrush/auth';

const cookie = cookieScheme<{ userId: string; role: string }>({
    cookieName: 'session',
    validate: async (cookieValue) => {
        // Look up session from DB, verify signature, etc.
        const session = await sessionStore.get(cookieValue);
        if (!session) return null;
        return { userId: session.userId, role: session.role };
    }
});

Authorization

Policy Builder

import { PolicyBuilder, requireRole } from '@cleverbrush/auth';

const adminPolicy = new PolicyBuilder()
    .requireRole('admin')
    .require(principal => principal.hasClaim('verified', 'true'))
    .build('admin-only');

Authorization Service

import { AuthorizationService } from '@cleverbrush/auth';

const policies = new Map([['admin-only', adminPolicy]]);
const authz = new AuthorizationService(policies);

// Check by requirements array
const result = await authz.authorize(principal, [requireRole('admin')]);

// Check by named policy
const result2 = await authz.authorize(principal, 'admin-only');

if (!result.allowed) {
    console.log(result.reason); // 'Not authenticated' | 'Forbidden'
}

Cookie Utilities

import { parseCookies, serializeCookie } from '@cleverbrush/auth';

// Parse Cookie header
const cookies = parseCookies('session=abc123; theme=dark');
// { session: 'abc123', theme: 'dark' }

// Build Set-Cookie header value
const header = serializeCookie('session', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    maxAge: 3600
});

Integration with @cleverbrush/server

import { ServerBuilder } from '@cleverbrush/server';
import { jwtScheme } from '@cleverbrush/auth';

const server = new ServerBuilder();

server
    .useAuthentication({
        defaultScheme: 'jwt',
        schemes: [jwtScheme({ secret: process.env.JWT_SECRET!, mapClaims: c => c })]
    })
    .useAuthorization();

await server.listen(3000);

License

BSD-3-Clause — see LICENSE.