Skip to content

Conversation

@zhongliang02
Copy link
Contributor

@zhongliang02 zhongliang02 commented Dec 2, 2025

Context

Previously, the implementation of OTP is susceptible to interception through shoulder surfing or insecure mail channel or other OTP interception scenarios.

To address this, @karrui implemented a session nonce (#518), which binds the OTP to the login session, thereby protecting against OTP interception.

However, a better approach in my opinion would be to replicate RFC 7636 (originally intended for OAuth 2.0) here.

Details

The idea is as follows,

Login request

  • On initiating a new login flow, the user generates a secret called codeVerifier
  • The user hashes this value to get a value called codeChallenge
  • The user sends codeChallenge + email in the login request

OTP response

  • Server generates an OTP and stores it (PK on [email, codeChallenge])
  • Server sends this OTP to user

OTP verification request

  • To complete the log in, the user submits codeVerifier, email, and OTP.
  • Server hashes the codeVerifier to get a codeChallenge
  • Server retrieves the corresponding OTP for the derived [email, codeChallenge]
  • Server verifies that the OTP is correct and logs the user in

Advantages

Protection against login request interception

The original nonce implementation only guards against OTP response interception.
The PKCE-like implementation guards against both OTP response interception and Login request interception.

The security improvement is negligible under the traditional browser-server-HTTPs assumption. But in mobile/native app implementations, the security improvement is great, since the Login request cannot be assumed to be transmitted over a secure channel.

I think using this implementation does not incur significant additional engineering costs, but can prevent insecure implementations for teams creating mobile/native apps based on starter-kit.

Protection against db dumps

In the event of a DB dump, while the attacker can extract the OTP through bruteforcing the entire keyspace, it will be impossible for the attacker to recover the original codeVerifier.

Disadvantages

Bloats VerificationToken table

This greatly bloats the VerificationToken table since each new login attempt creates a new [email, codeChallenge] entry which are only cleaned up on successful logins. However, this is no worse than the previous nonce implementation. Furthermore, the table is already bloated from users submitting wrong/invalid emails (though they are rate limited in this case).

A solution to this is to run a cronjob that removes old entries based on issuedAt.

Additional frontend memory usage

Every login attempt generates a new codeVerifier codeChallenge pair in frontend which are stored until login succeeds.
If the user attempts to log in an absurd amount of times without succeeding, it will lead to memory exhaustion.

Serverside implementation details

We modify the VfnIdentifier to be JSON.stringify([email, codeChallenge]) to prevent issues with string concatenation (e.g. if we construct the id as {email}.{codeChallenge}, an email with a . could lead to incorrect keying)

We modify all auth.utils.ts functions to require a codeChallenge as input.

We also prevent reuse of codeChallenge by replacing the VerificationToken upsert with create.

Finally, in our router we expect codeChallenge for the first leg, and codeVerifier for the second leg.

There are thorough input validations and tests have been rewritten.

Frontend implementation details

We remove nonce from session data.

In the SignInWizardProvider, we add a Map to keep track of all codeVerifier and codeChallenge ever generated. We then provide convenience functions newChallenge(), getVerifier(challenge: string) and cleanup function clearVerifierMap().

In the EmailStep, we generate a newChallenge() on form submission, and we store the challenge we used in VfnStepData of the SignInWizardProvider.

In the VerificationStep, we retrieve getVerifier(vfnStepData.codeChallenge) and send it with the form submission. When the login flow is successful, the map is cleared.

Other risks

This depends on storing the codeChallenge and codeVerifier in memory, as opposed to localStorage or sessionStorage. This means that if browser refresh or navigation occurs or any other context destroying event occurs, the user needs to restart the login flow.

@vercel
Copy link

vercel bot commented Dec 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
starter-kit Ready Ready Preview Comment Dec 6, 2025 4:35am
starter-kit-web Error Error Dec 6, 2025 4:35am

@zhongliang02 zhongliang02 requested review from Copilot and karrui and removed request for Copilot December 2, 2025 08:25
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a PKCE-like (Proof Key for Code Exchange) authentication flow to enhance OTP security, replacing the previous session-based nonce implementation. The new approach protects against both OTP interception and login request interception, particularly beneficial for mobile/native app implementations where secure channels cannot be guaranteed.

Key changes:

  • Replaced session nonce with PKCE flow using codeVerifier and codeChallenge pairs
  • Modified database operations to use create instead of upsert to prevent code challenge reuse
  • Updated client-side state management to store verifier/challenge pairs in memory via React context

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
apps/web/src/validators/auth.ts Added validation for codeChallenge and codeVerifier parameters
apps/web/src/server/session.ts Removed nonce field from session data interface
apps/web/src/server/modules/auth/auth.utils.ts Updated authentication utilities to use codeChallenge instead of nonce
apps/web/src/server/modules/auth/auth.service.ts Modified login/verification logic to use PKCE flow and prevent challenge reuse
apps/web/src/server/modules/auth/auth.pkce.ts New file implementing PKCE verifier/challenge generation functions
apps/web/src/server/modules/auth/tests/auth.utils.spec.ts Updated tests to use codeChallenge terminology and added entropy validation
apps/web/src/server/modules/auth/tests/auth.service.spec.ts Rewrote tests to validate PKCE flow scenarios
apps/web/src/server/api/routers/auth/auth.email.router.ts Updated router to accept codeChallenge/codeVerifier and removed session nonce handling
apps/web/src/app/(public)/sign-in/_components/wizard/email/verification-step.tsx Modified to retrieve and use codeVerifier from context
apps/web/src/app/(public)/sign-in/_components/wizard/email/email-step.tsx Updated to generate and pass codeChallenge on form submission
apps/web/src/app/(public)/sign-in/_components/wizard/email/email-flow.tsx Added codeChallenge to state propagation
apps/web/src/app/(public)/sign-in/_components/wizard/context.tsx Implemented verifier/challenge map storage and management functions
Comments suppressed due to low confidence (1)

apps/web/src/server/modules/auth/tests/auth.service.spec.ts:1

  • [nitpick] Missing spaces in object destructuring and object literal. Should be: const { token: invalidToken } = createAuthToken({ email: testEmail, codeChallenge: testCodeChallenge })
import '../../mail/__mocks__/mail.service'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@zhongliang02
Copy link
Contributor Author

@karrui I need help, pnpm run format:fix is broken for me

Logs

pnpm run format:fix

> @acme/monorepo@ format:fix /Users/zhongliang/Projects/starter-kit
> turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache

turbo 2.6.1

• Packages in scope: @acme/db, @acme/eslint-config, @acme/github, @acme/prettier-config, @acme/storybook-config, @acme/tailwind-config, @acme/tsconfig, @acme/ui, @acme/validators, @acme/web
• Running format in 10 packages
• Remote caching disabled
 @acme/validators#format > cache hit (outputs already on disk), suppressing logs d9d742494b5f8acd 
 @acme/storybook-config#format > cache hit (outputs already on disk), suppressing logs 16c84b6cf87dda9d 
 @acme/prettier-config#format > cache hit (outputs already on disk), suppressing logs 4e63fe74cb80cff8 
 @acme/tailwind-config#format > cache hit (outputs already on disk), suppressing logs 36bf0ed4b405da5f 
 @acme/eslint-config#format > cache hit (outputs already on disk), suppressing logs b89eef965239cf30 
 @acme/db#format > cache hit (outputs already on disk), suppressing logs a2f27ab31423f225 
┌─ @acme/ui#format > cache miss, executing 2bede75bd4e8d6c4 


> @acme/ui@ format /Users/zhongliang/Projects/starter-kit/packages/ui
> prettier --check . --ignore-path ../../.gitignore --write --cache --cache-location .cache/.prettiercache

Checking formatting...
types/css.d.ts
[error] types/css.d.ts: TypeError: Cannot read properties of undefined (reading 'some')
[error]     at getChunkTypeOfNode (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettie
[email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-chunk-type-of-node.js:40:45)
[error]     at /Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected]/node_modules
/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:30:70
[error]     at Array.reduce (<anonymous>)
[error]     at getSortedNodes (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettier@3.
6.2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:29:42)
[error]     at preprocessor (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected].
2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/preprocessor.js:51:65)
[error]     at Object.defaultPreprocessor [as preprocess] (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@ianvs+prettier-plugin-so
[email protected][email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/default.js:8:44)
[error]     at Object.preprocess (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]_@ianvs+pr
[email protected][email protected][email protected]/node_modules/prettier-plugin-tailwindcss/dist/index.mjs:3512:11124)
[error]     at parse5 (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.mjs:16727:4
3)
[error]     at async coreFormat (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.m
js:17287:25)
[error]     at async formatWithCursor (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/i
ndex.mjs:17504:14)
Error occurred when checking code style in the above file.
 ELIFECYCLE  Command failed with exit code 2.

command finished with error, but continuing...
└─ @acme/ui#format ──
┌─ @acme/web#format > cache miss, executing c0ac065db7ff924d 


> @acme/web@ format /Users/zhongliang/Projects/starter-kit/apps/web
> prettier --check . --ignore-path ../../.gitignore --write --cache --cache-location .cache/.prettiercache

Checking formatting...
.storybook/preview.ts
[error] .storybook/preview.ts: TypeError: Cannot read properties of undefined (reading 'some')
[error]     at getChunkTypeOfNode (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettie
[email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-chunk-type-of-node.js:40:45)
[error]     at /Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected]/node_modules
/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:30:70
[error]     at Array.reduce (<anonymous>)
[error]     at getSortedNodes (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettier@3.
6.2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:29:42)
[error]     at preprocessor (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected].
2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/preprocessor.js:51:65)
[error]     at Object.defaultPreprocessor [as preprocess] (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@ianvs+prettier-plugin-so
[email protected][email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/default.js:8:44)
[error]     at Object.preprocess (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]_@ianvs+pr
[email protected][email protected][email protected]/node_modules/prettier-plugin-tailwindcss/dist/index.mjs:3512:11124)
[error]     at parse5 (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.mjs:16727:4
3)
[error]     at async coreFormat (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.m
js:17287:25)
[error]     at async formatWithCursor (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/i
ndex.mjs:17504:14)
src/app/layout.tsx
[error] src/app/layout.tsx: TypeError: Cannot read properties of undefined (reading 'some')
[error]     at getChunkTypeOfNode (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettie
[email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-chunk-type-of-node.js:40:45)
[error]     at /Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected]/node_modules
/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:30:70
[error]     at Array.reduce (<anonymous>)
[error]     at getSortedNodes (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettier@3.
6.2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:29:42)
[error]     at preprocessor (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected].
2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/preprocessor.js:51:65)
[error]     at Object.defaultPreprocessor [as preprocess] (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@ianvs+prettier-plugin-so
[email protected][email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/default.js:8:44)
[error]     at Object.preprocess (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]_@ianvs+pr
[email protected][email protected][email protected]/node_modules/prettier-plugin-tailwindcss/dist/index.mjs:3512:11124)
[error]     at parse5 (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.mjs:16727:4
3)
[error]     at async coreFormat (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.m
js:17287:25)
[error]     at async formatWithCursor (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/i
ndex.mjs:17504:14)
src/server/modules/auth/__tests__/auth.service.spec.ts
[error] src/server/modules/auth/__tests__/auth.service.spec.ts: TypeError: Cannot read properties of undefined (reading 'some')
[error]     at getChunkTypeOfNode (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettie
[email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-chunk-type-of-node.js:40:45)
[error]     at /Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected]/node_modules
/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:30:70
[error]     at Array.reduce (<anonymous>)
[error]     at getSortedNodes (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected]_prettier@3.
6.2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/utils/get-sorted-nodes.js:29:42)
[error]     at preprocessor (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@[email protected][email protected].
2/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/preprocessor.js:51:65)
[error]     at Object.defaultPreprocessor [as preprocess] (/Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/@ianvs+prettier-plugin-so
[email protected][email protected]/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/preprocessors/default.js:8:44)
[error]     at Object.preprocess (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]_@ianvs+pr
[email protected][email protected][email protected]/node_modules/prettier-plugin-tailwindcss/dist/index.mjs:3512:11124)
[error]     at parse5 (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.mjs:16727:4
3)
[error]     at async coreFormat (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/index.m
js:17287:25)
[error]     at async formatWithCursor (file:///Users/zhongliang/Projects/starter-kit/node_modules/.pnpm/[email protected]/node_modules/prettier/i
ndex.mjs:17504:14)
Error occurred when checking code style in 3 files.
 ELIFECYCLE  Command failed with exit code 2.

command finished with error, but continuing...
└─ @acme/web#format ──
@acme/ui#format: command (/Users/zhongliang/Projects/starter-kit/packages/ui) /Users/zhongliang/Library/pnpm/.tools/pnpm/10.18.1/bin/pnpm run format --write --cache --cache-location .cache/.prettiercache exited (2)
@acme/web#format: command (/Users/zhongliang/Projects/starter-kit/apps/web) /Users/zhongliang/Library/pnpm/.tools/pnpm/10.18.1/bin/pnpm run format --write --cache --cache-location .cache/.prettiercache exited (2)

 Tasks:    6 successful, 8 total
Cached:    6 cached, 8 total
  Time:    951ms 
Failed:    @acme/ui#format, @acme/web#format

 ERROR  run failed: command  exited (2)
 ELIFECYCLE  Command failed with exit code 2.
➜  starter-kit git:(feat/add-pkce-like-session-binding) ✗ 

Co-authored-by: Copilot <[email protected]>
@karrui
Copy link
Collaborator

karrui commented Dec 4, 2025

build is failing (locally) too.

can see associated issue for the fix? (which looks like overriding some dependencies :O)
react-hook-form/resolvers#818

also also might want to rebase

@zhongliang02
Copy link
Contributor Author

lol its problematic because build works locally for me idek why

@zhongliang02
Copy link
Contributor Author

changed nothing and it works

@zhongliang02
Copy link
Contributor Author

ok i lied i changed vercel build settings to node24

@zhongliang02 zhongliang02 requested a review from karrui December 6, 2025 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants