Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ implement google oauth #27

Merged
merged 10 commits into from
Feb 9, 2024
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ TBA

If you need credentials for the `.env` file, contact the project lead ([Minh](https://github.com/minhxNguyen7/)).

After changes to the .env file, run `pnpm run check` to update SvelteKit's auto-generated environment variable types.
After changes to the .env file, run `pnpm run check` to update SvelteKit's auto-generated environment variable types.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dependencies": {
"@iconify/json": "^2.2.160",
"@lucia-auth/adapter-prisma": "3.0.2",
"@lucia-auth/oauth": "^3.5.3",
"@prisma/client": "5.6.0",
"lucia": "2.7.4",
"svelty-picker": "^5.2.1",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified prisma/dev.db
Binary file not shown.
2 changes: 2 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ export const handle: Handle = async ({ event, resolve }) => {
if (event.locals?.auth) {
const session = await event.locals.auth.validate();
const user = session?.user;

if (user) {
event.locals.user = user;
}

if (event.route.id?.startsWith("/(protected)")) {
if (!user) throw redirect(302, "/auth");
// if (!user.verified) throw redirect(302, "/auth/verify/email");
Expand Down
19 changes: 19 additions & 0 deletions src/lib/server/lucia.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { prisma } from "@lucia-auth/adapter-prisma";
import { google } from "@lucia-auth/oauth/providers";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";

import { dev } from "$app/environment";
import {
GOOGLE_OAUTH_CLIENT_ID,
GOOGLE_OAUTH_CLIENT_SECRET,
GOOGLE_OAUTH_REDIRECT_URI,
} from "$env/static/private";
import { prisma as client } from "$lib/server/prisma";

export const auth = lucia({
Expand All @@ -23,8 +29,21 @@ export const auth = lucia({
verified: data.verified,
receiveEmail: data.receiveEmail,
token: data.token,

googleId: data.google_id,
username: data.username,
};
},
});

export const googleAuth = google(auth, {
clientId: GOOGLE_OAUTH_CLIENT_ID!,
clientSecret: GOOGLE_OAUTH_CLIENT_SECRET!,
redirectUri: GOOGLE_OAUTH_REDIRECT_URI!,
scope: [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
],
});

export type Auth = typeof auth;
9 changes: 9 additions & 0 deletions src/routes/auth/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
</div>
{/if}

<div class="mb-4">
<a
href="/auth/login/google"
class="mx-auto flex w-fit rounded-md bg-blue-500 p-2 font-semibold text-white"
>
Continue with Google
</a>
</div>

<TabGroup justify="justify-center">
<Tab bind:group={tabSet} name="signInTab" value={"signIn"}>Sign In</Tab>
<Tab bind:group={tabSet} name="signUpTab" value={"signUp"}>Sign Up</Tab>
Expand Down
31 changes: 31 additions & 0 deletions src/routes/auth/login/google/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { dev } from "$app/environment";
import { googleAuth } from "$lib/server/lucia";

export const GET = async ({ cookies, locals }) => {
const session = await locals.auth.validate();

if (session) {
return new Response(null, {
status: 302,
headers: {
Location: "/",
},
});
}
const [url, state] = await googleAuth.getAuthorizationUrl();

// Store state.
cookies.set("google_oauth_state", state, {
httpOnly: true,
secure: !dev,
path: "/",
maxAge: 30 * 24 * 60 * 60,
});

return new Response(null, {
status: 302,
headers: {
Location: url.toString(),
},
});
};
114 changes: 114 additions & 0 deletions src/routes/auth/login/google/callback/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { OAuthRequestError } from "@lucia-auth/oauth";
import type { GoogleUser } from "@lucia-auth/oauth/providers";

import { auth, googleAuth } from "$lib/server/lucia";

const getUser = async (googleUser: GoogleUser) => {
if (!googleUser.email) {
return null;
}

try {
const dbUser = await auth.getUser(googleUser.email);
if (dbUser) {
return dbUser;
}
Comment on lines +13 to +15
Copy link
Member

Choose a reason for hiding this comment

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

This should be outside of the try block

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a weird... quirk of Lucia (?) where auth.getUser() raises an error when passed a key not in the database.

The try block allows for a "quiet" handling of the not found case. I've attempted to find an alternate solution, but this piece of code isn't particularly offensive to me.

I can hunt down an alternate solution if you'd pref.

} catch (error) {
/* If a user cannot be found, an error is raised and caught here. */
KevinWu098 marked this conversation as resolved.
Show resolved Hide resolved
console.log("User not found in database", error);
}

const token = crypto.randomUUID();
const user = await auth.createUser({
userId: googleUser.email.toLowerCase(),
key: {
providerId: "google",
providerUserId: googleUser.email.toLowerCase(),
password: null,
},
attributes: {
email: googleUser.email.toLowerCase(),
firstName: googleUser.given_name ?? "",
lastName: googleUser.family_name ?? "",
// role: "USER",
verified: false,
receiveEmail: true,
token: token,
},
});

return user;
};

export const GET = async ({ url, cookies, locals }) => {
/**
* Check for a session. if it exists,
* redirect to a page of your liking.
*/
const session = await locals.auth.validate();
if (session) {
return new Response(null, {
status: 302,
headers: {
Location: "/auth",
},
});
}

/**
* Validate state of the request.
*/
const storedState = cookies.get("google_oauth_state") ?? null;
const state = url.searchParams.get("state");
const code = url.searchParams.get("code");
if (!storedState || !state || storedState !== state || !code) {
return new Response(null, {
status: 400,
});
}

try {
const { googleUser } = await googleAuth.validateCallback(code);
const user = await getUser(googleUser);

if (!user) {
/**
* You should probably redirect the user to a page and show a
* message that the account could not be created.
*
* This is a very rare case, but it can happen.
*/
return new Response(null, {
status: 500,
});
}

const session = await auth.createSession({
userId: user.userId,
attributes: {},
});

locals.auth.setSession(session);

return new Response(null, {
status: 302,
headers: {
Location: "/auth",
},
});
} catch (e) {
console.log(e);

// Invalid code.
if (e instanceof OAuthRequestError) {
return new Response(null, {
status: 400,
});
}

// All other errors.
return new Response(null, {
status: 500,
});
}
};
Loading