Skip to content

@nodalite/auth

Authentication and authorization for Nodalite: JWT with refresh tokens, OAuth2 (PKCE), role-based access control, cookie-based sessions, password hashing, and CSRF protection.

npm install @nodalite/auth

Depends on @nodalite/core and jose (WebCrypto-based JWT, works on all runtimes). Optional peer: ioredis for Redis-backed token/session stores.

jwtAuth()

JWT verification middleware. Validates Authorization: Bearer <token> on every matching request and attaches the decoded payload to the context.

ts
import { jwtAuth } from '@nodalite/auth';

app.use('/api/*', jwtAuth({ secret: process.env.JWT_SECRET! }));
app.get('/api/me', (c) => c.json(c.get('user')));

Options

OptionTypeDefaultDescription
secretstring | Uint8ArrayHMAC secret (HS256) or CryptoKey for asymmetric algorithms
contextKeystring"user"Where to store the verified payload in context
getToken(c) => string | nullAuthorization: BearerCustom token extraction function
issuerstringExpected JWT issuer
audiencestringExpected JWT audience
algorithmstring"HS256"Signing algorithm
accessTokenExpiresInstring"15m"Access token expiry

issueTokenPair()

Issue an access + refresh token pair.

ts
import { issueTokenPair } from '@nodalite/auth';

const tokens = await issueTokenPair({
  secret: process.env.JWT_SECRET!,
  userId: user.id,
  roles: ['user'],
  permissions: ['read'],
});
// tokens.accessToken, tokens.refreshToken

Options

OptionTypeDefaultDescription
secretstring | Uint8ArraySigning secret
userIdstringSubject (sub) claim
rolesstring[]Roles to embed in the access token
permissionsstring[]Permissions to embed in the access token
issuerstringJWT issuer
audiencestringJWT audience
algorithmstring"HS256"Signing algorithm
accessTokenExpiresInstring"15m"Access token expiry
refreshTokenExpiresInstring"7d"Refresh token expiry

Returns

FieldTypeDescription
accessTokenstringSigned access token
refreshTokenstringSigned refresh token
accessTokenPayloadAccessTokenPayloadDecoded access token payload
refreshTokenPayloadRefreshTokenPayloadDecoded refresh token payload

tokenRefreshHandler()

Handler that validates a refresh token, checks for revocation and replay attacks, issues a new token pair, and stores the new refresh token.

ts
import { tokenRefreshHandler, MemoryTokenStore } from '@nodalite/auth';

const store = new MemoryTokenStore();
app.post('/auth/refresh', tokenRefreshHandler({
  secret: process.env.JWT_SECRET!,
  store,
}));

Options

OptionTypeDefaultDescription
secretstring | Uint8ArraySigning secret
storeTokenStoreToken store for rotation and revocation
issuerstringExpected JWT issuer
audiencestringExpected JWT audience
algorithmstring"HS256"Signing algorithm
accessTokenExpiresInstring"15m"New access token expiry
refreshTokenExpiresInstring"7d"New refresh token expiry

The handler expects a JSON body with { refreshToken: string } and returns { accessToken, refreshToken }.

Security

On replay detection (revoked refresh token reused), the entire token family is revoked. This prevents stolen refresh tokens from being used after the legitimate user has refreshed.

revokeToken()

Revoke a specific refresh token by its JTI.

ts
import { revokeToken } from '@nodalite/auth';

await revokeToken(tokenId, store);

oauth2authorize()

Start an OAuth2 authorization code flow with PKCE. Redirects the user to the provider's authorization endpoint.

ts
import { oauth2authorize, providers } from '@nodalite/auth';

app.get('/auth/login', oauth2authorize({
  provider: { ...providers.github, clientId: '...', clientSecret: '...' },
  redirectUri: 'https://myapp.com',
  callbackUrl: '/auth/callback',
}));

Built-in providers

ProviderScopes
providers.googleopenid, email, profile
providers.githubuser:email
providers.discordidentify, email

Options

OptionTypeDefaultDescription
providerOAuth2ProviderProvider config (base URLs + client credentials)
redirectUristringWhere to redirect after authorization
callbackUrlstringThe route path that handles the callback
scopesstring[]provider.scopesOverride default scopes
extraParamsRecord<string, string>Additional query parameters for the authorization URL

oauth2Callback()

Handle the OAuth2 callback, exchange the code for tokens, fetch the user profile, and call your callback to find/create the user.

ts
import { oauth2Callback, providers } from '@nodalite/auth';

app.get('/auth/callback', oauth2Callback({
  provider: { ...providers.github, clientId: '...', clientSecret: '...' },
  callback: async (profile) => {
    let user = await db.findUserByOAuth(profile.provider, profile.id);
    if (!user) user = await db.createUser({ email: profile.email, name: profile.name });
    return { userId: user.id, roles: ['user'] };
  },
}));

Options

OptionTypeDefaultDescription
providerOAuth2ProviderProvider config
redirectUristringOverride redirect URI for token exchange
callback(profile) => Promise<{ userId, roles? } | null>Maps the OAuth profile to your user. Return null to reject.
onError(error) => ResponseCustom error handler

rbac()

Middleware that builds an RBAC context from the verified JWT payload. Must be used after jwtAuth.

ts
import { jwtAuth, rbac, requireRole, requirePermission } from '@nodalite/auth';

app.use('/api/*', jwtAuth({ secret }));
app.use('/api/*', rbac({
  roles: { admin: ['read', 'write', 'delete'], user: ['read'] },
}));

app.get('/api/admin', handler, [requireRole('admin')]);
app.delete('/api/doc', handler, [requirePermission('delete')]);

Options

OptionTypeDefaultDescription
rolesRbacMapRole-to-permissions mapping
userContextKeystring"user"Context key where the JWT payload is stored
rbacContextKeystring"rbac"Context key where the RBAC context is stored
extractRoles(payload) => string[]payload.rolesCustom role extraction from JWT
extractPermissions(payload) => string[]payload.permissionsCustom permission extraction from JWT

RbacContext

Available on c.get('rbac') after the rbac() middleware:

MethodDescription
hasRole(role)Check if user has a specific role
hasPermission(perm)Check if user has a specific permission (resolved from roles + explicit)
hasAnyRole(...roles)Check if user has at least one of the specified roles
hasAllPermissions(...perms)Check if user has all of the specified permissions

requireRole()

Route-level middleware that requires the user to have at least one of the specified roles. Must be used as middleware (not as a terminal handler).

ts
app.get('/admin', handler, [requireRole('admin')]);
app.get('/moderator-or-admin', handler, [requireRole('moderator', 'admin')]);

requirePermission()

Route-level middleware that requires the user to have at least one of the specified permissions. Must be used as middleware (not as a terminal handler).

ts
app.delete('/doc', handler, [requirePermission('delete')]);
app.put('/doc', handler, [requirePermission('write', 'admin')]);

sessions()

Cookie-based session middleware with HMAC-signed session IDs.

ts
import { sessions } from '@nodalite/auth';

app.use('*', sessions({ secret: process.env.SESSION_SECRET! }));
app.get('/login', async (c) => {
  const session = c.get('session');
  session.userId = '123';
  return c.json({ loggedIn: true });
});

Options

OptionTypeDefaultDescription
secretstringHMAC secret for signing session IDs
cookieNamestring"sid"Cookie name
maxAgenumber86400Session max age in seconds (24 hours)
storeSessionStoreMemorySessionStoreSession store backend
contextKeystring"session"Context key for session data
cookie.httpOnlybooleantrueHttpOnly flag
cookie.securebooleantrueSecure flag
cookie.sameSite"Strict" | "Lax" | "None""Lax"SameSite attribute
cookie.pathstring"/"Cookie path

hashPassword()

Hash a password using PBKDF2 with SHA-256 (600k iterations, random salt). Returns a portable hash string.

ts
import { hashPassword } from '@nodalite/auth';

const hash = await hashPassword('user-password');
// "pbkdf2:sha256:600000:<base64-salt>:<base64-hash>"

Options

OptionTypeDefaultDescription
iterationsnumber600000PBKDF2 iteration count

verifyPassword()

Verify a password against a hash string produced by hashPassword. Uses constant-time comparison to prevent timing attacks.

ts
import { verifyPassword } from '@nodalite/auth';

const valid = await verifyPassword('user-password', hash);

csrf()

Double-submit cookie CSRF protection. Server sets a random token as a cookie; client echoes it in a header or body field. No server-side sessions required.

ts
import { csrf } from '@nodalite/auth';

app.use('*', csrf());

Options

OptionTypeDefaultDescription
cookieNamestring"XSRF-TOKEN"Cookie name for the CSRF token
headerNamestring"X-XSRF-Token"Header name the client must send
bodyFieldstring"_csrf"Request body field (fallback)
safeMethodsstring[]["GET", "HEAD", "OPTIONS", "QUERY"]Methods that skip validation
generateToken() => stringcrypto.randomUUID()Custom token generator
cookie.httpOnlybooleanfalseHttpOnly flag (must be false for client access)
cookie.securebooleantrueSecure flag
cookie.sameSite"Strict" | "Lax" | "None""Lax"SameSite attribute
cookie.pathstring"/"Cookie path
cookie.maxAgenumber3600Token max age in seconds

Stores

TokenStore

Interface for refresh token storage. Implement against your database for production:

ts
interface TokenStore {
  get(tokenId: string): Promise<TokenEntry | null>;
  set(tokenId: string, entry: TokenEntry, ttlMs: number): Promise<void>;
  delete(tokenId: string): Promise<void>;
  revokeFamily(family: string): Promise<void>;
  cleanup?(): Promise<void>;
}

SessionStore

Interface for session storage:

ts
interface SessionStore {
  get(id: string): Promise<Record<string, unknown> | null>;
  set(id: string, data: Record<string, unknown>, maxAge: number): Promise<void>;
  destroy(id: string): Promise<void>;
}

MemoryTokenStore / MemorySessionStore

In-memory implementations for development and testing. Include automatic cleanup timers and destroy() methods.

ts
import { MemoryTokenStore, MemorySessionStore } from '@nodalite/auth';

WARNING

Memory stores are single-process only. Each instance has its own isolated memory. Use Redis, DynamoDB, or Postgres for production.

Redis store

Redis-backed TokenStore via ioredis (optional peer dependency):

bash
npm install ioredis
ts
import { RedisTokenStore } from '@nodalite/auth/stores/redis';

Released under the MIT License.