Skip to content

feat(auth): add phone number methods to createNeonAuth - #256

Open
Shridhad wants to merge 5 commits into
mainfrom
feat/auth-phone-number-server
Open

Shridhad wants to merge 5 commits into
mainfrom
feat/auth-phone-number-server

Conversation

@Shridhad

Copy link
Copy Markdown
Collaborator

The phone number plugin already works on the client, but the server auth object had no methods for it. This adds signIn.phoneNumber() plus phoneNumber.sendOtp(), verify(), requestPasswordReset() and resetPassword(), using the Better Auth 1.7.6 phone-number routes. signIn.phoneNumber() and verify() set the session cookies the same way signIn.email() does.

'use server';
import { auth } from '@/lib/auth/server';

await auth.phoneNumber.sendOtp({ phoneNumber: '+15551234567' });
const { error } = await auth.signIn.phoneNumber({
  phoneNumber: '+15551234567',
  password,
});

The project needs the phone number plugin enabled in Neon Auth.

Test plan: one fetchMock test per method checks the path, method and body. Further tests check that signIn.phoneNumber and verify create the session_data cookie, and that verify with disableSession sets no cookie. Auth tests (271) pass and typecheck and lint are clean.

This pull request and its description were written by Isaac.

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
neon-auth-nextjs Ready Ready Preview Oct 23, 2026 10:05pm UTC
react-neon-js Ready Ready Preview Oct 23, 2026 10:05pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
neon-auth-magic-link-example Ignored Ignored Preview Oct 23, 2026 10:05pm UTC
react-auth-external-ui Ignored Ignored Oct 23, 2026 10:05pm UTC

Request Review

@Shridhad Shridhad left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

self-review: 0 blockers / 0 should-fixes / 5 questions / 5 nits inline

context (not on a changed line):

  • packages/auth/src/server/client-factory.test.ts:123 — lastCall() reads mock.calls[0], which is the first call. fine today since these tests make one fetch, but a test that also mints a cookie does a second get-session fetch and this helper will silently check the wrong request. name doesn't match behavior.
  • packages/auth/src/server/client-factory.test.ts:170 — table covers path, POST and exact body for all five endpoints, and the cookie tests use real Set-Cookie plus get-session so that part is solid. but each suffix is copied by hand from endpoints.ts, so it's circular: a wrong path like phone-number/request-password-reset vs an older forget-password returns 404 in prod and every case here stays green. nothing compares against the real better-auth 1.7.6 plugin routes.
  • packages/auth/src/server/client-factory.test.ts:176 — endsWith(suffix) just echoes the endpoints.ts literal back, so a wrong path can't fail it. asserting the full /api/auth/phone-number/... pathname, or anchoring to the better-auth route, would actually catch something.
  • packages/auth/src/server/client-factory.test.ts:213 — good that the no-Set-Cookie case asserts a single fetch and no setCookie. but the test never sends disableSession, body is just { phoneNumber, code }. it only hits the generic setCookieHeaders.length > 0 gate at client-factory.ts:242, shared by every endpoint, so the name overstates what it checks for phone verify.
  • packages/auth/src/server/types.ts:46 — NeonAuthServer picks the client's whole phoneNumber type, but the proxy returns undefined for any key missing from API_ENDPOINTS.phoneNumber (client-factory.ts:408). does phoneNumberClient() in 1.7.6 expose any method beyond the four mapped here? if so it type-checks and throws not a function at runtime.

Comment thread packages/auth/src/server/client-factory.test.ts
Comment thread packages/auth/src/server/client-factory.test.ts Outdated
Comment thread packages/auth/src/server/endpoints.ts
Comment thread packages/auth/src/server/endpoints.ts
@Shridhad

Copy link
Copy Markdown
Collaborator Author

Review summary items, addressed in 0da1979:

  • lastCall is renamed to firstCall. It only ever served single-fetch cases, so no assertion was wrong, and it now checks toHaveBeenCalledTimes(1) first.
  • Paths are compared exactly instead of with endsWith.
  • The disableSession test now sends disableSession: true and asserts it's forwarded.

types.ts:46: phoneNumberClient() in 1.7.6 has no methods beyond the 5 mapped here, so nothing type-checks but then fails at runtime.

@Shridhad Shridhad left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

self-review: 3 questions / 2 nits inline

Comment thread packages/auth/src/server/client-factory.test.ts Outdated
Comment thread packages/auth/src/server/client-factory.test.ts
Comment thread packages/auth/src/server/client-factory.test.ts
Comment thread packages/auth/src/server/client-factory.test.ts
Comment thread packages/auth/src/server/endpoints.ts
Shridhad and others added 3 commits October 23, 2026 21:19
Adds signIn.phoneNumber and phoneNumber.{sendOtp,verify,requestPasswordReset,resetPassword}
to the server endpoint tree. Paths match better-auth 1.7.6 plugins/phone-number/routes.mjs.
Session cookies already flow through the existing fetchWithAuth minting path.

Co-authored-by: Isaac <no-reply@databricks.com>
- rename lastCall to firstCall and assert a single fetch before inspecting it
- compare the exact request pathname instead of endsWith
- send disableSession in the verify test and assert it is forwarded

Co-authored-by: Isaac <no-reply@databricks.com>
- Pin phone number request bodies to Better Auth's own zod schemas
- Add phoneNumber to the mocked session user
- Cover signIn.phoneNumber with no session cookie on the two-factor path

Co-authored-by: Isaac <no-reply@databricks.com>

@Shridhad Shridhad left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

self-review: 1 question / 2 nits inline

context (not on a changed line):

  • [question] packages/auth/src/server/client-factory.test.ts:149 — does phoneNumber.resetPassword set session cookies on success? signIn.phoneNumber has a test that cookies are forwarded, and this case has none.

Comment thread packages/auth/src/server/endpoints.test.ts Outdated
Comment thread packages/auth/src/server/endpoints.test.ts
Comment thread packages/auth/src/server/endpoints.test.ts
Co-authored-by: Isaac <no-reply@databricks.com>
@Shridhad

Copy link
Copy Markdown
Collaborator Author

Summary question from the last round: phoneNumber.resetPassword doesn't set session cookies. better-auth 1.7.6's /phone-number/reset-password returns { status: true } without creating a session, and it revokes sessions when revokeSessionsOnPasswordReset is on (plugins/phone-number/routes.mjs). So there's nothing to forward, and no cookie test is needed.

@Shridhad Shridhad left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

self-review: 1 question / 2 nits inline

Comment thread packages/auth/src/server/endpoints.test.ts

// Phone Number
phoneNumber: {
sendOtp: { path: 'phone-number/send-otp', method: 'POST' },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

[question]
🤔 fetchWithAuth forwards only Cookie, Origin and the proxy header, no X-Forwarded-For or User-Agent. so every sendOtp from a server action hits neon auth from the app's egress IP. does phone-number/send-otp rate-limit by phone number or by IP? if by IP, all users share one SMS budget and abuse can't be traced to a client, and sms costs money per message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good question. better-auth 1.7.6 rate-limits by client IP plus path (createRateLimitKey(ip, path)), and the phone-number plugin allows 10 requests per 60 seconds on /phone-number/*. Server methods send only Cookie, Origin and the proxy header, so calls from server actions all count against the app's egress IP. This applies to every createNeonAuth() server method (for example signIn.email), not just phone numbers. Fixing it means forwarding the client IP and the Neon Auth server trusting it, which is a separate change. Leaving this open for the maintainer to decide.

Co-authored-by: Isaac <no-reply@databricks.com>

@Shridhad Shridhad left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🖍️ redline (automated reviewer)

self-review: nothing blocking — no findings

This branch was successfully deployed

3 active deployments
Preview – neon-auth-nextjs — 1b414139 Deployed Oct 23, 2026 by vercel[bot]
Preview – react-neon-js — 1b414139 Deployed Oct 23, 2026 by vercel[bot]
production — 1b414139 Deployed Oct 23, 2026 by Shridhad via e2e (nextjs-neon-auth) #614
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.

1 participant