From f9a27cbbb7c5655cd23a5ce51f935a769885e4f3 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Sat, 12 Apr 2025 01:34:07 -0700 Subject: [PATCH] WIP: auth: refactor to page routes instead of api routes --- README.md | 4 + src/lib/server/auth.ts | 21 ++-- src/routes/auth/+page.server.ts | 18 +-- src/routes/auth/authentik/callback/+server.ts | 4 +- .../auth/google/callback/+page.server.ts | 58 +++------- src/routes/auth/google/callback/+server.ts | 109 ------------------ 6 files changed, 47 insertions(+), 167 deletions(-) delete mode 100644 src/routes/auth/google/callback/+server.ts diff --git a/README.md b/README.md index c8f1cd6..e1b2020 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,8 @@ bun run dev - [ ] Proper invite page - [ ] Proper error page for login without invite - [ ] Support file provider (for wg-quick) +- [ ] Fix accessibility issues (lighthouse) +- [ ] Add title to profile page (lighthouse) - [ ] wg-quick scripts (maybe?) +- [ ] Get rid of api routes (maybe?) +- [ ] Get rid of custom result types (maybe?) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index f7a56f9..5caf886 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -3,7 +3,7 @@ import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; import { db } from '$lib/server/db'; import * as table from '$lib/server/db/schema'; -import type { RequestEvent } from '@sveltejs/kit'; +import type { Cookies } from '@sveltejs/kit'; import { dev } from '$app/environment'; import { env } from '$env/dynamic/private'; @@ -22,14 +22,14 @@ export async function createSession(userId: string): Promise { const session: table.Session = { id: sessionId, userId, - expiresAt: new Date(Date.now() + DAY_IN_MS * 30) + expiresAt: new Date(Date.now() + DAY_IN_MS * 30), }; await db.insert(table.sessions).values(session); return session; } -export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) { - event.cookies.set(sessionCookieName, sessionId, { +export function setSessionTokenCookie(cookies: Cookies, sessionId: string, expiresAt: Date) { + cookies.set(sessionCookieName, sessionId, { path: '/', sameSite: 'lax', httpOnly: true, @@ -42,16 +42,21 @@ export async function invalidateSession(sessionId: string): Promise { await db.delete(table.sessions).where(eq(table.sessions.id, sessionId)); } -export function deleteSessionTokenCookie(event: RequestEvent) { - event.cookies.delete(sessionCookieName, { path: '/' }); +export function deleteSessionTokenCookie(cookies: Cookies) { + cookies.delete(sessionCookieName, { path: '/' }); } export async function validateSession(sessionId: string) { const [result] = await db .select({ // Adjust user table here to tweak returned data - user: { id: table.users.id, authSource: table.users.authSource, username: table.users.username, name: table.users.name }, - session: table.sessions + user: { + id: table.users.id, + authSource: table.users.authSource, + username: table.users.username, + name: table.users.name, + }, + session: table.sessions, }) .from(table.sessions) .innerJoin(table.users, eq(table.sessions.userId, table.users.id)) diff --git a/src/routes/auth/+page.server.ts b/src/routes/auth/+page.server.ts index 7f23ea4..850472c 100644 --- a/src/routes/auth/+page.server.ts +++ b/src/routes/auth/+page.server.ts @@ -1,14 +1,14 @@ -import { fail, redirect } from "@sveltejs/kit"; -import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth"; -import type { Actions } from "./$types"; +import { fail, redirect } from '@sveltejs/kit'; +import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth'; +import type { Actions } from './$types'; export const actions: Actions = { - logout: async (event) => { - if (event.locals.session === null) { + logout: async ({ locals, cookies }) => { + if (locals.session === null) { return fail(401); } - await invalidateSession(event.locals.session.id); - deleteSessionTokenCookie(event); - return redirect(302, "/"); - } + await invalidateSession(locals.session.id); + deleteSessionTokenCookie(cookies); + redirect(302, '/'); + }, }; diff --git a/src/routes/auth/authentik/callback/+server.ts b/src/routes/auth/authentik/callback/+server.ts index 4a8ffd2..8519772 100644 --- a/src/routes/auth/authentik/callback/+server.ts +++ b/src/routes/auth/authentik/callback/+server.ts @@ -48,7 +48,7 @@ export async function GET(event: RequestEvent): Promise { if (existingUser) { const session = await createSession(existingUser.id); - setSessionTokenCookie(event, session.id, session.expiresAt); + setSessionTokenCookie(event.cookies, session.id, session.expiresAt); return new Response(null, { status: 302, headers: { @@ -67,7 +67,7 @@ export async function GET(event: RequestEvent): Promise { try { await db.insert(table.users).values(user); const session = await createSession(user.id); - setSessionTokenCookie(event, session.id, session.expiresAt); + setSessionTokenCookie(event.cookies, session.id, session.expiresAt); } catch (e) { console.error('failed to create user', e); return new Response(null, { diff --git a/src/routes/auth/google/callback/+page.server.ts b/src/routes/auth/google/callback/+page.server.ts index 57a8fed..aad3664 100644 --- a/src/routes/auth/google/callback/+page.server.ts +++ b/src/routes/auth/google/callback/+page.server.ts @@ -1,15 +1,17 @@ -import { error } from '@sveltejs/kit'; -import * as arctic from 'arctic'; -import { google } from '$lib/server/oauth'; -import { db } from '$lib/server/db'; -import { eq } from 'drizzle-orm'; -import * as table from '$lib/server/db/schema'; import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import { google } from '$lib/server/oauth'; +import { error, redirect } from '@sveltejs/kit'; import type { OAuth2Tokens } from 'arctic'; +import * as arctic from 'arctic'; +import { eq } from 'drizzle-orm'; import { assertGuard } from 'typia'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ url, cookies }) => { +export const load: PageServerLoad = async (event) => { + const { url, cookies } = event; + const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const storedState = cookies.get('google_oauth_state') ?? null; @@ -17,15 +19,12 @@ export const load: PageServerLoad = async ({ url, cookies }) => { if (code === null || state === null || storedState === null || codeVerifier === null) { error(400, 'Missing url parameters'); - return; } const stateGeneratedToken = state.slice(0, storedState.length); const stateInviteToken = state.slice(storedState.length); if (stateGeneratedToken !== storedState) { - return new Response(null, { - status: 400, - }); + error(400, 'Invalid state in url'); } let tokens: OAuth2Tokens; @@ -34,20 +33,14 @@ export const load: PageServerLoad = async ({ url, cookies }) => { } catch (e) { if (e instanceof arctic.OAuth2RequestError) { console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e); - return new Response(null, { - status: 400, - }); + throw error(400, 'Invalid authorization code'); } if (e instanceof arctic.ArcticFetchError) { console.debug('Arctic: failed to call `fetch()`', e); - return new Response(null, { - status: 400, - }); + error(400, 'Failed to validate authorization code'); } - - return new Response(null, { - status: 500, - }); + console.error('Unxepcted error validating authorization code', code, e); + error(500); } const idToken = tokens.idToken(); @@ -66,22 +59,14 @@ export const load: PageServerLoad = async ({ url, cookies }) => { if (existingUser) { const session = await createSession(existingUser.id); - setSessionTokenCookie(event, session.id, session.expiresAt); - return new Response(null, { - status: 302, - headers: { - Location: '/', - }, - }); + setSessionTokenCookie(cookies, session.id, session.expiresAt); + redirect(302, '/'); } if (!isValidInviteToken(stateInviteToken)) { const message = stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link'; - - return new Response('Not Authorized: ' + message, { - status: 403, - }); + error(403, 'Not Authorized: ' + message); } const user: table.User = { @@ -97,12 +82,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { const session = await createSession(user.id); - setSessionTokenCookie(event, session.id, session.expiresAt); + setSessionTokenCookie(cookies, session.id, session.expiresAt); - return new Response(null, { - status: 302, - headers: { - Location: '/', - }, - }); + redirect(302, '/'); }; diff --git a/src/routes/auth/google/callback/+server.ts b/src/routes/auth/google/callback/+server.ts deleted file mode 100644 index f389092..0000000 --- a/src/routes/auth/google/callback/+server.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { RequestHandler } from './$types'; -import * as arctic from 'arctic'; -import { google } from '$lib/server/oauth'; -import { db } from '$lib/server/db'; -import { eq } from 'drizzle-orm'; -import * as table from '$lib/server/db/schema'; -import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth'; -import type { OAuth2Tokens } from 'arctic'; -import { assertGuard } from 'typia'; - -export const GET: RequestHandler = async (event) => { - const { url, cookies } = event; - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const storedState = cookies.get('google_oauth_state') ?? null; - const codeVerifier = cookies.get('google_code_verifier') ?? null; - - if (code === null || state === null || storedState === null || codeVerifier === null) { - return new Response(null, { - status: 400, - }); - } - - const stateGeneratedToken = state.slice(0, storedState.length); - const stateInviteToken = state.slice(storedState.length); - if (stateGeneratedToken !== storedState) { - return new Response(null, { - status: 400, - }); - } - - let tokens: OAuth2Tokens; - try { - tokens = await google.validateAuthorizationCode(code, codeVerifier); - } catch (e) { - if (e instanceof arctic.OAuth2RequestError) { - console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e); - return new Response(null, { - status: 400, - }); - } - if (e instanceof arctic.ArcticFetchError) { - console.debug('Arctic: failed to call `fetch()`', e); - return new Response(null, { - status: 400, - }); - } - - return new Response(null, { - status: 500, - }); - } - - const idToken = tokens.idToken(); - const claims = arctic.decodeIdToken(idToken); - - console.log('claims', claims); - - assertGuard<{ - sub: string; - email: string; - name: string; - }>(claims); - - const userId = claims.sub; - const existingUser = await db.query.users.findFirst({ where: eq(table.users.id, userId) }); - - if (existingUser) { - const session = await createSession(existingUser.id); - setSessionTokenCookie(event, session.id, session.expiresAt); - return new Response(null, { - status: 302, - headers: { - Location: '/', - }, - }); - } - - if (!isValidInviteToken(stateInviteToken)) { - const message = - stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link'; - - return new Response('Not Authorized: ' + message, { - status: 403, - }); - } - - const user: table.User = { - id: userId, - authSource: 'google', - username: claims.email, - name: claims.name, - }; - - // TODO: proper error handling, delete cookies - await db.insert(table.users).values(user); - console.log('created user', user, 'with invite token', stateInviteToken); - - const session = await createSession(user.id); - - setSessionTokenCookie(event, session.id, session.expiresAt); - - return new Response(null, { - status: 302, - headers: { - Location: '/', - }, - }); -};