Password Reset
CruzJS provides a two-step password reset flow: request a reset (sends an email with a token link) and complete the reset (validates the token, updates the password, and revokes all sessions).
Request password reset
Section titled “Request password reset”The auth.requestPasswordReset mutation accepts an email address and queues a reset email. It intentionally does not reveal whether the email exists:
const result = await trpc.auth.requestPasswordReset.mutate({ email: 'user@example.com',});
// Always returns the same response regardless of whether the email exists// result.message -- "If an account exists, a password reset email has been sent"What happens server-side
Section titled “What happens server-side”AuthService.requestPasswordReset():
- Looks up the
authIdentityby email. If not found, returns silently (no error). - Generates a 32-byte random token.
- Calculates an expiry timestamp (default: 24 hours from now, configurable via
config.auth.passwordResetTokenExpiryHours). - Stores the token and expiry on the
authIdentityrow:
await db .update(authIdentity) .set({ passwordResetToken: token, passwordResetExpiry: expiresAt, // ISO string }) .where(eq(authIdentity.id, identity.id));- Queues a
send-emailjob withHIGHpriority:
await jobService.createJob({ type: 'send-email', payload: { to: email, template: 'password-reset', data: { name: name || 'there', resetUrl: `${APP_URL}/auth/reset-password/${token}`, }, }, priority: 'HIGH',});Reset password
Section titled “Reset password”The auth.resetPassword mutation validates the token, updates the password, and revokes all sessions:
const result = await trpc.auth.resetPassword.mutate({ token: 'abc123...', // from URL parameter newPassword: 'NewSecure1',});// result.message -- "Password reset successfully"What happens server-side
Section titled “What happens server-side”AuthService.resetPassword():
- Looks up the
authIdentitybypasswordResetToken. Throws if not found. - Checks the
passwordResetExpiryagainst the current time. Throws if expired. - Validates password strength (min 8 chars, uppercase, lowercase, number).
- Hashes the new password with bcrypt.
- Updates the identity:
await db .update(authIdentity) .set({ password: hashedPassword, passwordResetToken: null, // clear token (one-time use) passwordResetExpiry: null, // clear expiry }) .where(eq(authIdentity.id, identity.id));- Revokes all sessions for security:
await sessionService.deleteAllSessions(identity.id);This forces the user to re-authenticate on all devices after a password change.
Token expiry
Section titled “Token expiry”The reset token expiry is configurable:
export default defineConfig({ auth: { passwordResetTokenExpiryHours: 48, // default: 24 },});Expired tokens are rejected during the reset step. Each new reset request overwrites any existing token, so only the most recent token is valid.
Customizing the reset email
Section titled “Customizing the reset email”The password-reset email template receives:
| Variable | Description |
|---|---|
name | User’s name (or 'there' if not available) |
resetUrl | Full URL with token: {APP_URL}/auth/reset-password/{token} |
Frontend integration
Section titled “Frontend integration”A typical password reset page:
// routes/auth/reset-password/$token.tsximport { useParams } from 'react-router';
export default function ResetPasswordPage() { const { token } = useParams(); const resetMutation = trpc.auth.resetPassword.useMutation();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); resetMutation.mutate({ token: token!, newPassword: formData.get('password') as string, }); };
return ( <form onSubmit={handleSubmit}> <input name="password" type="password" placeholder="New password" required /> <button type="submit" disabled={resetMutation.isPending}> Reset Password </button> {resetMutation.isSuccess && <p>Password reset. Please log in.</p>} {resetMutation.error && <p>{resetMutation.error.message}</p>} </form> );}Security considerations
Section titled “Security considerations”- No email enumeration:
requestPasswordResetreturns the same response whether the email exists or not. - Token entropy: 32 bytes (256 bits) of randomness from
crypto.randomBytes. - One-time use: The token is cleared after a successful reset.
- Session revocation: All active sessions are destroyed on password reset, preventing an attacker who has the old password from maintaining access.
- Password strength: The new password must meet the same strength requirements as registration (8+ chars, uppercase, lowercase, number).
- Latest token wins: Requesting a new reset invalidates any previous token for the same account.