Compare commits

..

1 Commits

Author SHA1 Message Date
053cfa567c
WIP: auth: improve handling of invite tokens 2025-04-08 22:06:52 -07:00
7 changed files with 61 additions and 73 deletions

View File

@ -1,10 +0,0 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = tab
indent_size = 2
max_line_length = 100
quote_type = single
trim_trailing_whitespace = true

View File

@ -1,7 +0,0 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"formatter": "prettier"
}

View File

@ -18,7 +18,7 @@ and [wg-quick](https://www.wireguard.com/quickstart/) for standalone setups.
## Development
Development uses bun.
An additional prepare step is needed to set up typia for type validation.
An additional prepare step is needed to set up typia for type validation.
For example .env settings, see [.env.example](.env.example)
@ -27,14 +27,3 @@ bun install
bun run prepare
bun run dev
```
## To Do
- [ ] 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?)

View File

@ -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 { Cookies } from '@sveltejs/kit';
import type { RequestEvent } 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<table.Session> {
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(cookies: Cookies, sessionId: string, expiresAt: Date) {
cookies.set(sessionCookieName, sessionId, {
export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, sessionId, {
path: '/',
sameSite: 'lax',
httpOnly: true,
@ -42,21 +42,16 @@ export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
}
export function deleteSessionTokenCookie(cookies: Cookies) {
cookies.delete(sessionCookieName, { path: '/' });
export function deleteSessionTokenCookie(event: RequestEvent) {
event.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))

View File

@ -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 ({ locals, cookies }) => {
if (locals.session === null) {
logout: async (event) => {
if (event.locals.session === null) {
return fail(401);
}
await invalidateSession(locals.session.id);
deleteSessionTokenCookie(cookies);
redirect(302, '/');
},
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, "/");
}
};

View File

@ -48,7 +48,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
setSessionTokenCookie(event, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
@ -67,7 +67,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
try {
await db.insert(table.users).values(user);
const session = await createSession(user.id);
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
setSessionTokenCookie(event, session.id, session.expiresAt);
} catch (e) {
console.error('failed to create user', e);
return new Response(null, {

View File

@ -1,30 +1,32 @@
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 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';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
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) {
error(400, 'Missing url parameters');
return new Response(null, {
status: 400,
});
}
const stateGeneratedToken = state.slice(0, storedState.length);
const stateInviteToken = state.slice(storedState.length);
if (stateGeneratedToken !== storedState) {
error(400, 'Invalid state in url');
return new Response(null, {
status: 400,
});
}
let tokens: OAuth2Tokens;
@ -33,14 +35,20 @@ export const load: PageServerLoad = async (event) => {
} catch (e) {
if (e instanceof arctic.OAuth2RequestError) {
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
throw error(400, 'Invalid authorization code');
return new Response(null, {
status: 400,
});
}
if (e instanceof arctic.ArcticFetchError) {
console.debug('Arctic: failed to call `fetch()`', e);
error(400, 'Failed to validate authorization code');
return new Response(null, {
status: 400,
});
}
console.error('Unxepcted error validating authorization code', code, e);
error(500);
return new Response(null, {
status: 500,
});
}
const idToken = tokens.idToken();
@ -59,14 +67,22 @@ export const load: PageServerLoad = async (event) => {
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(cookies, session.id, session.expiresAt);
redirect(302, '/');
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';
error(403, 'Not Authorized: ' + message);
return new Response('Not Authorized: ' + message, {
status: 403,
});
}
const user: table.User = {
@ -82,7 +98,12 @@ export const load: PageServerLoad = async (event) => {
const session = await createSession(user.id);
setSessionTokenCookie(cookies, session.id, session.expiresAt);
setSessionTokenCookie(event, session.id, session.expiresAt);
redirect(302, '/');
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
};