{
    "componentChunkName": "component---src-templates-blog-template-js",
    "path": "/implementing-2fa-totp-with-remix",
    "result": {"data":{"site":{"siteMetadata":{"title":"Edwards Moses -  Web, Mobile - React & React Native Software Developer"}},"markdownRemark":{"html":"<!--StartFragment-->\n<h2><strong>Introduction</strong></h2>\n<p>I've always wondered how 2FA actually worked.You open Google Authenticator,  scan a QR code, and suddenly the app starts generating a 6-digit code that keeps changing every few seconds; it felt like magic. From the user's side, the whole thing feels simple. From the app side, though, I wanted to understand the moving pieces properly.</p>\n<p>So I decided to build a small demo app, <code class=\"language-text\">totp-demo</code> to see the whole flow end-to-end: login with email and password, turn on 2FA from settings, scan a QR code, verify the first code, and then enforce that TOTP step on the next login.</p>\n<p> Hopefully, by the end, if we don't get lost, we'd have a working 2FA system! </p>\n<p>We'll keep the auth system super simple, back it with SQLite, and wire the up TOTP flow.</p>\n<h2>Getting started</h2>\n<p>I'm using the current React Router / Remix tooling here, so the project starts with <code class=\"language-text\">create-react-router</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">npx create-react-router@latest totp-demo\n<span class=\"token builtin class-name\">cd</span> totp-demo</code></pre></div>\n<p>I use Remix on a daily, so the route/action flow feels very familiar, but using the latest version to see what's new as well. </p>\n<p>I also used this as an excuse to finally try <a href=\"https://jj-vcs.github.io/jj/latest/\">Jujutsu</a>. I've heard enough good things about it for a while now, so I figured I might as well let this demo be the project where I stop procrastinating.</p>\n<p>I'm on a Mac, so I installed it with:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">brew <span class=\"token function\">install</span> jj</code></pre></div>\n<p>Then initialized the repo:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">jj <span class=\"token function\">git</span> init</code></pre></div>\n<p>That gave me the first clean snapshot of the project:</p>\n<p><img src=\"/assets/totp/edwardsmoses.com_CleanShot%202025-08-24%20at%2015.30.21.png\" alt=\"Jujutsu repository initialized\"></p>\n<p>And <code class=\"language-text\">jj st</code> gives the status view of the working tree:</p>\n<p><img src=\"/assets/totp/edwardsmoses.com_CleanShot%202025-08-24%20at%2015.39.43.png\" alt=\"Jujutsu status output\"></p>\n<p>Before the first describe, I configured identity:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">jj config <span class=\"token builtin class-name\">set</span> <span class=\"token parameter variable\">--user</span> user.name <span class=\"token string\">\"Edwards Moses\"</span>\njj config <span class=\"token builtin class-name\">set</span> <span class=\"token parameter variable\">--user</span> user.email <span class=\"token string\">\"edwardsmoses3@gmail.com\"</span></code></pre></div>\n<p>And then:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">jj describe <span class=\"token parameter variable\">-m</span> <span class=\"token string\">\"chore: start of totp demo\"</span></code></pre></div>\n<p><img src=\"/assets/totp/edwardsmoses.com_CleanShot%202025-08-24%20at%2015.49.53.png\" alt=\"Jujutsu first describe\"></p>\n<h2>Installing the packages</h2>\n<p>For this demo, I wanted the smallest set of dependencies: .</p>\n<ul>\n<li><code class=\"language-text\">speakeasy</code> handles the TOTP secret generation and token verification.</li>\n<li><code class=\"language-text\">qrcode</code> turns the <code class=\"language-text\">otpauth://</code> URL into an image that an authenticator app can scan.</li>\n<li><code class=\"language-text\">better-sqlite3</code> gives us a simple local database without introducing an ORM.</li>\n<li><code class=\"language-text\">bcryptjs</code> lets us hash the demo password instead of storing it in plain text.</li>\n</ul>\n<p>To install them:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">npm</span> <span class=\"token function\">install</span> speakeasy qrcode better-sqlite3 bcryptjs</code></pre></div>\n<p>We also need a session secret for the cookie session storage. I added this to a <code class=\"language-text\">.env</code> file at the project root:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token assign-left variable\">SESSION_SECRET</span><span class=\"token operator\">=</span>super-secret-but-not-for-production</code></pre></div>\n<h2>Setting up the demo database</h2>\n<p>Since I wanted this rooted in an 'actual' project, I didn't want to wave the data layer away with \"assume auth already exists\".</p>\n<p>I'm keeping the demo database intentionally small. We only need a single <code class=\"language-text\">users</code> table with the normal login bits and the two fields that matter for 2FA:</p>\n<ul>\n<li><code class=\"language-text\">two_factor_temp_secret</code> while the user is in setup</li>\n<li><code class=\"language-text\">two_factor_secret</code> once setup is verified and permanent</li>\n</ul>\n<p>Create <code class=\"language-text\">app/utils/db.server.ts</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">import</span> Database <span class=\"token keyword\">from</span> <span class=\"token string\">\"better-sqlite3\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> hashSync <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"bcryptjs\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">type</span> <span class=\"token class-name\">User</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n  id<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">;</span>\n  email<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n  password_hash<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n  two_factor_enabled<span class=\"token operator\">:</span> <span class=\"token builtin\">boolean</span><span class=\"token punctuation\">;</span>\n  two_factor_secret<span class=\"token operator\">?</span><span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n  two_factor_temp_secret<span class=\"token operator\">?</span><span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> db <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Database</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"totp-demo.sqlite\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\ndb<span class=\"token punctuation\">.</span><span class=\"token function\">exec</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n  CREATE TABLE IF NOT EXISTS users (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    email TEXT NOT NULL UNIQUE,\n    password_hash TEXT NOT NULL,\n    two_factor_enabled INTEGER NOT NULL DEFAULT 0,\n    two_factor_secret TEXT,\n    two_factor_temp_secret TEXT\n  )\n</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">const</span> existingUser <span class=\"token operator\">=</span> db\n  <span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"SELECT id FROM users WHERE email = ?\"</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"demo@example.com\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>existingUser<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span>\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n      INSERT INTO users (\n        email,\n        password_hash,\n        two_factor_enabled\n      ) VALUES (?, ?, 0)\n    </span><span class=\"token template-punctuation string\">`</span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">run</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"demo@example.com\"</span><span class=\"token punctuation\">,</span> <span class=\"token function\">hashSync</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"password123\"</span><span class=\"token punctuation\">,</span> <span class=\"token number\">10</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">findUserByEmail</span><span class=\"token punctuation\">(</span>email<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> User <span class=\"token operator\">|</span> <span class=\"token keyword\">undefined</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"SELECT * FROM users WHERE email = ?\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span>email<span class=\"token punctuation\">)</span> <span class=\"token keyword\">as</span> User <span class=\"token operator\">|</span> <span class=\"token keyword\">undefined</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">findUserById</span><span class=\"token punctuation\">(</span>id<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> User <span class=\"token operator\">|</span> <span class=\"token keyword\">undefined</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"SELECT * FROM users WHERE id = ?\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span>id<span class=\"token punctuation\">)</span> <span class=\"token keyword\">as</span> User <span class=\"token operator\">|</span> <span class=\"token keyword\">undefined</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">setTemporaryTwoFactorSecret</span><span class=\"token punctuation\">(</span>userId<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">,</span> secret<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span>\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n      UPDATE users\n      SET two_factor_temp_secret = ?\n      WHERE id = ?\n    </span><span class=\"token template-punctuation string\">`</span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">run</span><span class=\"token punctuation\">(</span>secret<span class=\"token punctuation\">,</span> userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">enableTwoFactor</span><span class=\"token punctuation\">(</span>userId<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span>\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n      UPDATE users\n      SET\n        two_factor_enabled = 1,\n        two_factor_secret = two_factor_temp_secret,\n        two_factor_temp_secret = NULL\n      WHERE id = ?\n    </span><span class=\"token template-punctuation string\">`</span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">run</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">clearTwoFactor</span><span class=\"token punctuation\">(</span>userId<span class=\"token operator\">:</span> <span class=\"token builtin\">number</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  db<span class=\"token punctuation\">.</span><span class=\"token function\">prepare</span><span class=\"token punctuation\">(</span>\n    <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n      UPDATE users\n      SET\n        two_factor_enabled = 0,\n        two_factor_secret = NULL,\n        two_factor_temp_secret = NULL\n      WHERE id = ?\n    </span><span class=\"token template-punctuation string\">`</span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">run</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>If you wanna get rid of the red-lines, you can install the types:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">npm</span> i --save-dev @types/better-sqlite3</code></pre></div>\n<p>For the demo, I'm seeding a single user directly from the DB helper, so I don't have to build registration too. That gives us a stable account to test against:</p>\n<ul>\n<li>email: <code class=\"language-text\">demo@example.com</code></li>\n<li>password: <code class=\"language-text\">password123</code></li>\n</ul>\n<p>At this point, the app shape I cared about looked like this:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">app/\n  routes/\n    login.tsx\n    settings.2fa.tsx\n    verify-2fa.tsx\n  utils/\n    db.server.ts\n    session.server.ts</code></pre></div>\n<h2>Session helpers</h2>\n<p>We need two session states in this app:</p>\n<ul>\n<li>a fully authenticated session with <code class=\"language-text\">userId</code></li>\n<li>a temporary \"halfway through auth\" session with <code class=\"language-text\">pending2faUserId</code></li>\n</ul>\n<p>That second state is the important part. After the password is correct, we still don't want to treat the user as fully signed in until the TOTP code checks out.</p>\n<p>Create <code class=\"language-text\">app/utils/session.server.ts</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> createCookieSessionStorage<span class=\"token punctuation\">,</span> redirect <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"react-router\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">const</span> sessionStorage <span class=\"token operator\">=</span> <span class=\"token function\">createCookieSessionStorage</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  cookie<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    name<span class=\"token operator\">:</span> <span class=\"token string\">\"__session\"</span><span class=\"token punctuation\">,</span>\n    httpOnly<span class=\"token operator\">:</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">,</span>\n    sameSite<span class=\"token operator\">:</span> <span class=\"token string\">\"lax\"</span><span class=\"token punctuation\">,</span>\n    path<span class=\"token operator\">:</span> <span class=\"token string\">\"/\"</span><span class=\"token punctuation\">,</span>\n    secure<span class=\"token operator\">:</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">,</span>\n    secrets<span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span>process<span class=\"token punctuation\">.</span>env<span class=\"token punctuation\">.</span><span class=\"token constant\">SESSION_SECRET</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>cookieHeader<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token operator\">|</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> sessionStorage<span class=\"token punctuation\">.</span><span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>cookieHeader<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">commitSession</span><span class=\"token punctuation\">(</span>session<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> sessionStorage<span class=\"token punctuation\">.</span><span class=\"token function\">commitSession</span><span class=\"token punctuation\">(</span>session<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">destroySession</span><span class=\"token punctuation\">(</span>session<span class=\"token operator\">:</span> <span class=\"token builtin\">any</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> sessionStorage<span class=\"token punctuation\">.</span><span class=\"token function\">destroySession</span><span class=\"token punctuation\">(</span>session<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">requireUserId</span><span class=\"token punctuation\">(</span>request<span class=\"token operator\">:</span> Request<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> session <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Cookie\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> userId <span class=\"token operator\">=</span> session<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"userId\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>userId<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">throw</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/login\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> userId<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>I'm using the same cookie for both states; the only thing that changes is which key is present in the session.</p>\n<h2>Building the login route</h2>\n<p>We making some progress, nothing visible yet, let's move to the actual auth flow. </p>\n<p>The <code class=\"language-text\">login</code> route checks the email and password. If the user doesn't have 2FA turned on, we set <code class=\"language-text\">userId</code> in session and redirect home. If they do have 2FA enabled, we set <code class=\"language-text\">pending2faUserId</code> instead and redirect to <code class=\"language-text\">/verify-2fa</code>.</p>\n<p>Create <code class=\"language-text\">app/routes/login.tsx</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> compareSync <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"bcryptjs\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Form<span class=\"token punctuation\">,</span> redirect<span class=\"token punctuation\">,</span> useActionData<span class=\"token punctuation\">,</span> useNavigation <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"react-router\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> findUserByEmail <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/db.server\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> getSession<span class=\"token punctuation\">,</span> commitSession <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/session.server\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">action</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> request <span class=\"token punctuation\">}</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> request<span class=\"token operator\">:</span> Request <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> formData <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> request<span class=\"token punctuation\">.</span><span class=\"token function\">formData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> email <span class=\"token operator\">=</span> formData<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"email\"</span><span class=\"token punctuation\">)</span><span class=\"token operator\">?.</span><span class=\"token function\">toString</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">trim</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> password <span class=\"token operator\">=</span> formData<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"password\"</span><span class=\"token punctuation\">)</span><span class=\"token operator\">?.</span><span class=\"token function\">toString</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">const</span> user <span class=\"token operator\">=</span> <span class=\"token function\">findUserByEmail</span><span class=\"token punctuation\">(</span>email<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>user <span class=\"token operator\">||</span> <span class=\"token operator\">!</span><span class=\"token function\">compareSync</span><span class=\"token punctuation\">(</span>password<span class=\"token punctuation\">,</span> user<span class=\"token punctuation\">.</span>password_hash<span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token string\">\"Invalid email or password.\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">const</span> session <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Cookie\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>user<span class=\"token punctuation\">.</span>two_factor_enabled<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    session<span class=\"token punctuation\">.</span><span class=\"token function\">set</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"pending2faUserId\"</span><span class=\"token punctuation\">,</span> user<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">return</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/verify-2fa\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n      headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n        <span class=\"token string-property property\">\"Set-Cookie\"</span><span class=\"token operator\">:</span> <span class=\"token keyword\">await</span> <span class=\"token function\">commitSession</span><span class=\"token punctuation\">(</span>session<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  session<span class=\"token punctuation\">.</span><span class=\"token function\">set</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"userId\"</span><span class=\"token punctuation\">,</span> user<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token string-property property\">\"Set-Cookie\"</span><span class=\"token operator\">:</span> <span class=\"token keyword\">await</span> <span class=\"token function\">commitSession</span><span class=\"token punctuation\">(</span>session<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">Login</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> actionData <span class=\"token operator\">=</span> <span class=\"token function\">useActionData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> navigation <span class=\"token operator\">=</span> <span class=\"token function\">useNavigation</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Login</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Form</span></span> <span class=\"token attr-name\">method</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>post<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>email<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>email<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">placeholder</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>demo@example.com<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">required</span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span>\n          <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>password<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>password<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">placeholder</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>password123<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">required</span>\n        <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>submit<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token punctuation\">{</span>navigation<span class=\"token punctuation\">.</span>state <span class=\"token operator\">===</span> <span class=\"token string\">\"submitting\"</span> <span class=\"token operator\">?</span> <span class=\"token string\">\"Signing in...\"</span> <span class=\"token operator\">:</span> <span class=\"token string\">\"Sign in\"</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">Form</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span>actionData<span class=\"token operator\">?.</span>error <span class=\"token operator\">?</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>actionData<span class=\"token punctuation\">.</span>error<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>I like this split because the password step stays boring. The only extra branch is whether we stop there or send the user to the second factor page.</p>\n<p>So, the below bit was something new, I'm used to the previous filesystem always routing for Remix, but in the new React Router flow, that needs to be explicitly <a href=\"https://reactrouter.com/start/framework/routing\">configured</a>.</p>\n<p>First, we want to install: </p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">npm</span> i @react-router/fs-routes</code></pre></div>\n<p>Then, we want to replace <code class=\"language-text\">app/routes.ts</code> with:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> <span class=\"token keyword\">type</span> <span class=\"token class-name\">RouteConfig</span><span class=\"token punctuation\">,</span> index <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"@react-router/dev/routes\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> flatRoutes <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"@react-router/fs-routes\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token punctuation\">[</span><span class=\"token operator\">...</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">await</span> <span class=\"token function\">flatRoutes</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">]</span> satisfies RouteConfig<span class=\"token punctuation\">;</span></code></pre></div>\n<p>I'm definitely not winning any design awards for this login page:</p>\n<p><img src=\"/assets/totp/edwardsmoses_CleanShot%202026-04-12%20at%2023.50.49.png\" alt=\"our current login page\"></p>\n<h2>Generating the QR code and turning on 2FA</h2>\n<p>Once the user is logged in, we can give them a page to enable 2FA.</p>\n<p>The first job on <code class=\"language-text\">/settings/2fa</code> is to generate a new secret and store it in <code class=\"language-text\">two_factor_temp_secret</code>. I only move it into <code class=\"language-text\">two_factor_secret</code> after the user proves the setup worked by entering a valid code from their authenticator app.</p>\n<p>Create <code class=\"language-text\">app/routes/settings.2fa.tsx</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">import</span> QRCode <span class=\"token keyword\">from</span> <span class=\"token string\">\"qrcode\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> speakeasy <span class=\"token keyword\">from</span> <span class=\"token string\">\"speakeasy\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Form<span class=\"token punctuation\">,</span> useActionData<span class=\"token punctuation\">,</span> useLoaderData <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"react-router\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span>\n  findUserById<span class=\"token punctuation\">,</span>\n  setTemporaryTwoFactorSecret<span class=\"token punctuation\">,</span>\n  enableTwoFactor<span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/db.server\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> requireUserId <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/session.server\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">qrCodeForSecret</span><span class=\"token punctuation\">(</span>userEmail<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">,</span> secret<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> otpAuthUrl <span class=\"token operator\">=</span> speakeasy<span class=\"token punctuation\">.</span><span class=\"token function\">otpauthURL</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    secret<span class=\"token punctuation\">,</span>\n    label<span class=\"token operator\">:</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">totp-demo:</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>userEmail<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">,</span>\n    issuer<span class=\"token operator\">:</span> <span class=\"token string\">\"totp-demo\"</span><span class=\"token punctuation\">,</span>\n    encoding<span class=\"token operator\">:</span> <span class=\"token string\">\"base32\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> QRCode<span class=\"token punctuation\">.</span><span class=\"token function\">toDataURL</span><span class=\"token punctuation\">(</span>otpAuthUrl<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">loader</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> request <span class=\"token punctuation\">}</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> request<span class=\"token operator\">:</span> Request <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> userId <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">requireUserId</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> user <span class=\"token operator\">=</span> <span class=\"token function\">findUserById</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>user<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">throw</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Response</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"User not found\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> status<span class=\"token operator\">:</span> <span class=\"token number\">404</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n    twoFactorEnabled<span class=\"token operator\">:</span> <span class=\"token function\">Boolean</span><span class=\"token punctuation\">(</span>user<span class=\"token punctuation\">.</span>two_factor_enabled<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">action</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> request <span class=\"token punctuation\">}</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> request<span class=\"token operator\">:</span> Request <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> userId <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">requireUserId</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> user <span class=\"token operator\">=</span> <span class=\"token function\">findUserById</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> formData <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> request<span class=\"token punctuation\">.</span><span class=\"token function\">formData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> intent <span class=\"token operator\">=</span> formData<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"intent\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>user<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">throw</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Response</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"User not found\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> status<span class=\"token operator\">:</span> <span class=\"token number\">404</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>intent <span class=\"token operator\">===</span> <span class=\"token string\">\"generate\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">const</span> secret <span class=\"token operator\">=</span> speakeasy<span class=\"token punctuation\">.</span><span class=\"token function\">generateSecret</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n      name<span class=\"token operator\">:</span> user<span class=\"token punctuation\">.</span>email<span class=\"token punctuation\">,</span>\n      issuer<span class=\"token operator\">:</span> <span class=\"token string\">\"totp-demo\"</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token function\">setTemporaryTwoFactorSecret</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">,</span> secret<span class=\"token punctuation\">.</span>base32<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n      qrCodeDataUrl<span class=\"token operator\">:</span> <span class=\"token keyword\">await</span> QRCode<span class=\"token punctuation\">.</span><span class=\"token function\">toDataURL</span><span class=\"token punctuation\">(</span>secret<span class=\"token punctuation\">.</span>otpauth_url <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      success<span class=\"token operator\">:</span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>intent <span class=\"token operator\">===</span> <span class=\"token string\">\"verify\"</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">const</span> token <span class=\"token operator\">=</span> formData<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"token\"</span><span class=\"token punctuation\">)</span><span class=\"token operator\">?.</span><span class=\"token function\">toString</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>user<span class=\"token punctuation\">.</span>two_factor_temp_secret<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token string\">\"Start setup again so we can generate a new secret.\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token keyword\">const</span> verified <span class=\"token operator\">=</span> speakeasy<span class=\"token punctuation\">.</span>totp<span class=\"token punctuation\">.</span><span class=\"token function\">verify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n      secret<span class=\"token operator\">:</span> user<span class=\"token punctuation\">.</span>two_factor_temp_secret<span class=\"token punctuation\">,</span>\n      encoding<span class=\"token operator\">:</span> <span class=\"token string\">\"base32\"</span><span class=\"token punctuation\">,</span>\n      token<span class=\"token punctuation\">,</span>\n      window<span class=\"token operator\">:</span> <span class=\"token number\">1</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>verified<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n        error<span class=\"token operator\">:</span>\n          <span class=\"token string\">\"That code didn't match. Try the current code from your authenticator app.\"</span><span class=\"token punctuation\">,</span>\n        qrCodeDataUrl<span class=\"token operator\">:</span> <span class=\"token keyword\">await</span> <span class=\"token function\">qrCodeForSecret</span><span class=\"token punctuation\">(</span>\n          user<span class=\"token punctuation\">.</span>email<span class=\"token punctuation\">,</span>\n          user<span class=\"token punctuation\">.</span>two_factor_temp_secret<span class=\"token punctuation\">,</span>\n        <span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n      <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span>\n\n    <span class=\"token function\">enableTwoFactor</span><span class=\"token punctuation\">(</span>userId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span>\n      success<span class=\"token operator\">:</span> <span class=\"token boolean\">true</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token string\">\"Unknown action.\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">TwoFactorSettings</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> actionData <span class=\"token operator\">=</span> <span class=\"token function\">useActionData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> loaderData <span class=\"token operator\">=</span> <span class=\"token function\">useLoaderData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Two-Factor Authentication</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span>loaderData<span class=\"token punctuation\">.</span>twoFactorEnabled <span class=\"token operator\">?</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">2FA is already enabled on this account.</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span>\n      <span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span><span class=\"token operator\">!</span>actionData<span class=\"token operator\">?.</span>qrCodeDataUrl <span class=\"token operator\">&amp;&amp;</span> <span class=\"token operator\">!</span>actionData<span class=\"token operator\">?.</span>success <span class=\"token operator\">?</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Form</span></span> <span class=\"token attr-name\">method</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>post<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>hidden<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>intent<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">value</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>generate<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>submit<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Generate QR code</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">Form</span></span><span class=\"token punctuation\">></span></span>\n      <span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span>actionData<span class=\"token operator\">?.</span>qrCodeDataUrl <span class=\"token operator\">?</span> <span class=\"token punctuation\">(</span>\n        <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n            Scan this with Google Authenticator, Microsoft Authenticator, or\n            Authy.\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>img</span> <span class=\"token attr-name\">src</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span>actionData<span class=\"token punctuation\">.</span>qrCodeDataUrl<span class=\"token punctuation\">}</span></span> <span class=\"token attr-name\">alt</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>TOTP QR code<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Form</span></span> <span class=\"token attr-name\">method</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>post<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n            </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>hidden<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>intent<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">value</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>verify<span class=\"token punctuation\">\"</span></span> <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n            </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span>\n              <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>text<span class=\"token punctuation\">\"</span></span>\n              <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>token<span class=\"token punctuation\">\"</span></span>\n              <span class=\"token attr-name\">placeholder</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>123456<span class=\"token punctuation\">\"</span></span>\n              <span class=\"token attr-name\">maxLength</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token number\">6</span><span class=\"token punctuation\">}</span></span>\n              <span class=\"token attr-name\">required</span>\n            <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n            </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>submit<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Verify and enable 2FA</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">Form</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n      <span class=\"token punctuation\">)</span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span>actionData<span class=\"token operator\">?.</span>error <span class=\"token operator\">?</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>actionData<span class=\"token punctuation\">.</span>error<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n      </span><span class=\"token punctuation\">{</span>actionData<span class=\"token operator\">?.</span>success <span class=\"token operator\">?</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">2FA is now enabled for this account.</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>A couple of useful things are happening here:</p>\n<ul>\n<li><code class=\"language-text\">speakeasy.generateSecret()</code> creates the shared secret</li>\n<li><code class=\"language-text\">QRCode.toDataURL()</code> turns the <code class=\"language-text\">otpauth://</code> URL into an image</li>\n<li>the first valid token is what graduates the secret from temporary to permanent</li>\n<li><code class=\"language-text\">window: 1</code> gives a small amount of time drift tolerance, which makes testing less annoying</li>\n</ul>\n<p>That temporary secret field is doing a real job for us. If the user abandons setup halfway through, we haven't fully enabled 2FA yet, and we haven't accidentally made the account harder to access.</p>\n<p>If you haven't already, let's also install the types for the packages we're using in this route:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">npm</span> i --save-dev @types/qrcode\n<span class=\"token function\">npm</span> i --save-dev @types/speakeasy</code></pre></div>\n<p>Okay, now we have the QR code displayed, yay progress!!</p>\n<p><img src=\"/assets/totp/edwardsmoses_CleanShot%202026-04-13%20at%2000.00.06.png\" alt=\"yay, progress!\"></p>\n<h2>Verifying the TOTP code during login</h2>\n<p>Now for the enforcing part. </p>\n<p>When a user with 2FA enabled logs in, we redirect them to <code class=\"language-text\">/verify-2fa</code>. On this route, we read <code class=\"language-text\">pending2faUserId</code> from session, look up the stored secret, and verify the submitted token.</p>\n<p>If the token is correct, we promote the session from \"pending 2FA\" to \"fully logged in\".</p>\n<p>Create <code class=\"language-text\">app/routes/verify-2fa.tsx</code>:</p>\n\n          <div class=\"gatsby-remark-prismjs-copy-button-container\">\n            <div class=\"gatsby-remark-prismjs-copy-button\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" onclick=\"gatsbyRemarkCopyToClipboard(this, this.parentNode.nextElementSibling)\">\n              Copy\n            </div>\n          </div>\n          \n<div class=\"gatsby-highlight\" data-language=\"tsx\"><pre class=\"language-tsx\"><code class=\"language-tsx\"><span class=\"token keyword\">import</span> speakeasy <span class=\"token keyword\">from</span> <span class=\"token string\">\"speakeasy\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Form<span class=\"token punctuation\">,</span> redirect<span class=\"token punctuation\">,</span> useActionData<span class=\"token punctuation\">,</span> useNavigation <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"react-router\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> findUserById <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/db.server\"</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> getSession<span class=\"token punctuation\">,</span> commitSession <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"~/utils/session.server\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">loader</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> request <span class=\"token punctuation\">}</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> request<span class=\"token operator\">:</span> Request <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> session <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Cookie\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>session<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"pending2faUserId\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">throw</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/login\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">async</span> <span class=\"token keyword\">function</span> <span class=\"token function\">action</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span> request <span class=\"token punctuation\">}</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> request<span class=\"token operator\">:</span> Request <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> session <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">getSession</span><span class=\"token punctuation\">(</span>request<span class=\"token punctuation\">.</span>headers<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"Cookie\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> pendingUserId <span class=\"token operator\">=</span> session<span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"pending2faUserId\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> token <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">await</span> request<span class=\"token punctuation\">.</span><span class=\"token function\">formData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"token\"</span><span class=\"token punctuation\">)</span><span class=\"token operator\">?.</span><span class=\"token function\">toString</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">||</span> <span class=\"token string\">\"\"</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>pendingUserId<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">throw</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/login\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">const</span> user <span class=\"token operator\">=</span> <span class=\"token function\">findUserById</span><span class=\"token punctuation\">(</span>pendingUserId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>user <span class=\"token operator\">||</span> <span class=\"token operator\">!</span>user<span class=\"token punctuation\">.</span>two_factor_secret<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token string\">\"This account doesn't have 2FA enabled.\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">const</span> verified <span class=\"token operator\">=</span> speakeasy<span class=\"token punctuation\">.</span>totp<span class=\"token punctuation\">.</span><span class=\"token function\">verify</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    secret<span class=\"token operator\">:</span> user<span class=\"token punctuation\">.</span>two_factor_secret<span class=\"token punctuation\">,</span>\n    encoding<span class=\"token operator\">:</span> <span class=\"token string\">\"base32\"</span><span class=\"token punctuation\">,</span>\n    token<span class=\"token punctuation\">,</span>\n    window<span class=\"token operator\">:</span> <span class=\"token number\">1</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">!</span>verified<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> error<span class=\"token operator\">:</span> <span class=\"token string\">\"Invalid code. Try the latest code from your authenticator app.\"</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  session<span class=\"token punctuation\">.</span><span class=\"token function\">unset</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"pending2faUserId\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  session<span class=\"token punctuation\">.</span><span class=\"token function\">set</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"userId\"</span><span class=\"token punctuation\">,</span> user<span class=\"token punctuation\">.</span>id<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token function\">redirect</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n    headers<span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token string-property property\">\"Set-Cookie\"</span><span class=\"token operator\">:</span> <span class=\"token keyword\">await</span> <span class=\"token function\">commitSession</span><span class=\"token punctuation\">(</span>session<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">default</span> <span class=\"token keyword\">function</span> <span class=\"token function\">VerifyTwoFactor</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> actionData <span class=\"token operator\">=</span> <span class=\"token function\">useActionData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> navigation <span class=\"token operator\">=</span> <span class=\"token function\">useNavigation</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Verify 2FA</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>h1</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">Enter the 6-digit code from your authenticator app.</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span><span class=\"token class-name\">Form</span></span> <span class=\"token attr-name\">method</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>post<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>input</span>\n          <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>text<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">name</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>token<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">placeholder</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>123456<span class=\"token punctuation\">\"</span></span>\n          <span class=\"token attr-name\">maxLength</span><span class=\"token script language-javascript\"><span class=\"token script-punctuation punctuation\">=</span><span class=\"token punctuation\">{</span><span class=\"token number\">6</span><span class=\"token punctuation\">}</span></span>\n          <span class=\"token attr-name\">required</span>\n        <span class=\"token punctuation\">/></span></span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>submit<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n          </span><span class=\"token punctuation\">{</span>navigation<span class=\"token punctuation\">.</span>state <span class=\"token operator\">===</span> <span class=\"token string\">\"submitting\"</span> <span class=\"token operator\">?</span> <span class=\"token string\">\"Checking...\"</span> <span class=\"token operator\">:</span> <span class=\"token string\">\"Verify code\"</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n        </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n      </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span><span class=\"token class-name\">Form</span></span><span class=\"token punctuation\">></span></span><span class=\"token plain-text\">\n\n      </span><span class=\"token punctuation\">{</span>actionData<span class=\"token operator\">?.</span>error <span class=\"token operator\">?</span> <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>p</span><span class=\"token punctuation\">></span></span><span class=\"token punctuation\">{</span>actionData<span class=\"token punctuation\">.</span>error<span class=\"token punctuation\">}</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>p</span><span class=\"token punctuation\">></span></span> <span class=\"token operator\">:</span> <span class=\"token keyword\">null</span><span class=\"token punctuation\">}</span><span class=\"token plain-text\">\n    </span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>And, we have a winner on our hands! With this, the loop is completed. </p>\n<p>At this point, the app behaves the way I wanted from the start:</p>\n<ul>\n<li>password-only login for accounts without 2FA</li>\n<li>password + TOTP for accounts with 2FA enabled</li>\n<li>setup only becomes permanent after the first successful verification</li>\n</ul>\n<h2>A quick note</h2>\n<p>For this demo, I'm storing the TOTP secret directly in SQLite so the flow is easy to follow.</p>\n<p>In a real application, I'd treat that secret much more carefully:</p>\n<ul>\n<li>encrypt it at rest</li>\n<li>make sure disable / reset flows are deliberate</li>\n<li>add backup codes so users don't get stranded when they lose a device</li>\n</ul>\n<p>I'm intentionally leaving recovery codes and more complex logic / edge-cases out of this walkthrough so the core TOTP flow stays easy to follow.</p>\n<h2>Testing the flow</h2>\n<p>Once everything was wired up, I ran through the whole thing with the seeded user.</p>\n<p>The test path is pretty simple:</p>\n<ol>\n<li>Sign in with <code class=\"language-text\">demo@example.com</code> and <code class=\"language-text\">password123</code>.</li>\n<li>Visit <code class=\"language-text\">/settings/2fa</code> and generate a QR code.</li>\n<li>Scan the QR code with an authenticator app.</li>\n<li>Enter the current 6-digit token to finish setup.</li>\n<li>Sign out and log back in.</li>\n<li>Confirm the app now pauses at <code class=\"language-text\">/verify-2fa</code> before finishing login.</li>\n</ol>\n<p>A few useful failure cases to test too:</p>\n<ul>\n<li>enter the wrong code during setup and make sure 2FA does not turn on</li>\n<li>enter the wrong code on <code class=\"language-text\">/verify-2fa</code> and make sure the session stays pending</li>\n<li>wait for the code to rotate and verify the next one still works</li>\n<li>clear the 2FA columns in SQLite and confirm the account falls back to password-only login</li>\n</ul>\n<p>If you can get through those cases cleanly, the implementation is in a pretty solid place.</p>\n<h2>Wrapping up</h2>\n<p>This was a really fun one.</p>\n<p>TOTP feels a lot less magical once I got hands-on. At the end of the day, the flow is just:</p>\n<ul>\n<li>generate a shared secret</li>\n<li>let the user scan it</li>\n<li>verify one code to confirm setup</li>\n<li>ask for the code again on future logins</li>\n</ul>\n<p>The nice part is how well this fits the Remix model.</p>\n<p>If you're adding this to an existing app, the main pieces to lift from the demo are the same ones that made this one work: temporary secret during setup, permanent secret after verification, and a separate login state for users who are halfway through authentication.</p>\n<p>Here's the repo, if you wanna grab and run the project: <a href=\"https://github.com/edwardsmoses/totp-remix-demo\">Totp Remix demo</a></p>\n<p>I'd like to chat with anyone who has implemented this in a production system, any weird edge-cases you have encountered ? </p>\n<p>Drop a comment below, I'd love to know. </p>\n<p>Until next time, folks!!</p>\n<p><em>PS: Not any closer to figuring out Jujutsu, yet, the mental model still feels foreign..</em></p>\n<!--EndFragment-->","frontmatter":{"date":"April 12, 2026","path":"/implementing-2fa-totp-with-remix","title":"Implementing Two-Factor Authentication (2FA) with TOTP in a Remix app","thumbnail":"/assets/totp/edwardsmoses.com_pexels-zulfugarkarimov-33440144.jpg","metaDescription":null}}},"pageContext":{}},
    "staticQueryHashes": ["3604841143"]}