diff --git a/.env.example b/.env.example index 55924c1..59c7535 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,4 @@ DATABASE_URL=local.db AUTH_DOMAIN=auth.lab.cazzzer.com AUTH_CLIENT_ID= AUTH_CLIENT_SECRET= -AUTH_REDIRECT_URI= +AUTH_REDIRECT_URI=http://localhost:5173/auth/authentik/callback diff --git a/bun.lockb b/bun.lockb index 15cd4e3..9876da1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 114cfb3..8bc45b9 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", + "arctic": "^2.2.1", "better-sqlite3": "^11.1.2", "drizzle-orm": "^0.33.0", "lucide-svelte": "^0.454.0" diff --git a/src/lib/components/app/auth-form/auth-form.svelte b/src/lib/components/app/auth-form/auth-form.svelte index 1e4b3d3..e851b1a 100644 --- a/src/lib/components/app/auth-form/auth-form.svelte +++ b/src/lib/components/app/auth-form/auth-form.svelte @@ -6,8 +6,8 @@ let { class: className, ...rest }: {class: string | undefined | null, rest: { [p: string]: unknown }} = $props(); let isLoading = $state(false); - async function onSubmit(event: Event) { - event.preventDefault(); + async function onSubmit() { + // event.preventDefault(); isLoading = true; setTimeout(() => { @@ -17,12 +17,14 @@
- + + +
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 839907e..8a36ee4 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -3,6 +3,8 @@ 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 { dev } from '$app/environment'; const DAY_IN_MS = 1000 * 60 * 60 * 24; @@ -25,6 +27,16 @@ export async function createSession(userId: string): Promise { return session; } +export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) { + event.cookies.set(sessionCookieName, sessionId, { + path: '/', + sameSite: 'lax', + httpOnly: true, + expires: expiresAt, + secure: !dev, + }); +} + export async function invalidateSession(sessionId: string): Promise { await db.delete(table.session).where(eq(table.session.id, sessionId)); } @@ -33,7 +45,7 @@ export async function validateSession(sessionId: string) { const [result] = await db .select({ // Adjust user table here to tweak returned data - user: { id: table.user.id, username: table.user.username }, + user: { id: table.user.id, username: table.user.username, name: table.user.name }, session: table.session }) .from(table.session) diff --git a/src/lib/server/oauth.ts b/src/lib/server/oauth.ts new file mode 100644 index 0000000..9d11186 --- /dev/null +++ b/src/lib/server/oauth.ts @@ -0,0 +1,9 @@ +import { Authentik } from 'arctic'; +import * as env from '$env/static/private'; + +export const authentik = new Authentik( + env.AUTH_DOMAIN, + env.AUTH_CLIENT_ID, + env.AUTH_CLIENT_SECRET, + env.AUTH_REDIRECT_URI +); diff --git a/src/routes/auth/authentik/+server.ts b/src/routes/auth/authentik/+server.ts new file mode 100644 index 0000000..5810654 --- /dev/null +++ b/src/routes/auth/authentik/+server.ts @@ -0,0 +1,30 @@ +import { generateState, generateCodeVerifier } from "arctic"; +import { authentik } from "$lib/server/oauth"; + +import type { RequestEvent } from "@sveltejs/kit"; + +export async function GET(event: RequestEvent): Promise { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const url = authentik.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]); + + event.cookies.set("authentik_oauth_state", state, { + path: "/", + httpOnly: true, + maxAge: 60 * 10, // 10 minutes + sameSite: "lax" + }); + event.cookies.set("authentik_code_verifier", codeVerifier, { + path: "/", + httpOnly: true, + maxAge: 60 * 10, // 10 minutes + sameSite: "lax" + }); + + return new Response(null, { + status: 302, + headers: { + Location: url.toString() + } + }); +} diff --git a/src/routes/auth/authentik/callback/+server.ts b/src/routes/auth/authentik/callback/+server.ts new file mode 100644 index 0000000..791e8fa --- /dev/null +++ b/src/routes/auth/authentik/callback/+server.ts @@ -0,0 +1,78 @@ +import { createSession, setSessionTokenCookie } from "$lib/server/auth"; +import { authentik } from "$lib/server/oauth"; +import { decodeIdToken } from "arctic"; + +import type { RequestEvent } from "@sveltejs/kit"; +import type { OAuth2Tokens } from "arctic"; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; + +import * as table from '$lib/server/db/schema'; + +export async function GET(event: RequestEvent): Promise { + const code = event.url.searchParams.get("code"); + const state = event.url.searchParams.get("state"); + const storedState = event.cookies.get("authentik_oauth_state") ?? null; + const codeVerifier = event.cookies.get("authentik_code_verifier") ?? null; + if (code === null || state === null || storedState === null || codeVerifier === null) { + return new Response(null, { + status: 400 + }); + } + if (state !== storedState) { + return new Response(null, { + status: 400 + }); + } + + let tokens: OAuth2Tokens; + try { + tokens = await authentik.validateAuthorizationCode(code, codeVerifier); + } catch (e) { + // Invalid code or client credentials + return new Response(null, { + status: 400 + }); + } + const claims = decodeIdToken(tokens.idToken()); + console.log("claims", claims); + const userId: string = claims.sub; + const username: string = claims.preferred_username; + + const [existingUser] = await db.select().from(table.user).where(eq(table.user.id, userId)); + + if (existingUser) { + const session = await createSession(existingUser.id); + setSessionTokenCookie(event, session.id, session.expiresAt); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } + + const user: table.User = { + id: userId, + username, + name: claims.name as string, + }; + + try { + await db.insert(table.user).values(user); + const session = await createSession(user.id); + setSessionTokenCookie(event, session.id, session.expiresAt); + } catch (e) { + console.error('failed to create user', e); + return new Response(null, { + status: 500 + }); + } + + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); +}