From 606a8b6d9169dce7c14acbabdd61624eeb2c8ebf Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 26 May 2026 11:38:02 +0200 Subject: [PATCH 1/4] fix: ensure only verified github emails --- __tests__/routes/betterAuth.ts | 60 ++++++++++++++++++++++++++++++++++ src/betterAuth.ts | 11 +++++++ 2 files changed, 71 insertions(+) diff --git a/__tests__/routes/betterAuth.ts b/__tests__/routes/betterAuth.ts index 3f4ace59a6..f381bbb939 100644 --- a/__tests__/routes/betterAuth.ts +++ b/__tests__/routes/betterAuth.ts @@ -129,6 +129,66 @@ describe('betterAuth routes', () => { return before; }; + it('should reject GitHub OAuth sign-ups when email is not verified', async () => { + const before = await getBeforeHook(); + + await expect( + before( + { + email: 'unverified@github.example', + name: 'Unverified GitHub User', + emailVerified: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + request: new Request('http://localhost/auth/callback/github'), + body: {}, + }, + ), + ).rejects.toMatchObject({ message: 'github_email_not_verified' }); + }); + + it('should allow GitHub OAuth sign-ups when email is verified', async () => { + const before = await getBeforeHook(); + + const result = await before( + { + email: 'verified@github.example', + name: 'Verified GitHub User', + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + request: new Request('http://localhost/auth/callback/github'), + body: {}, + }, + ); + + expect((result as { data: { id: string } }).data.id).toBeDefined(); + }); + + it('should not reject email/password sign-ups when email is unverified', async () => { + const before = await getBeforeHook(); + + const result = await before( + { + email: 'pending@example.com', + name: 'Pending User', + emailVerified: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + request: new Request('http://localhost/auth/sign-up/email'), + body: {}, + }, + ); + + expect((result as { data: { id: string } }).data.id).toBeDefined(); + }); + it('should regenerate id when tracking cookie matches a deleted user', async () => { const before = await getBeforeHook(); const deletedUserId = 'aBcDeFgHiJkLmNoPqRsTu'; diff --git a/src/betterAuth.ts b/src/betterAuth.ts index b0434ae306..99ef966838 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -540,6 +540,17 @@ export const getBetterAuthOptions = (pool: Pool): BetterAuthOptions => { user: { create: { before: async (user, ctx) => { + const hookCtx = ctx as BetterAuthDbHookContext; + const requestPath = hookCtx?.request?.url + ? new URL(hookCtx.request.url).pathname + : ''; + if ( + requestPath.endsWith('/callback/github') && + user.emailVerified === false + ) { + throwBadRequest('github_email_not_verified'); + } + const resolved = await resolveSignUpUserId(pool, user, ctx); resolved.data.id = await ensureNonDeletedUserId( pool, From ca27aff395de97488c77ac9859a891407a272ca1 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 26 May 2026 12:50:54 +0200 Subject: [PATCH 2/4] fix: ensure only verified github emails --- __tests__/routes/betterAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/routes/betterAuth.ts b/__tests__/routes/betterAuth.ts index f381bbb939..701f53230e 100644 --- a/__tests__/routes/betterAuth.ts +++ b/__tests__/routes/betterAuth.ts @@ -146,7 +146,7 @@ describe('betterAuth routes', () => { body: {}, }, ), - ).rejects.toMatchObject({ message: 'github_email_not_verified' }); + ).rejects.toThrow('github_email_not_verified'); }); it('should allow GitHub OAuth sign-ups when email is verified', async () => { From d6779a2d8a7fe9173fe95421d73f5312711296a0 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 26 May 2026 13:24:57 +0200 Subject: [PATCH 3/4] fix: ensure only verified github emails --- src/betterAuth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/betterAuth.ts b/src/betterAuth.ts index 99ef966838..a579c58494 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -1,5 +1,5 @@ -import { betterAuth, type BetterAuthOptions } from 'better-auth'; -import { APIError, createAuthMiddleware, getOAuthState } from 'better-auth/api'; +import { APIError, betterAuth, type BetterAuthOptions } from 'better-auth'; +import { createAuthMiddleware, getOAuthState } from 'better-auth/api'; import { captcha, emailOTP } from 'better-auth/plugins'; import type { Pool } from 'pg'; import * as argon2 from 'argon2'; From 4294ab416596b19629dcd6dbb7f0fcbacc772530 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 26 May 2026 13:39:34 +0200 Subject: [PATCH 4/4] fix: ensure only verified github emails --- src/betterAuth.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/betterAuth.ts b/src/betterAuth.ts index a579c58494..ecb1422496 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -1,5 +1,5 @@ -import { APIError, betterAuth, type BetterAuthOptions } from 'better-auth'; -import { createAuthMiddleware, getOAuthState } from 'better-auth/api'; +import { betterAuth, type BetterAuthOptions } from 'better-auth'; +import { APIError, createAuthMiddleware, getOAuthState } from 'better-auth/api'; import { captcha, emailOTP } from 'better-auth/plugins'; import type { Pool } from 'pg'; import * as argon2 from 'argon2'; @@ -548,7 +548,9 @@ export const getBetterAuthOptions = (pool: Pool): BetterAuthOptions => { requestPath.endsWith('/callback/github') && user.emailVerified === false ) { - throwBadRequest('github_email_not_verified'); + const err = new Error('github_email_not_verified'); + err.name = 'APIError'; + throw err; } const resolved = await resolveSignUpUserId(pool, user, ctx);