|
| 1 | +--- |
| 2 | +title: "Tutorial: Google OAuth in SvelteKit" |
| 3 | +--- |
| 4 | + |
| 5 | +# Tutorial: Google OAuth in SvelteKit |
| 6 | + |
| 7 | +_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._ |
| 8 | + |
| 9 | +An [example project](https://github.com/lucia-auth/example-sveltekit-google-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-sveltekit-google-oauth). |
| 10 | + |
| 11 | +``` |
| 12 | +git clone git@github.com:lucia-auth/example-sveltekit-google-oauth.git |
| 13 | +``` |
| 14 | + |
| 15 | +## Create an OAuth App |
| 16 | + |
| 17 | +Create an Google OAuth client on the Cloud Console. Set the redirect URI to `http://localhost:5173/login/google/callback`. Copy and paste the client ID and secret to your `.env` file. |
| 18 | + |
| 19 | +```bash |
| 20 | +# .env |
| 21 | +GOOGLE_CLIENT_ID="" |
| 22 | +GOOGLE_CLIENT_SECRET="" |
| 23 | +``` |
| 24 | + |
| 25 | +## Update database |
| 26 | + |
| 27 | +Update your user model to include the user's Google ID and username. |
| 28 | + |
| 29 | +```ts |
| 30 | +interface User { |
| 31 | + id: number; |
| 32 | + googleId: string; |
| 33 | + name: string; |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +## Setup Arctic |
| 38 | + |
| 39 | +``` |
| 40 | +npm install arctic |
| 41 | +``` |
| 42 | + |
| 43 | +Initialize the Google provider with the client ID, client secret, and redirect URI. |
| 44 | + |
| 45 | +```ts |
| 46 | +import { Google } from "arctic"; |
| 47 | +import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from "$env/static/private"; |
| 48 | + |
| 49 | +export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, "http://localhost:5173/login/google/callback"); |
| 50 | +``` |
| 51 | + |
| 52 | +## Sign in page |
| 53 | + |
| 54 | +Create `routes/login/+page.svelte` and add a basic sign in button, which should be a link to `/login/google`. |
| 55 | + |
| 56 | +```svelte |
| 57 | +<!-- routes/login/+page.svelte --> |
| 58 | +<h1>Sign in</h1> |
| 59 | +<a href="/login/google">Sign in with Google</a> |
| 60 | +``` |
| 61 | + |
| 62 | +## Create authorization URL |
| 63 | + |
| 64 | +Create an API route in `routes/login/google/+server.ts`. Generate a new state and code verifier, and create a new authorization URL. Add the `openid` and `profile` scope to have access to the user's profile later on. Store the state and code verifier, and redirect the user to the authorization URL. The user will be redirected to Google's sign in page. |
| 65 | + |
| 66 | +```ts |
| 67 | +// routes/login/google/+server.ts |
| 68 | +import { redirect } from "@sveltejs/kit"; |
| 69 | +import { generateState, generateCodeVerifier } from "arctic"; |
| 70 | +import { google } from "$lib/server/oauth"; |
| 71 | + |
| 72 | +import type { RequestEvent } from "@sveltejs/kit"; |
| 73 | + |
| 74 | +export async function GET(event: RequestEvent): Promise<Response> { |
| 75 | + const state = generateState(); |
| 76 | + const codeVerifier = generateCodeVerifier(); |
| 77 | + const url = await google.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]); |
| 78 | + |
| 79 | + event.cookies.set("google_oauth_state", state, { |
| 80 | + path: "/", |
| 81 | + httpOnly: true, |
| 82 | + maxAge: 60 * 10, // 10 minutes |
| 83 | + sameSite: "lax" |
| 84 | + }); |
| 85 | + event.cookies.set("google_code_verifier", codeVerifier, { |
| 86 | + path: "/", |
| 87 | + httpOnly: true, |
| 88 | + maxAge: 60 * 10, // 10 minutes |
| 89 | + sameSite: "lax" |
| 90 | + }); |
| 91 | + |
| 92 | + return new Response(null, { |
| 93 | + status: 302, |
| 94 | + headers: { |
| 95 | + Location: url.toString() |
| 96 | + } |
| 97 | + }); |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +## Validate callback |
| 102 | + |
| 103 | +Create an API route in `routes/login/google/callback/+server.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. If you passed the `openid` and `profile` scope, Google will return a token ID with the user's profile. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process. |
| 104 | + |
| 105 | +```ts |
| 106 | +// routes/login/google/callback/+server.ts |
| 107 | +import { generateSessionToken, createSession, setSessionTokenCookie } from "$lib/server/session"; |
| 108 | +import { google } from "$lib/server/oauth"; |
| 109 | + |
| 110 | +import type { RequestEvent } from "@sveltejs/kit"; |
| 111 | +import type { OAuth2Tokens } from "arctic"; |
| 112 | + |
| 113 | +export async function GET(event: RequestEvent): Promise<Response> { |
| 114 | + const code = event.url.searchParams.get("code"); |
| 115 | + const state = event.url.searchParams.get("state"); |
| 116 | + const storedState = event.cookies.get("google_oauth_state") ?? null; |
| 117 | + const codeVerifier = event.cookies.get("google_code_verifier") ?? null; |
| 118 | + if (code === null || state === null || storedState === null || codeVerifier === null) { |
| 119 | + return new Response(null, { |
| 120 | + status: 400 |
| 121 | + }); |
| 122 | + } |
| 123 | + if (state !== storedState) { |
| 124 | + return new Response(null, { |
| 125 | + status: 400 |
| 126 | + }); |
| 127 | + } |
| 128 | + |
| 129 | + let tokens: OAuth2Tokens; |
| 130 | + try { |
| 131 | + tokens = await google.validateAuthorizationCode(code, codeVerifier); |
| 132 | + } catch (e) { |
| 133 | + // Invalid code or client credentials |
| 134 | + return new Response(null, { |
| 135 | + status: 400 |
| 136 | + }); |
| 137 | + } |
| 138 | + const claims = decodeIdToken(tokens.idToken()); |
| 139 | + const googleUserId = claims.sub; |
| 140 | + const username = claims.name; |
| 141 | + |
| 142 | + // TODO: Replace this with your own DB query. |
| 143 | + const existingUser = await getUserFromGoogleId(googleUserId); |
| 144 | + |
| 145 | + if (existingUser !== null) { |
| 146 | + const token = generateSessionToken(); |
| 147 | + const session = await createSession(token, existingUser.id); |
| 148 | + setSessionTokenCookie(event, token, session.expiresAt); |
| 149 | + return new Response(null, { |
| 150 | + status: 302, |
| 151 | + headers: { |
| 152 | + Location: "/" |
| 153 | + } |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + // TODO: Replace this with your own DB query. |
| 158 | + const user = await createUser(googleUserId, username); |
| 159 | + |
| 160 | + const sessionToken = generateSessionToken(); |
| 161 | + const session = await createSession(sessionToken, user.id); |
| 162 | + setSessionTokenCookie(event, token, session.expiresAt); |
| 163 | + return new Response(null, { |
| 164 | + status: 302, |
| 165 | + headers: { |
| 166 | + Location: "/" |
| 167 | + } |
| 168 | + }); |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +## Get the current user |
| 173 | + |
| 174 | +If you implemented the middleware outlined in the [Session cookies in SvelteKit](/sessions/cookies/sveltekit) page, you can get the current session and user from `Locals`. |
| 175 | + |
| 176 | +```ts |
| 177 | +// routes/+page.server.ts |
| 178 | +import { redirect } from "@sveltejs/kit"; |
| 179 | + |
| 180 | +import type { PageServerLoad } from "./$types"; |
| 181 | + |
| 182 | +export const load: PageServerLoad = async (event) => { |
| 183 | + if (!event.locals.user) { |
| 184 | + return redirect(302, "/login"); |
| 185 | + } |
| 186 | + |
| 187 | + return { |
| 188 | + user |
| 189 | + }; |
| 190 | +}; |
| 191 | +``` |
| 192 | + |
| 193 | +## Sign out |
| 194 | + |
| 195 | +Sign out users by invalidating their session. Make sure to remove the session cookie as well. |
| 196 | + |
| 197 | +```ts |
| 198 | +// routes/+page.server.ts |
| 199 | +import { fail, redirect } from "@sveltejs/kit"; |
| 200 | +import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/session"; |
| 201 | + |
| 202 | +import type { Actions, PageServerLoad } from "./$types"; |
| 203 | + |
| 204 | +export const load: PageServerLoad = async ({ locals }) => { |
| 205 | + // ... |
| 206 | +}; |
| 207 | + |
| 208 | +export const actions: Actions = { |
| 209 | + default: async (event) => { |
| 210 | + if (event.locals.session === null) { |
| 211 | + return fail(401); |
| 212 | + } |
| 213 | + await invalidateSession(event.locals.session.id); |
| 214 | + deleteSessionTokenCookie(event); |
| 215 | + return redirect(302, "/login"); |
| 216 | + } |
| 217 | +}; |
| 218 | +``` |
| 219 | + |
| 220 | +```svelte |
| 221 | +<!-- routes/+page.svelte --> |
| 222 | +<script lang="ts"> |
| 223 | + import { enhance } from "$app/forms"; |
| 224 | +</script> |
| 225 | +
|
| 226 | +<form method="post" use:enhance> |
| 227 | + <button>Sign out</button> |
| 228 | +</form> |
| 229 | +``` |
0 commit comments