One-Time Token Plugin

Secure verification tokens for sensitive operations like account deletion and organization management.

Protect sensitive operations with one-time verification codes. When users delete accounts, remove organizations, or perform other high-security actions, they must verify with a 6-digit code sent to their email.

This page is part of the Authentication documentation.

Overview

The one-time token plugin generates cryptographically secure, single-use verification codes. Unlike session-based verification, these codes:

  • Expire after a short time (default: 10 minutes)
  • Can only be used once
  • Are stored as hashed values for security
  • Require the user to have email access

Use Cases

The one-time token plugin is used for:

  • Account deletion - Verify before permanently deleting user account
  • Organization deletion - Verify before removing organization and all data
  • Sensitive profile changes - Email changes, phone number updates
  • Any operation requiring email-based confirmation

Configuration

Default Settings

SettingValueDescription
Expiration10 minutesToken validity period
StorageHashedTokens stored as SHA-256 hashes
Length6 digitsCode format (100000-999999)

Environment Variables

apps/web/.env.local

# Optional: customize expiration time in minutes
OTP_EXPIRES_MINUTES=10

Implementation

The plugin is configured in packages/better-auth/src/plugins/one-time-token.ts:

import { oneTimeToken } from 'better-auth/plugins/one-time-token';
const MIN_CODE = 100_000;
const CODE_RANGE = 900_000;
const UINT32_RANGE = 2 ** 32;
const UINT32_LIMIT = Math.floor(UINT32_RANGE / CODE_RANGE) * CODE_RANGE;
function generateSixDigitCode() {
const crypto = globalThis.crypto;
if (!crypto?.getRandomValues) {
throw new Error('Web Crypto unavailable');
}
const buffer = new Uint32Array(1);
let value: number;
// Rejection sampling for uniform distribution
do {
crypto.getRandomValues(buffer);
value = buffer[0]!;
} while (value >= UINT32_LIMIT);
return String((value % CODE_RANGE) + MIN_CODE);
}
const OTP_EXPIRES_MINUTES = parseInt(
process.env.OTP_EXPIRES_MINUTES ?? '10',
10,
);
export const oneTimeTokenPlugin = oneTimeToken({
expiresIn: OTP_EXPIRES_MINUTES,
generateToken: async () => {
const code = generateSixDigitCode();
if (process.env.NODE_ENV === 'development') {
console.log(`[DEV] Verification code: ${code}`);
}
return code;
},
storeToken: 'hashed',
});

Cryptographic Security

The code generation uses:

  • Web Crypto API - crypto.getRandomValues() for true randomness
  • Rejection sampling - Ensures uniform distribution across the code range
  • No modulo bias - Codes are evenly distributed (100000-999999)

Development Mode

In development (NODE_ENV=development), generated codes are logged to the console:

[DEV] Verification code: 847293

This allows testing sensitive operations without email configuration.

Token Lifecycle

  1. User initiates sensitive operation (e.g., delete account)
  2. Server generates a 6-digit code
  3. Code is hashed and stored with expiration
  4. Email is sent to user with the code
  5. User enters code in verification dialog
  6. Server verifies the hashed code
  7. If valid, operation proceeds
  8. Code is invalidated immediately

Client Usage

Account Deletion Flow

The delete account dialog demonstrates the typical flow:

import { authClient } from '@kit/better-auth/client';
// Step 1: Request verification email
const handleRequestCode = async () => {
await authClient.user.sendDeleteAccountVerification();
setStep('verify');
};
// Step 2: Verify code and delete
const handleVerifyAndDelete = async (code: string) => {
const result = await authClient.user.deleteUser({
token: code,
});
if (result.error) {
console.error('Invalid or expired code');
return;
}
// Account deleted successfully
router.push('/');
};

Generic Token Operations

import { authClient } from '@kit/better-auth/client';
// Request a one-time token
await authClient.oneTimeToken.sendToken({
type: 'custom-operation',
});
// Verify the token
const result = await authClient.oneTimeToken.verifyToken({
token: '847293',
type: 'custom-operation',
});
if (result.data?.valid) {
// Proceed with operation
}

Security Features

Hashed Storage

Tokens are stored as SHA-256 hashes in the database. Even if the database is compromised:

  • Attackers cannot retrieve the original codes
  • Codes cannot be reused from stolen hashes
  • Each code maps to a unique hash

Single Use

Each token is invalidated immediately after successful verification. Replay attacks are prevented.

Short Expiration

The 10-minute default expiration limits the attack window. If a code is intercepted, it becomes useless quickly.

Cryptographically Secure Generation

Using the Web Crypto API ensures:

  • No predictable patterns in codes
  • Uniform distribution (no bias toward certain numbers)
  • Resistance to statistical analysis

Error Handling

ErrorDescriptionUser Action
Token expiredCode was not used within expiration windowRequest a new code
Token invalidCode doesn't match or was already usedCheck for typos, request new code
Token not foundNo pending verification for this operationStart the operation again

In the Application

The delete account flow is implemented in packages/account/ui/src/components/delete-account-dialog.tsx:

  1. User clicks "Delete Account"
  2. Warning dialog explains consequences
  3. User confirms and email is sent
  4. User enters 6-digit code from email
  5. Account is permanently deleted

Common Pitfalls

  • Code expiration: Users must enter codes within 10 minutes. UI should show countdown or warning.
  • Email delays: Slow email delivery can cause codes to expire. Monitor delivery times.
  • Copy-paste errors: Ensure UI allows easy code entry. Consider auto-focus and input validation.
  • Multiple requests: New requests invalidate previous codes. UI should indicate this to users.

Frequently Asked Questions

Why use one-time tokens instead of just password confirmation?
One-time tokens verify email access in addition to knowing the password. This protects against stolen credentials being used to delete accounts.
Can I extend the expiration time?
Yes. Set OTP_EXPIRES_MINUTES environment variable. However, longer expiration windows reduce security.
What operations use one-time tokens?
Account deletion, organization deletion, and email changes. You can add one-time token verification to any sensitive operation.
Are one-time tokens the same as OTPs?
Similar but different plugins. OTP (email OTP plugin) is for authentication. One-time tokens are for verifying specific operations after authentication.
What happens if a user never uses their code?
The code expires after 10 minutes. Expired tokens are cleaned up automatically. Users can request new codes as needed.

Next: Admin Plugin →