diff --git a/prisma/migrations/20240710050838_save_owner_uid/migration.sql b/prisma/migrations/20240710050838_save_owner_uid/migration.sql new file mode 100644 index 0000000..822f61d --- /dev/null +++ b/prisma/migrations/20240710050838_save_owner_uid/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - Added the required column `owner` to the `Token` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Token" ( + "id" TEXT NOT NULL PRIMARY KEY, + "owner" TEXT NOT NULL, + "token" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL +); +INSERT INTO "new_Token" ("id", "refreshToken", "token") SELECT "id", "refreshToken", "token" FROM "Token"; +DROP TABLE "Token"; +ALTER TABLE "new_Token" RENAME TO "Token"; +CREATE UNIQUE INDEX "Token_id_key" ON "Token"("id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20240710052149_mix_owner_with_the_token/migration.sql b/prisma/migrations/20240710052149_mix_owner_with_the_token/migration.sql new file mode 100644 index 0000000..feb00c8 --- /dev/null +++ b/prisma/migrations/20240710052149_mix_owner_with_the_token/migration.sql @@ -0,0 +1,15 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Token" ( + "id" TEXT NOT NULL PRIMARY KEY, + "owner" TEXT NOT NULL, + "token" TEXT NOT NULL, + "refreshToken" TEXT +); +INSERT INTO "new_Token" ("id", "owner", "refreshToken", "token") SELECT "id", "owner", "refreshToken", "token" FROM "Token"; +DROP TABLE "Token"; +ALTER TABLE "new_Token" RENAME TO "Token"; +CREATE UNIQUE INDEX "Token_id_key" ON "Token"("id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3f3b9f6..e97eddb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,7 @@ datasource db { model Token { id String @id @unique @default(uuid()) + owner String token String - refreshToken String + refreshToken String? } \ No newline at end of file diff --git a/src/lib/configuration.ts b/src/lib/configuration.ts index 116f3f7..47823f6 100644 --- a/src/lib/configuration.ts +++ b/src/lib/configuration.ts @@ -3,7 +3,7 @@ const configuration = { endpoints: { authenticate: process.env.OAUTH2__AUTHENTICATE, logout: process.env.OAUTH2__LOGOUT, - token: process.env.OAUTH2__TOKEN + token: process.env.OAUTH2__GET_TOKEN }, client: { id: process.env.OAUTH2__CLIENT_ID, diff --git a/src/lib/index.ts b/src/lib/index.ts index 5e3381b..bb41ccf 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,7 +8,7 @@ const prisma = new PrismaClient() // Map of OAuth2 states const states = new Map }>() // Cache of userinfo -const usercache = new Map() +const userInfoCache = new Map() /** * @description Launch an OAuth2 login request for this request. @@ -23,7 +23,7 @@ export function launchLogin(req: Request) { response_type: "code", client_id: configuration.oauth2.client.id, redirect_uri: req.url, - scope: "openid profile email", + scope: "openid profile", state }) // Did not think this would work lmao @@ -55,7 +55,7 @@ export function launchLogin(req: Request) { * @param params * @returns Access token, its time-to-expiration, and refresh token if applicable */ -export async function retrieveToken( +export async function getNewToken( params: {grant_type: "authorization_code", redirect_uri: string, code: string} | {grant_type: "refresh_token", refresh_token: string} @@ -66,26 +66,123 @@ export async function retrieveToken( client_id: configuration.oauth2.client.id, client_secret: configuration.oauth2.client.secret }) - const url = new URL( - `?${searchParams.toString()}`, - configuration.oauth2.endpoints.token - ) - let res = await fetch(url) - if (!res.ok) - throw error(401, "Couldn't retrieve token for user") - else + // send request to retrieve tokens + let res = await fetch(configuration.oauth2.endpoints.token, { + method: "POST", + body: searchParams // this standard sucks, actually + }) + if (res.ok) return (await res.json()) as { access_token: string, expires_in: number, refresh_token?: string } } +export function fetchUserInfo(token: string) { + // try fetching new userinfo + return fetch(configuration.userinfo.route, { + headers: { + "Authorization": `Bearer ${token}` + } + }) +} + +export async function getUserInfo(id: string) { + // fetch token information + const tokenInfo = await prisma.token.findUnique({ + where: { id } + }) + if (!tokenInfo) return + + // check for cached userinfo + if (userInfoCache.has(tokenInfo.owner)) + return userInfoCache.get(tokenInfo.owner) + + let userInfoRequest = await fetchUserInfo(tokenInfo.token) + if (!userInfoRequest.ok) { + // assume that token has expired. + // try fetching a new one + + if (!tokenInfo.refreshToken) return // no refresh token. back out + let token = await getNewToken({ + grant_type: "refresh_token", + refresh_token: tokenInfo.refreshToken + }) + + if (!token) return // refresh failed. back out + prisma.token.update({ + where: { id }, + data: { + token: token.access_token, + refreshToken: token.refresh_token + } + }) + + userInfoRequest = await fetchUserInfo(token.access_token) + if (!userInfoRequest.ok) return // Give up + } + + const userInfo = await userInfoRequest.json() + + // cache userinfo + userInfoCache.set(tokenInfo.owner, userInfo) + setTimeout(() => userInfoCache.delete(tokenInfo.owner), 60*60*1000) + + return userInfo as User +} + +export function deleteToken(id: string) { + prisma.token.delete({ + where: {id} + }) +} + export async function getRequestUser(request: Request, cookies: Cookies) { const params = new URLSearchParams(request.url.split("?").slice(1).join("?")) let token = cookies.get("token") + // log user in if (!token && params.has("code") && params.has("state")) { + // check if state is real if (!states.has(params.get("state")!)) throw error(401, "bad state") - token = params.get("code")! + // get state + let state = states.get(params.get("state")!)! + states.delete(params.get("state")!) + clearTimeout(state.timeout) + + // try getting a token + let tokens = await getNewToken({ + grant_type: "authorization_code", + redirect_uri: state.redirect_uri, + code: params.get("code")! + }) + + if (!tokens) + throw error(401, "Couldn't get initial token, code may be incorrect") + + // fetch userdata + // could cache this, but lazy + + let userInfo = await (await fetchUserInfo(tokens.access_token)).json() as User + + // create a new token + let newToken = await prisma.token.create({ + data: { + token: tokens.access_token, + refreshToken: tokens.refresh_token, + owner: userInfo.sub + } + }) + + token = newToken.id cookies.set("token", token, { path: "/" }) } + + if (!token) return + let userinfo = await getUserInfo(token) + if (!userinfo) { + cookies.delete("token", { path: "/" }) + deleteToken(token) + } + + return userinfo } \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 74c2718..a2b181d 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,3 +1,7 @@ -export async function load({request}) { - +import { getRequestUser } from '$lib'; + +export async function load({request, cookies}) { + return { + user: await getRequestUser(request, cookies) + } } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1949a53..204290d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,8 @@ diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts index 6bcf1dd..cc67892 100644 --- a/src/routes/logout/+page.server.ts +++ b/src/routes/logout/+page.server.ts @@ -1,6 +1,16 @@ +import { invalidate } from "$app/navigation"; +import { deleteToken } from "$lib"; import configuration from "$lib/configuration.js"; import { redirect } from "@sveltejs/kit"; -export function load({}) { - throw redirect(301, configuration.oauth2.endpoints.logout) +export async function load({ cookies }) { + let tok = cookies.get("token") + if (!tok) + return + await deleteToken(tok) + cookies.delete("token", { path: "/" }) + if (configuration.oauth2.endpoints.logout) + throw redirect(302, configuration.oauth2.endpoints.logout) + + return { user: null } } \ No newline at end of file diff --git a/src/routes/logout/+page.svelte b/src/routes/logout/+page.svelte new file mode 100644 index 0000000..e5e9066 --- /dev/null +++ b/src/routes/logout/+page.svelte @@ -0,0 +1,4 @@ +

Logged out

+

+ If you were previously logged in, you have been logged out of ava. +

\ No newline at end of file diff --git a/src/routes/set/+page.server.ts b/src/routes/set/+page.server.ts index f744a97..f9ff6ba 100644 --- a/src/routes/set/+page.server.ts +++ b/src/routes/set/+page.server.ts @@ -1,7 +1,6 @@ import {launchLogin} from "$lib" export async function load({ request, parent }) { - //const { user } = await parent(); - let user = null + const { user } = await parent(); if (!user) launchLogin(request) } \ No newline at end of file diff --git a/src/routes/set/+page.svelte b/src/routes/set/+page.svelte index aa6096b..e364721 100644 --- a/src/routes/set/+page.svelte +++ b/src/routes/set/+page.svelte @@ -1,8 +1,10 @@ -

Hi, {data.user.username}

+

Hi, {data.user.name}

Your identifier is {data.user.sub}.

\ No newline at end of file