Skip to content

Commit 4f8522a

Browse files
fix(auth): normalize JWT identity for Google-authenticated users (#201)
* fix(auth): normalize JWT identity for Google-authenticated users * chore(auth): removed comment and fixed lint issues * fix(auth): normalize email casing for Google sign-in * fix(auth): normalize email casing and ensure passwordSet selection for Google sign-in * fix(auth): guard against null return from findOneAndUpdate
1 parent fab46be commit 4f8522a

File tree

1 file changed

+39
-28
lines changed

1 file changed

+39
-28
lines changed

app/api/auth/[...nextauth]/options.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ export const authOptions: AuthOptions = {
8787
},
8888
}),
8989
],
90+
9091
pages: {
9192
signIn: '/auth/signin',
9293
error: '/auth/error',
9394
},
95+
9496
callbacks: {
9597
async signIn({ user, account }) {
9698
// Skip if not Google sign in or if email is missing
@@ -105,18 +107,20 @@ export const authOptions: AuthOptions = {
105107

106108
await dbConnect()
107109

110+
const normalizedEmail = user.email.toLowerCase()
111+
108112
// Check if user already exists
109113
const existingUser = (await User.findOne({
110-
email:
111-
typeof user.email === 'string' ? user.email.toLowerCase() : undefined,
114+
email: normalizedEmail,
112115
}).select('+passwordSet +roles')) as IUser | null
113116

117+
let dbUser: IUser | null = null
114118
let isNewUser = false
115119

116120
if (existingUser) {
117-
// Update Google ID if needed
121+
dbUser = existingUser
122+
118123
if (!existingUser.googleId && user.id) {
119-
// Use findOneAndUpdate to ensure atomic update
120124
const updatedUser = await User.findOneAndUpdate(
121125
{ email: existingUser.email },
122126
{
@@ -127,42 +131,45 @@ export const authOptions: AuthOptions = {
127131
},
128132
},
129133
{ new: true, runValidators: true }
130-
)
134+
).select('+passwordSet +roles')
131135

132-
// Ensure we have the latest user data with roles
133136
if (updatedUser) {
134-
user.roles = updatedUser.roles
137+
dbUser = updatedUser
135138
}
136-
} else {
137-
// If user already has Google ID, ensure roles are passed
138-
user.roles = existingUser.roles
139139
}
140-
// If user exists and has a password set, allow sign in
141-
if (existingUser.passwordSet) {
142-
return true
143-
}
144-
} else if (user.email) {
145-
// Create new user from Google auth
146-
await User.create({
140+
141+
} else {
142+
dbUser = await User.create({
147143
name: user.name,
148-
email: user.email,
149-
image: user.image || undefined, // Convert null to undefined
144+
email: normalizedEmail,
145+
image: user.image || undefined,
150146
googleId: user.id,
151147
passwordSet: false,
152-
emailVerified: true, // Google OAuth automatically verifies email
153-
roles: ['user'], // Default role for new users
148+
emailVerified: true,
149+
roles: ['user'],
154150
})
155151
isNewUser = true
156152
}
157153

158-
// Only redirect to set-password for new users or existing users without a password
159-
if (isNewUser || (existingUser && !existingUser.passwordSet)) {
160-
const email = user.email || ''
161-
return `/auth/set-password?email=${encodeURIComponent(email)}${isNewUser ? '&new=true' : ''}`
154+
if (dbUser && dbUser._id) {
155+
const mutableUser = user as {
156+
id?: string
157+
roles?: string[]
158+
}
159+
160+
mutableUser.id = dbUser._id.toString()
161+
mutableUser.roles = dbUser.roles
162+
}
163+
164+
if (isNewUser || (dbUser && !dbUser.passwordSet)) {
165+
return `/auth/set-password?email=${encodeURIComponent(normalizedEmail)}${
166+
isNewUser ? '&new=true' : ''
167+
}`
162168
}
163169

164170
return true
165171
},
172+
166173
async session({ session, token }) {
167174
if (token.sub && session.user) {
168175
session.user.id = token.sub
@@ -173,6 +180,7 @@ export const authOptions: AuthOptions = {
173180
}
174181
return session
175182
},
183+
176184
async jwt({ token, user }) {
177185
if (user) {
178186
token.sub = user.id
@@ -182,13 +190,16 @@ export const authOptions: AuthOptions = {
182190
return token
183191
},
184192
},
193+
185194
session: {
186195
strategy: 'jwt',
187-
maxAge: 24 * 60 * 60, // 24 hours
188-
updateAge: 60 * 60, // 1 hour
196+
maxAge: 24 * 60 * 60,
197+
updateAge: 60 * 60,
189198
},
199+
190200
jwt: {
191-
maxAge: 24 * 60 * 60, // 24 hours
201+
maxAge: 24 * 60 * 60,
192202
},
203+
193204
secret: process.env.NEXTAUTH_SECRET,
194205
}

0 commit comments

Comments
 (0)