Skip to content

createTokenVerifier

function createTokenVerifier(config: TokenVerifierConfig): TokenVerifierInstance

Creates a token verifier for generating and validating signed, expiring tokens. Useful for password reset links, email verification, magic links, and similar one-time-use flows.

import { createTokenVerifier } from 'ideal-auth';
const passwordResetTokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'password-reset',
});

interface TokenVerifierConfig {
secret: string;
purpose: string;
expiryMs?: number;
}
Field Type Required Default
secret string Yes
purpose string Yes
expiryMs number (milliseconds) No 3600000 (1 hour)

The secret used to sign and verify tokens. Must be at least 32 characters.

A label identifying the flow the tokens belong to (e.g. 'password-reset', 'email-verification', 'magic-link'). The purpose is mixed into the HMAC signature — it is not part of the token string — so a token only verifies against a verifier configured with the same purpose. This provides domain separation between flows without requiring a separate secret for each one.

Token lifetime in milliseconds. After this duration, verifyToken returns null.

// Tokens expire in 15 minutes
const shortLivedTokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'password-reset',
expiryMs: 15 * 60 * 1000,
});
// Tokens expire in 24 hours
const longLivedTokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'email-verification',
expiryMs: 24 * 60 * 60 * 1000,
});

createToken(userId: string): string

Generates a signed token for the given user ID. The token encodes the user ID, a random identifier, creation time, expiration time, and an HMAC signature.

const tokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'email-verification',
});
const token = tokens.createToken('user_abc123');
// "dXNlcl9hYmMxMjM.k8f3j2m1.1709136000000.1709139600000.a1b2c3d4..."

Token format:

The token is a dot-separated string with 5 parts:

base64url(userId).randomId.iat.exp.signature
Part Description
base64url(userId) The user ID, base64url-encoded to support dots and special characters
randomId A random identifier unique to this token
iat Issued-at timestamp in milliseconds (Date.now() at creation)
exp Expiration timestamp in milliseconds (iat + expiryMs)
signature HMAC-SHA256 signature covering all four preceding fields plus the verifier’s purpose
verifyToken(token: string): { userId: string; iatMs: number } | null

Validates a token’s signature and checks that it has not expired. Returns the decoded payload on success, or null if the token is invalid, expired, or tampered with.

const tokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'email-verification',
});
const result = tokens.verifyToken(token);
if (!result) {
return { error: 'Invalid or expired token.' };
}
// result.userId — the user ID encoded in the token
// result.iatMs — creation timestamp in milliseconds

Returns null when:

  • The token format is malformed (wrong number of segments)
  • The signature does not match (token was tampered with)
  • The token was created by a verifier with a different purpose
  • The token has expired (Date.now() > exp)

Tokens are stateless — there is no token store to revoke. Use the iatMs field to implement invalidation patterns without a database lookup per verification.

Store a passwordChangedAt timestamp on the user. Reject tokens issued before the last password change.

async function handlePasswordReset(token: string, newPassword: string) {
const result = tokens.verifyToken(token);
if (!result) {
return { error: 'Invalid or expired token.' };
}
const user = await db.user.findUnique({
where: { id: result.userId },
});
if (!user) {
return { error: 'User not found.' };
}
// Reject if token was issued before the last password change
if (user.passwordChangedAt && result.iatMs < user.passwordChangedAt.getTime()) {
return { error: 'This token has already been used.' };
}
const hashedPassword = await hash.make(newPassword);
await db.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
passwordChangedAt: new Date(),
},
});
return { success: true };
}

Store the token’s iatMs or a nonce in the database and check it during verification.

async function verifyEmail(token: string) {
const result = tokens.verifyToken(token);
if (!result) {
return { error: 'Invalid or expired token.' };
}
const user = await db.user.findUnique({
where: { id: result.userId },
});
if (!user || user.emailVerifiedAt) {
return { error: 'Invalid token.' };
}
await db.user.update({
where: { id: user.id },
data: { emailVerifiedAt: new Date() },
});
return { success: true };
}

import { createTokenVerifier, createHash } from 'ideal-auth';
const hash = createHash();
const passwordResetTokens = createTokenVerifier({
secret: process.env.AUTH_SECRET!,
purpose: 'password-reset',
expiryMs: 60 * 60 * 1000, // 1 hour
});
// Step 1: Generate a reset token and send it via email
async function requestPasswordReset(email: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user) {
// Return success even if user doesn't exist to prevent enumeration
return { success: true };
}
const token = passwordResetTokens.createToken(user.id);
const resetUrl = `https://example.com/reset-password?token=${token}`;
await sendEmail(user.email, 'Reset your password', resetUrl);
return { success: true };
}
// Step 2: Verify the token and update the password
async function resetPassword(token: string, newPassword: string) {
const result = passwordResetTokens.verifyToken(token);
if (!result) {
return { error: 'Invalid or expired reset link.' };
}
const hashedPassword = await hash.make(newPassword);
await db.user.update({
where: { id: result.userId },
data: {
password: hashedPassword,
passwordChangedAt: new Date(),
},
});
return { success: true };
}

import type {
TokenVerifierConfig,
TokenVerifierInstance,
} from 'ideal-auth';