diff --git a/src/app/login/github/callback/route.ts b/src/app/login/github/callback/route.ts index 882bf77f..25354ee1 100644 --- a/src/app/login/github/callback/route.ts +++ b/src/app/login/github/callback/route.ts @@ -1,15 +1,12 @@ -import { github, lucia } from "@/lib/auth"; -import { cookies, headers } from "next/headers"; +import { PROVIDER, github } from "@/lib/auth"; +import { cookies } from "next/headers"; import { OAuth2RequestError } from "arctic"; -import { generateId } from "lucia"; -import { db } from "@/db"; -import { user, user_oauth } from "@/db/schema"; +import * as AuthController from "@/controllers/auth"; export async function GET(request: Request): Promise { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); - const headerStore = headers(); const GITHUB_API_URL = "https://api.github.com"; const storedState = cookies().get("github_oauth_state")?.value ?? null; @@ -39,61 +36,13 @@ export async function GET(request: Request): Promise { githubEmails.find((email) => email.primary)?.email || null; } - // Replace this with your own DB client. - const existingUser = await db.query.user_oauth.findFirst({ - where: (field, op) => - op.and( - op.eq(field.provider, "GITHUB"), - op.eq(field.providerId, githubUser.id) - ), - }); - - if (existingUser?.userId) { - const session = await lucia.createSession(existingUser.userId, { - auth_id: existingUser.id, - user_agent: headerStore.get("user-agent"), - }); - - const sessionCookie = lucia.createSessionCookie(session.id); - - cookies().set( - sessionCookie.name, - sessionCookie.value, - sessionCookie.attributes - ); - - return new Response(null, { - status: 302, - headers: { - Location: "/connect", - }, - }); - } - - const userId = generateId(15); - const authId = generateId(15); - - await db - .insert(user) - .values({ id: userId, name: githubUser.login, email: githubUser.email }); - await db.insert(user_oauth).values({ - id: authId, - provider: "GITHUB", - providerId: githubUser.id, - userId: userId, - }); - - const session = await lucia.createSession(userId, { - auth_id: userId, - user_agent: headerStore.get("user-agent"), - }); - - const sessionCookie = lucia.createSessionCookie(session.id); - - cookies().set( - sessionCookie.name, - sessionCookie.value, - sessionCookie.attributes + await AuthController.save( + { + id: githubUser.id, + name: githubUser.login, + email: githubUser.email || "", + }, + PROVIDER.GITHUB ); return new Response(null, { diff --git a/src/app/login/google/callback/route.ts b/src/app/login/google/callback/route.ts new file mode 100644 index 00000000..d11d730b --- /dev/null +++ b/src/app/login/google/callback/route.ts @@ -0,0 +1,80 @@ +import { PROVIDER, google } from "@/lib/auth"; +import { cookies } from "next/headers"; +import { OAuth2RequestError } from "arctic"; +import * as AuthController from "@/controllers/auth"; + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + const storedState = cookies().get("google_oauth_state")?.value ?? null; + const storedCodeVerifier = + cookies().get("google_oauth_code_verifier")?.value ?? null; + if ( + !code || + !state || + !storedState || + !storedCodeVerifier || + state !== storedState + ) { + return new Response(null, { + status: 400, + }); + } + + try { + const token = await google.validateAuthorizationCode( + code, + storedCodeVerifier + ); + + const resp = await fetch( + "https://openidconnect.googleapis.com/v1/userinfo", + { + headers: { + Authorization: `Bearer ${token.accessToken}`, + }, + } + ); + const googleUser: GoogleUser = await resp.json(); + + await AuthController.save( + { + id: googleUser.sub, + name: googleUser.name, + email: googleUser.email, + }, + PROVIDER.GOOGLE + ); + + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } catch (e) { + console.error(e); + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response(null, { + status: 400, + }); + } + return new Response(null, { + status: 500, + }); + } +} + +interface GoogleUser { + sub: string; + name: string; + given_name: string; + picture: string; + email: string; + email_verified: boolean; + locale: string; +} diff --git a/src/app/login/google/route.ts b/src/app/login/google/route.ts new file mode 100644 index 00000000..49dfc7c7 --- /dev/null +++ b/src/app/login/google/route.ts @@ -0,0 +1,29 @@ +import { google } from "@/lib/auth"; +import { generateCodeVerifier, generateState } from "arctic"; +import { cookies } from "next/headers"; + +export async function GET(): Promise { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const url = await google.createAuthorizationURL(state, codeVerifier, { + scopes: ["profile", "email"], + }); + + cookies().set("google_oauth_state", state, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }); + + cookies().set("google_oauth_code_verifier", codeVerifier, { + path: "/", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 60 * 10, + sameSite: "lax", + }); + + return Response.redirect(url); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index b5535d64..2968ae1c 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -10,10 +10,13 @@ export default function LoginPage() {

Sign In

- - - - + + + diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts new file mode 100644 index 00000000..13baf314 --- /dev/null +++ b/src/controllers/auth.ts @@ -0,0 +1,67 @@ +import { db } from "@/db"; +import { user, user_oauth } from "@/db/schema"; +import { Provider, lucia } from "@/lib/auth"; +import { generateId } from "lucia"; +import { cookies, headers } from "next/headers"; + +interface UserAuth { + id: string; + name: string; + email: string; +} + +export const save = async (data: UserAuth, provider: Provider) => { + const { id, name, email } = data; + const headerStore = headers(); + + const existingUser = await db.query.user_oauth.findFirst({ + where: (field, op) => + op.and(op.eq(field.provider, provider), op.eq(field.providerId, id)), + }); + + if (existingUser?.userId) { + const session = await lucia.createSession(existingUser.userId, { + auth_id: existingUser.id, + user_agent: headerStore.get("user-agent"), + }); + + const sessionCookie = lucia.createSessionCookie(session.id); + + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); + + return new Response(null, { + status: 302, + headers: { + Location: "/connect", + }, + }); + } + + const userId = generateId(15); + const authId = generateId(15); + + await db.insert(user).values({ id: userId, name, email }); + await db.insert(user_oauth).values({ + id: authId, + provider: provider, + providerId: id, + userId: userId, + }); + + const session = await lucia.createSession(userId, { + auth_id: userId, + user_agent: headerStore.get("user-agent"), + }); + + const sessionCookie = lucia.createSessionCookie(session.id); + + cookies().set( + sessionCookie.name, + sessionCookie.value, + sessionCookie.attributes + ); +}; diff --git a/src/env.ts b/src/env.ts index 55c0fac6..7ef07582 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,12 +7,16 @@ export const appVersion = pkg.version; export const env = createEnv({ server: { + BASE_URL: z.string().min(1), DATABASE_URL: z.string().min(1), DATABASE_AUTH_TOKEN: z.string().min(1), GITHUB_CLIENT_ID: z.string().min(1), GITHUB_CLIENT_SECRET: z.string().min(1), + GOOGLE_CLIENT_ID: z.string().min(1), + GOOGLE_CLIENT_SECRET: z.string().min(1), + ENCRYPTION_KEY: z.string().min(30), }, experimental__runtimeEnv: { @@ -21,5 +25,7 @@ export const env = createEnv({ GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, }, }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 610d53e5..db15ce13 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,16 +1,31 @@ import { Lucia } from "lucia"; -import { GitHub } from "arctic"; +import { GitHub, Google } from "arctic"; import { LibSQLAdapter } from "@lucia-auth/adapter-sqlite"; import { connection } from "@/db"; import { env } from "@/env"; import { cache } from "react"; import { cookies, headers } from "next/headers"; +export const PROVIDER = { + GITHUB: "GITHUB", + GOOGLE: "GOOGLE", +} as const; + +type ObjectValues = T[keyof T]; + +export type Provider = ObjectValues; + export const github = new GitHub( env.GITHUB_CLIENT_ID, env.GITHUB_CLIENT_SECRET ); +export const google = new Google( + env.GOOGLE_CLIENT_ID, + env.GOOGLE_CLIENT_SECRET, + `${env.BASE_URL}/login/google/callback` +); + const adapter = new LibSQLAdapter(connection, { user: "user", session: "user_session",