Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion src/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { USER_MESSAGES } from '@/configs/user-messages'
import { l } from '@/lib/clients/logger/logger'
import { createClient } from '@/lib/clients/supabase/server'
import { encodedRedirect } from '@/lib/utils/auth'
import { encodedRedirect, isOAuthEmailVerified } from '@/lib/utils/auth'
import { redirect } from 'next/navigation'
import { serializeError } from 'serialize-error'

Expand Down Expand Up @@ -57,6 +58,59 @@ export async function GET(request: Request) {
},
`OTP successfully exchanged for user session`
)

// Check email verification only for new users (signup)
// Existing users with linked identities can sign in with any provider
const isNewUser = data.user.identities?.length === 1
Copy link

Choose a reason for hiding this comment

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

Bug: OAuth Users Blocked by Incorrect New User Check

The isNewUser check, based on data.user.identities?.length === 1, incorrectly identifies existing users with a single OAuth identity as new. This subjects existing users to email verification checks, potentially blocking their sign-in if their OAuth provider's email verification status isn't met.

Fix in Cursor Fix in Web


if (isNewUser) {
const emailVerification = isOAuthEmailVerified(data.user)

if (!emailVerification.verified) {
l.warn(
{
key: 'auth_callback:email_not_verified',
user_id: data.user.id,
provider: emailVerification.provider,
reason: emailVerification.reason,
},
`User OAuth email not verified: ${emailVerification.reason}`
)

// Sign out the user since they don't have a verified email
await supabase.auth.signOut()

// Redirect with appropriate error message based on provider
let errorMessage: string
if (emailVerification.provider === 'google') {
errorMessage = USER_MESSAGES.googleEmailNotVerified.message
} else if (emailVerification.provider === 'github') {
errorMessage = USER_MESSAGES.githubEmailNotVerified.message
} else if (emailVerification.provider) {
errorMessage = USER_MESSAGES.oauthEmailNotVerified.message
} else {
errorMessage = USER_MESSAGES.genericEmailNotVerified.message
}

throw encodedRedirect('error', AUTH_URLS.SIGN_IN, errorMessage)
}

l.info(
{
key: 'auth_callback:email_verified',
user_id: data.user.id,
},
`User OAuth email verified successfully`
)
} else {
l.info(
{
key: 'auth_callback:existing_user',
user_id: data.user.id,
},
`Existing user sign-in, skipping email verification check`
)
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/configs/user-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ export const USER_MESSAGES = {
checkCredentials: {
message: 'Please check your credentials.',
},
googleEmailNotVerified: {
message:
'Your Google account email is not verified. Please verify your email with Google and try again.',
timeoutMs: 30000,
},
githubEmailNotVerified: {
message:
'Your GitHub email is not verified. Please verify your email with GitHub and try again.',
timeoutMs: 30000,
},
oauthEmailNotVerified: {
message:
'Your email is not verified. Please verify your email and try again.',
timeoutMs: 30000,
},
genericEmailNotVerified: {
message:
'Your email is not verified. Please verify your email and try again.',
timeoutMs: 30000,
},
}

export const getTimeoutMsFromUserMessage = (
Expand Down
77 changes: 77 additions & 0 deletions src/lib/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,80 @@ export function encodedRedirect(
export function getUserProviders(user: User) {
return user.app_metadata.providers as string[] | undefined
}

/**
* Checks if a user's email is verified based on their OAuth provider's identity data.
* Intended for use at signup when user has exactly one identity.
* @param {User} user - The Supabase user object
* @returns {{ verified: boolean, provider?: string, reason?: string }} - Verification status and details
*/
export function isOAuthEmailVerified(user: User): {
verified: boolean
provider?: string
reason?: string
} {
// Get the user's identities (OAuth providers they've signed in with)
const identities = user.identities || []

if (identities.length === 0) {
// Email/password user - consider verified if email_confirmed_at is set
return {
verified: !!user.email_confirmed_at,
reason: user.email_confirmed_at
? 'Email confirmed'
: 'Email not confirmed',
}
}

// Check the first identity (at signup, there's only one)
const identity = identities[0]
if (!identity) {
return { verified: false, reason: 'No identity found' }
}

const provider = identity.provider
const identityData = identity.identity_data

if (!identityData) {
return {
verified: false,
provider,
reason: 'No identity data available',
}
}

switch (provider) {
case 'google':
// Google provides email_verified field
// Require explicit true - fail closed if undefined/null/false
if (identityData.email_verified !== true) {
return {
verified: false,
provider: 'google',
reason: 'Google email not verified',
}
}
break

case 'github':
// GitHub provides verified field (for email verification)
// Note: GitHub returns the primary email's verification status
// Require explicit true - fail closed if undefined/null/false
if (identityData.verified !== true) {
return {
verified: false,
provider: 'github',
reason: 'GitHub email not verified',
}
}
break

// Add other providers as needed
default:
// For other OAuth providers, assume verified if they have an email
break
}

// If we get here, the check passed
return { verified: true, provider }
Copy link

Choose a reason for hiding this comment

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

Bug: OAuth Email Verification Bypass

The isOAuthEmailVerified function's default case for OAuth providers marks emails as verified without any actual check. This means users from unhandled providers are considered verified even if their email isn't, creating a security vulnerability.

Fix in Cursor Fix in Web

}
Loading