Secure JWT Cookie & CSRF Token Auth
Auth-vir is a JavaScript/TypeScript library designed to simplify and secure authentication mechanisms in web applications. It provides robust features for handling JWT (JSON Web Token) based session cookies, CSRF (Cross-Site Request Forgery) protection through tokens, and secure password hashing. The library is actively maintained, with the current stable version being 5.2.0. Releases typically follow a semantic versioning approach, with minor and patch updates occurring every few weeks to months, and major versions introducing breaking changes less frequently, as observed from recent release history. Its key differentiators include a focus on security best practices out-of-the-box, offering both backend and frontend client implementations, and explicit support for ESM (ECMAScript Modules) and browser environments, ensuring modern application compatibility. It aims to abstract away common auth complexities while maintaining high security standards.
Common errors
-
TypeError: __dirname is not defined in ES module scope
cause Attempting to use CommonJS-specific globals (`__dirname`, `__filename`, `require`) in an ESM-only package context.fixRefactor your code to use ESM equivalents (e.g., `import.meta.url` for path resolution) or ensure your environment is configured for ESM. -
Error: Invalid JWT keys: Expected 3 parts for signing key, got X
cause The JWT signing and encryption keys were not correctly generated, stored, or parsed, leading to an invalid format being passed to `parseJwtKeys` or related functions.fixVerify that the output from `generateNewJwtKeys()` is stored verbatim and loaded without corruption. Ensure no whitespace or malformation occurs during storage/retrieval from environment variables or secret managers. -
Error: Login failure: CSRF token mismatch
cause The CSRF token sent by the client in the request headers (or expected location) does not match the token stored in the server's session or cookie, or is missing.fixEnsure your frontend is correctly extracting the CSRF token from the initial login response (e.g., from the `Set-Cookie` header if `AuthCookie.CsrfToken` is used) and sending it back in subsequent requests via the `csrf-token` header. Check CORS configuration. -
SyntaxError: Named export 'X' not found. The requested module 'auth-vir' does not provide an export named 'X'
cause Attempting to import a symbol that is not a named export, or has been renamed/removed in a different version.fixConsult the `auth-vir` documentation for the correct named exports. Check for potential default exports or specific versions where the symbol might have existed under a different name or export style.
Warnings
- breaking Auth-vir v5.0.0 introduced a major change where the CSRF token is now stored exclusively in a cookie, rather than as a JSON object within a cookie. This requires consumers to update their frontend and backend logic to correctly handle the CSRF token from the cookie header directly.
- breaking Auth-vir v4.0.0 reverted CSRF token storage from JSON back to raw token storage. This means the token itself is the raw value, not a JSON string encapsulating it. This was a reversal of an earlier change.
- gotcha JWT signing and encryption keys generated by `generateNewJwtKeys()` must be stored securely and never committed to version control or exposed publicly. Compromised keys lead to severe security vulnerabilities, allowing attackers to forge tokens and impersonate users.
- gotcha When integrating `auth-vir` with a client-side application, the `AuthHeaderName.CsrfToken` (default 'csrf-token') must be explicitly exposed via CORS headers (`Access-Control-Allow-Headers`) on your server. Additionally, `Access-Control-Allow-Origin` cannot be `*` and must be properly configured for your client's origin.
- gotcha The library explicitly states it is ESM-only. Attempting to `require()` any part of `auth-vir` in a CommonJS module will result in a runtime error.
Install
-
npm install auth-vir -
yarn add auth-vir -
pnpm add auth-vir
Imports
- hashPassword
const { hashPassword } = require('auth-vir');import { hashPassword } from 'auth-vir'; - BackendAuthClient
import BackendAuthClient from 'auth-vir';
import { BackendAuthClient } from 'auth-vir'; - AuthHeaderName
import { AuthHeaderName } from 'auth-vir'; - type CreateJwtParams
import { CreateJwtParams } from 'auth-vir';import type { CreateJwtParams } from 'auth-vir';
Quickstart
import {type ClientRequest, type ServerResponse} from 'node:http';
import {
AuthCookie,
doesPasswordMatchHash,
extractUserIdFromRequestHeaders,
generateNewJwtKeys,
generateSuccessfulLoginHeaders,
hashPassword,
parseJwtKeys,
type CookieParams,
type CreateJwtParams,
type CsrfHeaderNameOption,
} from 'auth-vir';
type MyUserId = string;
// Generate JWT keys (do this once and store securely, e.g., in environment variables)
async function getJwtKeys() {
const generatedKeys = await generateNewJwtKeys();
// In a real app, store these securely (e.g., environment variables)
// For demo purposes, we'll return them directly.
console.log('Generated JWT Keys (store securely!):', generatedKeys);
return parseJwtKeys(generatedKeys);
}
const jwtKeysPromise = getJwtKeys();
async function handleLogin(req: ClientRequest, res: ServerResponse, passwordInput: string) {
const jwtKeys = await jwtKeysPromise;
const userId: MyUserId = 'user-123'; // Replace with actual user ID after DB lookup
const storedPasswordHash = await hashPassword('password123'); // From your user database
if (await doesPasswordMatchHash({ hash: storedPasswordHash, password: passwordInput })) {
const authHeaders = await generateSuccessfulLoginHeaders({
jwtKeys,
userId,
maxAgeMs: 1000 * 60 * 60 * 24 * 7, // 7 days
csrfHeaderName: 'csrf-token',
});
for (const [headerName, headerValue] of Object.entries(authHeaders)) {
res.setHeader(headerName, headerValue);
}
res.statusCode = 200;
res.end('Logged in successfully!');
} else {
res.statusCode = 401;
res.end('Invalid credentials.');
}
}
async function handleAuthenticatedRequest(req: ClientRequest, res: ServerResponse) {
const jwtKeys = await jwtKeysPromise;
try {
const userId = await extractUserIdFromRequestHeaders({
jwtKeys,
requestHeaders: req.headers as Record<string, string | string[] | undefined>,
csrfHeaderName: 'csrf-token',
});
res.statusCode = 200;
res.end(`Access granted for user ID: ${userId}`);
} catch (error) {
console.error('Authentication error:', error);
res.statusCode = 401;
res.end('Unauthorized or invalid session.');
}
}
// Example usage (simplified HTTP server)
import { createServer } from 'node:http';
const server = createServer(async (req, res) => {
if (req.url === '/login' && req.method === 'POST') {
// In a real app, parse body for password
await handleLogin(req, res, 'password123');
} else if (req.url === '/protected' && req.method === 'GET') {
await handleAuthenticatedRequest(req, res);
} else {
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});