One-time tokens
Single-use tokens for email verification, password resets, invitations, and custom flows.
One-time tokens are short-lived, single-use strings for flows like email verification, password resets, and invitations. The raw token is handed to the caller once and never stored — only a SHA-256 hash lives in the database. On first use (or expiry), the token is gone.
Setup
The module is part of KavachOS core. No extra plugin needed.
import { createKavach } from 'kavachos';
const kavach = await createKavach({
database: { provider: 'postgres', url: process.env.DATABASE_URL! },
secret: process.env.KAVACH_SECRET!,
baseUrl: 'https://auth.example.com',
});
// Access the module
const tokens = kavach.oneTimeTokens;Token purposes
Each token has a purpose that scopes its validity. Validation fails if the purpose at creation does not match the purpose at consumption.
| Purpose | Use |
|---|---|
email-verify | Confirm a new email address |
password-reset | Authenticate a password-reset request |
invitation | Invite a user to an org or workspace |
custom | Any application-specific flow |
Creating a token
createToken returns the raw token exactly once. Put it in a link or hand it to your mailer — there is no way to recover it from the database later.
const result = await tokens.createToken({
purpose: 'password-reset',
identifier: 'alice@example.com', // email, user ID, or any scoping key
ttlSeconds: 1800, // 30 minutes
});
if (result.success) {
const { token, expiresAt } = result.data;
await mailer.send({
to: 'alice@example.com',
subject: 'Reset your password',
html: `<a href="https://app.example.com/reset?token=${token}">Reset password</a>`,
});
}const result = await tokens.createToken({
purpose: 'email-verify',
identifier: user.id,
ttlSeconds: 86400, // 24 hours
});
if (result.success) {
await sendVerificationEmail(user.email, result.data.token);
}const result = await tokens.createToken({
purpose: 'invitation',
identifier: 'invited@example.com',
ttlSeconds: 604800, // 7 days
metadata: { orgId: 'org_abc123', role: 'member' },
});The default TTL is 3600 seconds (1 hour). Override it per call with ttlSeconds, or set defaultTtlSeconds on the module config to change the default for all tokens.
Validating a token
Call validateToken when the user lands on your reset or verification page. On success, the token is consumed immediately — a second call with the same token always fails.
const result = await tokens.validateToken(
incomingToken, // from the URL query param
'password-reset',
);
if (!result.success) {
// result.error.code is one of:
// TOKEN_NOT_FOUND | TOKEN_ALREADY_USED | TOKEN_EXPIRED | TOKEN_PURPOSE_MISMATCH
return { error: result.error.message };
}
const { identifier, metadata } = result.data;
// identifier === 'alice@example.com'
// Proceed with the resetvalidateToken marks the token as used before it returns. Even if your handler crashes after this call, the token cannot be reused. Handle the downstream action (password update, email confirmation) in the same request.
Revoking tokens
Revoke all active tokens for an identifier when a user takes an action that makes them obsolete — for example, invalidating outstanding reset links when a user changes their password through a different flow.
// Revoke all active password-reset tokens for this user
const result = await tokens.revokeTokens('alice@example.com', 'password-reset');
if (result.success) {
console.log(`Revoked ${result.data.count} token(s)`);
}
// Revoke everything for this identifier (all purposes)
await tokens.revokeTokens(user.id);Revocation is a soft operation — tokens are marked as used, not deleted. Expired tokens are excluded from the count.
Attaching metadata
Pass a metadata object to store arbitrary data alongside the token. It is returned on successful validation.
const result = await tokens.createToken({
purpose: 'invitation',
identifier: 'bob@example.com',
metadata: { orgId: 'org_xyz', role: 'admin', invitedBy: 'alice' },
});
// On validation:
const validation = await tokens.validateToken(token, 'invitation');
if (validation.success) {
const { orgId, role } = validation.data.metadata as { orgId: string; role: string };
await addUserToOrg(user.id, orgId, role);
}Error codes
| Code | Cause |
|---|---|
TOKEN_NOT_FOUND | Token does not exist or was already deleted |
TOKEN_ALREADY_USED | Token was consumed by a previous call |
TOKEN_EXPIRED | Token's expiresAt is in the past |
TOKEN_PURPOSE_MISMATCH | Purpose at validation does not match purpose at creation |
INVALID_INPUT | Empty token string or unknown purpose value |
CREATE_TOKEN_FAILED | Database write failed |
REVOKE_TOKENS_FAILED | Database update failed during revocation |
Security notes
Tokens are hashed at rest. Only a SHA-256 hash is stored. A database dump does not expose usable tokens.
Single-use enforcement is atomic. The mark-as-used update runs before the result is returned, with a conditional WHERE used = false. Concurrent requests for the same token will fail at the database level.
Purpose binding prevents cross-flow reuse. A password-reset token cannot be submitted to an email-verify endpoint.