Skip to content

Commit

Permalink
feat[auth]: add login with google (#35)
Browse files Browse the repository at this point in the history
* update package-lock.json

* add GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET env

* add continue to with google button

* add google provider

* add BASE_URL

* add auth controller

* clean up

* add login with google

* use link as button child
  • Loading branch information
thormengkheang authored Mar 21, 2024
1 parent d8b3679 commit 44bd032
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 66 deletions.
71 changes: 10 additions & 61 deletions src/app/login/github/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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;
Expand Down Expand Up @@ -39,61 +36,13 @@ export async function GET(request: Request): Promise<Response> {
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, {
Expand Down
80 changes: 80 additions & 0 deletions src/app/login/google/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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;
}
29 changes: 29 additions & 0 deletions src/app/login/google/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { google } from "@/lib/auth";
import { generateCodeVerifier, generateState } from "arctic";
import { cookies } from "next/headers";

export async function GET(): Promise<Response> {
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);
}
11 changes: 7 additions & 4 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ export default function LoginPage() {
<CardHeader>
<h2 className="text-xl font-bold">Sign In</h2>
</CardHeader>
<CardContent>
<Link href="/login/github">
<Button>Continue with Github</Button>
</Link>
<CardContent className="grid gap-4">
<Button asChild>
<Link href="/login/github">Continue with Github</Link>
</Button>
<Button asChild>
<Link href="/login/google">Continue with Google</Link>
</Button>
</CardContent>
</Card>
</div>
Expand Down
67 changes: 67 additions & 0 deletions src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -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
);
};
6 changes: 6 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
},
});
17 changes: 16 additions & 1 deletion src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -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> = T[keyof T];

export type Provider = ObjectValues<typeof PROVIDER>;

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",
Expand Down

0 comments on commit 44bd032

Please sign in to comment.