Remix Auth TOTP

4.0.0 · active · verified Wed Apr 22

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

Warnings

Install

Imports

Quickstart

Demonstrates setting up `remix-auth-totp` with `remix-auth` to initialize the strategy with `secret` and `sendTOTP` callbacks, and handle an authentication attempt in a Remix action.

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");
  }
}

view raw JSON →