From babfb526de16daa3fc139322b065ad9ea4f0de16 Mon Sep 17 00:00:00 2001 From: Cfp Date: Tue, 19 Aug 2025 11:21:27 +0200 Subject: [PATCH] feat: improve OIDC to use refresh tokens --- bun.lock | 33 +++++++++++++-- package.json | 1 + .../components/lnv/sidebar/UserSidebar.svelte | 4 ++ src/lib/services/lnv.ts | 40 +++++++++++++++++++ src/lib/services/oidc.ts | 14 +++++-- src/main.ts | 2 +- 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 325a0af..4660acd 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "eslint": "^9.29.0", "eslint-plugin-svelte": "^3.9.3", "globals": "^16.2.0", + "jsonwebtoken": "^9.0.2", "libsodium-wrappers": "^0.7.15", "opening_hours": "^3.8.0", "pako": "^2.1.0", @@ -505,6 +506,8 @@ "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -549,6 +552,8 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "elementtree": ["elementtree@0.1.7", "", { "dependencies": { "sax": "1.1.4" } }, "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -715,8 +720,14 @@ "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -767,8 +778,22 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], @@ -903,7 +928,7 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], @@ -1133,6 +1158,10 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="], "through2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -1166,7 +1195,5 @@ "global-prefix/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], "through2/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "through2/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], } } diff --git a/package.json b/package.json index 8d2052c..c74ff97 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "eslint": "^9.29.0", "eslint-plugin-svelte": "^3.9.3", "globals": "^16.2.0", + "jsonwebtoken": "^9.0.2", "libsodium-wrappers": "^0.7.15", "opening_hours": "^3.8.0", "pako": "^2.1.0", diff --git a/src/lib/components/lnv/sidebar/UserSidebar.svelte b/src/lib/components/lnv/sidebar/UserSidebar.svelte index 2d4039a..9874310 100644 --- a/src/lib/components/lnv/sidebar/UserSidebar.svelte +++ b/src/lib/components/lnv/sidebar/UserSidebar.svelte @@ -5,6 +5,7 @@ import { getAuthURL, getOIDCUser } from "$lib/services/oidc"; import * as Avatar from "$lib/components/ui/avatar"; import { m } from "$lang/messages"; + import { refreshToken } from "$lib/services/lnv"; interface OIDCUser { sub: string; @@ -68,6 +69,9 @@ {user.name || user.preferred_username} +
{user.sub}
{JSON.stringify(user, null, 2)} {/if} diff --git a/src/lib/services/lnv.ts b/src/lib/services/lnv.ts index f6f960f..9bf73ce 100644 --- a/src/lib/services/lnv.ts +++ b/src/lib/services/lnv.ts @@ -1,4 +1,5 @@ import { LNV_SERVER } from "./hosts"; +import type { OIDCUser } from "./oidc"; export type Capabilities = ("auth" | "reviews" | "ai" | "fuel" | "post")[]; export let capabilities: Capabilities = []; @@ -52,6 +53,45 @@ export async function hasCapability( return caps.includes(capability); } +export async function refreshToken() { + const config = await getOIDCConfig(); + if(!config) throw new Error("Server does not support OIDC."); + const refresh_token = localStorage.getItem("lnv-refresh"); + if(!refresh_token) throw new Error("No refresh token.") + const params = new URLSearchParams(); + params.append("grant_type", "refresh_token"); + params.append("refresh_token", refresh_token); + params.append("client_id", config.CLIENT_ID); + const res = await fetch(config.TOKEN_URL, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params + }); + const data = (await res.json()) as OIDCUser; + if(!res.ok) { + console.error("Refreshing token: " + res.status + " " + res.statusText) + console.error(data); + } + console.log(data); + localStorage.setItem("lnv-id", data.id_token); + localStorage.setItem("lnv-token", data.access_token); + localStorage.setItem("lnv-refresh", data.refresh_token); +} + +export async function authFetch(url: string, params: RequestInit): ReturnType { + let res = await fetch(url, params); + if(res.status == 401) { + await refreshToken(); + } + res = await fetch(url, params); + if(res.status == 401) { + console.error("Server is misconfigured."); + } + return res; +} + export interface Review { user_id: string; username: string; diff --git a/src/lib/services/oidc.ts b/src/lib/services/oidc.ts index cc6adcf..9f56f0f 100644 --- a/src/lib/services/oidc.ts +++ b/src/lib/services/oidc.ts @@ -17,7 +17,7 @@ export async function getAuthURL() { const state = generateRandomString(16); return { - url: `${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/login/callback&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`, + url: `${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/oidc&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`, codeVerifier: pkce.codeVerifier, state, }; @@ -60,6 +60,14 @@ async function sha256(input: string | undefined): Promise { return await window.crypto.subtle.digest("SHA-256", data); } +export type OIDCUser = { + access_token: string; + token_type: string; + refresh_token: string; + expires_in: number; + id_token: string; +} + export async function getOIDCUser(code: string, codeVerifier: string) { if (!(await hasCapability("auth"))) { throw new Error("Server does not support OIDC authentication"); @@ -75,7 +83,7 @@ export async function getOIDCUser(code: string, codeVerifier: string) { params.append("code", code); params.append("client_id", CLIENT_ID); params.append("code_verifier", codeVerifier); - params.append("redirect_uri", window.location.origin + "/login/callback"); + params.append("redirect_uri", window.location.origin + "/oidc"); const res = await fetch(TOKEN_URL, { method: "POST", @@ -85,6 +93,6 @@ export async function getOIDCUser(code: string, codeVerifier: string) { body: params, }).then((res) => res.json()); - return res; + return res as OIDCUser; // return JSON.parse(atob(id_token.split(".")[1])); } diff --git a/src/main.ts b/src/main.ts index aef5dee..568f0bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import { mount } from "svelte"; import "./app.css"; import App from "./App.svelte"; -if (location.href.includes("/login/callback")) { +if (location.href.includes("/oidc")) { const url = new URL(location.href); const code = url.searchParams.get("code"); const state = url.searchParams.get("state");