diff --git a/__tests__/routes/betterAuth.ts b/__tests__/routes/betterAuth.ts index 3f4ace59a6..701f53230e 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.toThrow('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..ecb1422496 100644 --- a/src/betterAuth.ts +++ b/src/betterAuth.ts @@ -540,6 +540,19 @@ 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 + ) { + const err = new Error('github_email_not_verified'); + err.name = 'APIError'; + throw err; + } + const resolved = await resolveSignUpUserId(pool, user, ctx); resolved.data.id = await ensureNonDeletedUserId( pool,