I am trying to implement an OAuth flow with Google in Next.js using Lucia. I am following the GitHub OAuth in Next.js App Router documentation, and converting it for use with Google. This question specifically relates to the Validate Callback
section.
When the user clicks a “sign up with Google” button, the server generates an authorization URL and redirects the user to it. If the user successfully signs in with Google, then Google makes the GET request to my sites /api/oauth/google
route.
This route will either create a new account for the user in my database and give them a session, or refresh their session if the user already exists. On success, the user gets redirected to /
.
My question is, how should errors be handled? Aside from creating a dedicated /errors
page, is there an idiomatic way to handle this situation? Currently the user just gets a blank screen with {"error":"msg here"}
/api/oauth/google
route:
export const GET = async (req: NextRequest) => {
try {
const url = new URL(req.url);
const searchParams = url.searchParams;
const code = accessSearchParams(searchParams, "code");
const state = accessSearchParams(searchParams, "state");
const codeVerifier = accessCookies("codeVerifier");
const savedState = accessCookies("state");
if (savedState !== state) {
throw new ValidationError("State does not match", 400);
}
const { accessToken, idToken, refreshToken, accessTokenExpiresAt } =
await google.validateAuthorizationCode(code, codeVerifier);
const googleRes = await fetch(
"https://www.googleapis.com/oauth2/v1/userinfo",
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
method: "GET",
}
);
const googleData = (await googleRes.json()) as GoogleUser;
await prisma.$transaction(async (tx) => {
const user = await tx.user.findFirst({
where: {
id: googleData.id,
},
});
if (!user) {
// Create the user
await prisma.user.create({
data: {
email: googleData.email,
id: googleData.id,
isEmailVerified: true,
name: googleData.name,
profilePictureUrl: googleData.picture,
},
});
// Create OAuth account
await tx.oauth_account.create({
data: {
id: googleData.id,
userId: googleData.id,
provider: "google",
providerUserId: googleData.id,
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: accessTokenExpiresAt,
},
});
} else {
// Update the OAuth account entry
await tx.oauth_account.update({
where: {
id: googleData.id,
},
data: {
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: accessTokenExpiresAt,
},
});
// Redirect them
return NextResponse.redirect(
new URL("/", process.env.NEXT_PUBLIC_BASE_URL),
{
status: 302,
}
);
}
});
// New user is now created, create a session for them
const session = await lucia.createSession(googleData.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);
cookies().set("state", "", {
expires: new Date(0),
});
cookies().set("codeVerifier", "", {
expires: new Date(0),
});
return NextResponse.redirect(
new URL("/", process.env.NEXT_PUBLIC_BASE_URL),
{
status: 302,
}
);
} catch (err: any) {
if (err instanceof ValidationError) {
return NextResponse.json({ error: err.message }, { status: err.code });
}
console.log(err);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
};