@ttoss/auth-core
Framework-agnostic authentication primitives for Node.js, with zero
dependencies beyond node:crypto (Amazon Cognito verification excepted).
Installation
pnpm add @ttoss/auth-core
Password hashing
PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP recommendation) and
constant-time comparison. Hashes are self-describing
(pbkdf2-sha256$<iterations>$<salt>$<hash>), so iterations can be raised
later without invalidating stored hashes. The legacy salt:hash format is
still verified for backwards compatibility.
import { comparePassword, hashPassword, needsRehash } from '@ttoss/auth-core';
const stored = await hashPassword('my-password');
const isMatch = await comparePassword('my-password', stored);
// On successful login, upgrade weak/legacy hashes:
if (isMatch && needsRehash(stored)) {
await saveHash(await hashPassword('my-password'));
}
JWT (HS256)
Sign and verify JWTs for self-hosted authentication, where the application
owns the signing secret. verifyJwt returns null for malformed, badly
signed, or expired tokens.
import { signJwt, verifyJwt } from '@ttoss/auth-core';
const token = signJwt({
payload: { sub: 'user_123', email: 'user@example.com' },
secret: process.env.JWT_SECRET,
expiresInSeconds: 60 * 60 * 24 * 7, // 7 days
});
const payload = verifyJwt({ token, secret: process.env.JWT_SECRET });
For Amazon Cognito tokens, use @ttoss/auth-core/amazon-cognito, which
re-exports aws-jwt-verify.
One-time tokens
Building block for magic links, email verification, and password reset.
Store only tokenHash and expires; send token to the user and destroy
the record after a successful verification.
import { generateOneTimeToken, verifyOneTimeToken } from '@ttoss/auth-core';
const { token, tokenHash, expires } = generateOneTimeToken({
expiresInSeconds: 60 * 60, // 1 hour, e.g. for password reset
});
// later, when the user clicks the link:
const isValid = verifyOneTimeToken({ token: received, tokenHash, expires });
API tokens
Personal access tokens in the form <prefix>_<hex>, recognizable in logs and
secret scanners. Show the plain token once; persist only the SHA-256 hash and
a short display prefix.
import { generateApiToken, verifyApiToken } from '@ttoss/auth-core';
const { token, tokenHash, displayPrefix } = generateApiToken({
prefix: 'myapp',
});
const isValid = verifyApiToken({
token: received,
tokenHash,
expiresAt: storedExpiresAt, // optional
});
Encryption at rest
AES-256-GCM helpers for storing sensitive values (e.g., third-party API keys) in a database. The ciphertext is a single base64 string containing the IV, auth tag, and payload. Decryption throws on a wrong key or tampered ciphertext.
import {
decryptValue,
encryptValue,
generateEncryptionKey,
} from '@ttoss/auth-core';
// Generate once and store in a secret manager:
const key = generateEncryptionKey(); // 64-char hex (32 bytes)
const ciphertext = encryptValue({ plaintext: 'third-party-api-key', key });
const plaintext = decryptValue({ ciphertext, key });
Webhook signatures
HMAC-SHA256 payload signing using the common sha256=<hex> header
convention (e.g., GitHub's X-Hub-Signature-256), with constant-time
verification on the receiving side.
import {
generateWebhookSecret,
signWebhookPayload,
verifyWebhookSignature,
} from '@ttoss/auth-core';
// Sender:
const secret = generateWebhookSecret();
const signature = signWebhookPayload({ payload: body, secret });
// send as a header, e.g. `X-Myapp-Signature: ${signature}`
// Receiver:
const isValid = verifyWebhookSignature({ payload: body, secret, signature });
Encoding helpers
import { decode, encode } from '@ttoss/auth-core';
const encoded = encode({ id: 1 }); // base64 JSON
const obj = decode(encoded);
OAuth 2.1 authorization server
createOAuthHandlers is a runner-agnostic OAuth 2.1 authorization-server engine: it implements the authorize/token/register flow and discovery metadata (RFC 8414, 7591, 7636, 6749, 9728) on top of the PKCE/code/JWT primitives above. It operates on plain { query, body, headers } → { status, body, redirect } objects, with no HTTP framework coupling, so any runtime (Koa, AWS Lambda, GraphQL) can host it through a thin adapter — @ttoss/http-server-auth ships the Koa one as oauthServer().
import { createOAuthHandlers } from '@ttoss/auth-core';
const oauth = createOAuthHandlers({
issuer,
clientStore,
authCodeStore,
issueTokens,
onAuthorize,
});
const res = await oauth.token({ query: {}, body, headers }); // { status, body }
Your app keeps its user model, signing keys, and login/consent UI behind the hooks. See the OAuth Authorization Server guideline for the full flow.
Refresh token rotation
createRefreshRotation implements opaque, server-stored refresh tokens with OAuth 2.1 rotation against any RefreshTokenStore: single use, expiry, scope narrowing, and reuse detection (replaying a rotated token revokes the owner's whole token set). Only token hashes are persisted. Wire issue into issueTokens and pass the ready onRefreshToken straight through.
import { createRefreshRotation } from '@ttoss/auth-core';
const refresh = createRefreshRotation({ store: refreshTokenStore });
createOAuthHandlers({
// …,
issueTokens: async ({ subject, scopes, client }) => ({
accessToken: signJwt({
payload: { sub: subject },
secret,
expiresInSeconds: 3600,
}),
refreshToken: await refresh.issue({ client, subject, scopes }),
expiresIn: 3600,
}),
onRefreshToken: refresh.onRefreshToken,
});
Opaque access tokens
createAccessTokenVerifier verifies opaque, server-stored access tokens (and long-lived personal API keys) against any AccessTokenStore. Tokens are stored hash-at-rest — only the SHA-256 hash crosses the store boundary, so a store compromise leaks nothing usable. Verification is default-deny (an unknown or expired token returns null without revealing whether it ever existed) and revocation is immediate (a token removed via delete/deleteBySubject fails the next call). Mint the opaque value with generateApiToken; its default hashing matches the verifier, so no extra wiring is needed.
import { createAccessTokenVerifier, generateApiToken } from '@ttoss/auth-core';
// Issue: persist only the hash; show the plain token to the user once.
const { token, tokenHash } = generateApiToken({ prefix: 'myapp' });
await store.save({
tokenHash,
subject: 'user_123',
scopes: ['read'],
clientId,
expiresAt: Date.now() + 3600_000, // null = never expires (personal API keys)
});
// Verify (e.g. inside an MCP/HTTP auth layer).
const verify = createAccessTokenVerifier({ store, touchLastUsed: true });
const identity = await verify(bearerToken); // VerifiedAccessToken | null
Prefer short-lived signed JWT access tokens (signJwt/verifyJwt) with refresh rotation when statelessness matters; reach for the opaque store when you need revocable access tokens or API keys.
StoredAccessToken carries two optional display fields useful for token-management UIs:
displayPrefix— a masked safe-to-show value (e.g."oca_3f2a…") generated bygenerateApiToken, so users can identify a token without exposing the full secret.createdAt— Unix timestamp (ms) when the token was issued, for sorted listing.
AccessTokenStore also accepts an optional listBySubject(subject) method. When implemented, it enables listing all active tokens for a user — useful for "Your active sessions" or "Personal access tokens" management pages.
const tokens = await store.listBySubject!('user_123');
// [{ tokenHash, displayPrefix, createdAt, expiresAt, scopes, … }, …]
Consent-redirect enrichment
createRedirectConsentOnAuthorize accepts two optional parameters for enriching the consent-screen redirect URL with client display data:
clientStore— fetches the registeredOAuthClientrecord byclient_id. When found,client_nameandlogo_urifields are appended as query parameters so the consent screen can display them without a separate API call.getClientDisplayFallback— called with{ clientId, client? }when the registered record is absent or missing display fields. Return a partialClientDisplay({ clientName?, logoUri? }) to fill the gaps. Consumer-owned — ttoss never hard-codes client display data.
import {
createRedirectConsentOnAuthorize,
type ClientDisplay,
} from '@ttoss/auth-core';
const onAuthorize = createRedirectConsentOnAuthorize({
consentUrl: 'https://app.example.com/consent',
getConsentGrant,
deleteConsentGrant,
clientStore,
getClientDisplayFallback: ({ clientId }): ClientDisplay => ({
clientName: clientNames[clientId],
logoUri: clientLogos[clientId],
}),
});
In-memory reference stores
createMemoryClientStore, createMemoryAuthCodeStore, createMemoryRefreshTokenStore, and createMemoryAccessTokenStore are Map-backed implementations of the store contracts — for tests, local development, and examples. Production swaps in a durable backend behind the same interfaces.
createMemoryAccessTokenStore implements listBySubject and round-trips displayPrefix/createdAt for tests and local development.