Remix Auth TOTP
remix-auth-totp is an authentication strategy for Remix Auth that enables Time-Based One-Time Password (TOTP) based authentication, including support for email-code and magic link workflows. The current stable version is 4.0.0, which introduced compatibility with Remix Auth v4 and React Router v7. The package maintains a regular release cadence, with major versions often aligning with upstream Remix Auth updates, and minor/patch releases addressing features and bug fixes. Key differentiators include built-in magic link functionality, support for Cloudflare Pages, enhanced security through JWE encryption and SHA256 hashing (since v3.2.0), and a strong TypeScript foundation. A significant architectural shift in v3.0.0 eliminated direct database reliance, simplifying its integration model.
Common errors
-
TypeError: Buffer is not a constructor
cause Running an older version of `remix-auth-totp` (prior to v3.4.2) on Node.js v20 or a similar runtime environment that has removed the global Buffer constructor.fixUpgrade `remix-auth-totp` to v3.4.2 or higher to utilize `Uint8Array` and Web Crypto API for compatibility with modern Node.js versions. -
Error: TOTPStrategy: secret is a required option
cause The `secret` callback function was not provided or returned null/undefined in the `TOTPStrategy` constructor options.fixEnsure the `TOTPStrategy` constructor is provided with a `secret` async function that reliably retrieves the user's unique TOTP secret based on the input (e.g., email). -
ReferenceError: TOTPStrategy is not defined
cause Incorrect CommonJS `require` syntax or incorrect named import for `TOTPStrategy` in an ES module context.fixUse a named import: `import { TOTPStrategy } from 'remix-auth-totp';` or for CommonJS: `const { TOTPStrategy } = require('remix-auth-totp');`. -
Authentication failed: Error: Invalid TOTP code or Magic Link.
cause The `verify` callback function provided to the `TOTPStrategy` threw an error, indicating that the submitted code or magic link was incorrect, expired, or the associated user could not be found.fixDebug the `verify` callback. Check if the submitted `code` matches the expected TOTP, if the `magicLink` is valid and not expired, and if the user data used for verification is correct.
Warnings
- breaking Version 4.0.0 introduces support for Remix Auth v4. Applications using older versions of Remix Auth (v3 or earlier) must upgrade `remix-auth` to a compatible v4 release when updating `remix-auth-totp` to v4.0.0.
- breaking Version 3.0.0 fundamentally changed the strategy's internal workings by eliminating direct reliance on a database for state management. This means `TOTPData` and CRUD interfaces introduced in v2 are no longer used. Existing implementations from v2 will require significant refactoring to adapt to the new stateless approach.
- breaking Version 3.2.0 enhanced security by introducing JWE encryption and updating the default hashing algorithm to SHA256. This change might invalidate existing TOTP secrets or verification flows if not properly migrated, as the underlying cryptographic methods have changed.
- gotcha Versions prior to 3.4.2 might encounter issues when running on Node.js v20+ environments due to changes in Node.js's `Buffer` and `crypto` APIs. Specifically, `Buffer` was replaced with `Uint8Array` and `crypto` with Web Crypto.
- gotcha Magic link generation in environments like Cloudflare local development (wrangler/miniflare) using versions prior to 1.4.1 might use incorrect host URLs, leading to broken magic links.
Install
-
npm install remix-auth-totp -
yarn add remix-auth-totp -
pnpm add remix-auth-totp
Imports
- TOTPStrategy
const TOTPStrategy = require('remix-auth-totp');import { TOTPStrategy } from 'remix-auth-totp'; - TOTPStrategyOptions
import { TOTPStrategyOptions } from 'remix-auth-totp';import type { TOTPStrategyOptions } from 'remix-auth-totp'; - TOTPError
const { TOTPError } = require('remix-auth-totp');import { TOTPError } from 'remix-auth-totp';
Quickstart
import { Authenticator } from "remix-auth";
import { TOTPStrategy } from "remix-auth-totp";
import { createCookieSessionStorage, redirect } from "@remix-run/node";
interface User {
id: string;
email: string;
}
// 1. Setup session storage
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
sameSite: "lax",
path: "/",
secrets: [process.env.SESSION_SECRET ?? 'a_secret_key'], // Replace with actual secret
secure: process.env.NODE_ENV === "production",
},
});
// 2. Initialize Authenticator
export const authenticator = new Authenticator<User>(sessionStorage);
// Mock database for TOTP secrets (replace with your actual database)
const userTotpSecrets = new Map<string, string>();
userTotpSecrets.set("test@example.com", process.env.TOTP_USER_SECRET ?? 'A_VERY_SECURE_SECRET');
// 3. Register the TOTP Strategy
authenticator.use(
new TOTPStrategy(
{
secret: async ({ email }) => {
// Fetch the user's TOTP secret from your database
const secret = userTotpSecrets.get(email);
if (!secret) {
throw new Error(`TOTP not configured for ${email}.`);
}
return secret;
},
sendTOTP: async ({ email, code, magicLink, request }) => {
// In a real app, send `code` or `magicLink` via email/SMS to `email`.
console.log(`Sending code ${code} or magic link to ${email}`);
// Example: await sendEmail({ to: email, subject: "Your TOTP Code", body: `Code: ${code}. Or login: ${magicLink}` });
},
// You can add other options like `maxAge`, `issuer`, `magicLinkPath`
},
async ({ email, code, magicLink }) => {
// Verify the user and return the user object upon successful authentication.
// This is where you would fetch the user from your DB and return it.
if (email === "test@example.com" && (code === "123456" || magicLink)) { // Simplified logic
return { id: "user-abc", email: "test@example.com" };
}
throw new Error("Invalid TOTP or Magic Link.");
}
),
"totp" // Unique name for this strategy
);
// 4. Example Remix Action (e.g., in `app/routes/login.tsx`)
export async function action({ request }: { request: Request }) {
try {
return await authenticator.authenticate("totp", request, {
successRedirect: "/dashboard",
failureRedirect: "/login?error=true",
});
} catch (error) {
if (error instanceof Response && error.status >= 300 && error.status < 400) {
throw error; // Propagate redirects from authenticator (e.g., magic link sent)
}
console.error("Login failed:", error);
return redirect("/login?error=true");
}
}