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 | Uint8Array—HMAC 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
issuerstring—Expected JWT issuer
audiencestring—Expected 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 | Uint8Array—Signing secret
userIdstring—Subject (sub) claim
rolesstring[]—Roles to embed in the access token
permissionsstring[]—Permissions to embed in the access token
issuerstring—JWT issuer
audiencestring—JWT 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 | Uint8Array—Signing secret
storeTokenStore—Token store for rotation and revocation
issuerstring—Expected JWT issuer
audiencestring—Expected 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
providerOAuth2Provider—Provider config (base URLs + client credentials)
redirectUristring—Where to redirect after authorization
callbackUrlstring—The 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
providerOAuth2Provider—Provider config
redirectUristring—Override redirect URI for token exchange
callback(profile) => Promise<{ userId, roles? } | null>—Maps the OAuth profile to your user. Return null to reject.
onError(error) => Response—Custom 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
rolesRbacMap—Role-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
secretstring—HMAC 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.