authentication

This commit is contained in:
split / May 2024-07-10 00:23:38 -07:00
parent 348cb6c204
commit c62056f433
Signed by: split
GPG key ID: C325C61F0BF517C0
11 changed files with 177 additions and 23 deletions

View file

@ -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;

View file

@ -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;

View file

@ -12,6 +12,7 @@ datasource db {
model Token { model Token {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
owner String
token String token String
refreshToken String refreshToken String?
} }

View file

@ -3,7 +3,7 @@ const configuration = {
endpoints: { endpoints: {
authenticate: process.env.OAUTH2__AUTHENTICATE, authenticate: process.env.OAUTH2__AUTHENTICATE,
logout: process.env.OAUTH2__LOGOUT, logout: process.env.OAUTH2__LOGOUT,
token: process.env.OAUTH2__TOKEN token: process.env.OAUTH2__GET_TOKEN
}, },
client: { client: {
id: process.env.OAUTH2__CLIENT_ID, id: process.env.OAUTH2__CLIENT_ID,

View file

@ -8,7 +8,7 @@ const prisma = new PrismaClient()
// Map of OAuth2 states // Map of OAuth2 states
const states = new Map<string, { redirect_uri: string, timeout: ReturnType<typeof setTimeout> }>() const states = new Map<string, { redirect_uri: string, timeout: ReturnType<typeof setTimeout> }>()
// Cache of userinfo // Cache of userinfo
const usercache = new Map<string, User>() const userInfoCache = new Map<string, User>()
/** /**
* @description Launch an OAuth2 login request for this request. * @description Launch an OAuth2 login request for this request.
@ -23,7 +23,7 @@ export function launchLogin(req: Request) {
response_type: "code", response_type: "code",
client_id: configuration.oauth2.client.id, client_id: configuration.oauth2.client.id,
redirect_uri: req.url, redirect_uri: req.url,
scope: "openid profile email", scope: "openid profile",
state state
}) })
// Did not think this would work lmao // Did not think this would work lmao
@ -55,7 +55,7 @@ export function launchLogin(req: Request) {
* @param params * @param params
* @returns Access token, its time-to-expiration, and refresh token if applicable * @returns Access token, its time-to-expiration, and refresh token if applicable
*/ */
export async function retrieveToken( export async function getNewToken(
params: params:
{grant_type: "authorization_code", redirect_uri: string, code: string} {grant_type: "authorization_code", redirect_uri: string, code: string}
| {grant_type: "refresh_token", refresh_token: string} | {grant_type: "refresh_token", refresh_token: string}
@ -66,26 +66,123 @@ export async function retrieveToken(
client_id: configuration.oauth2.client.id, client_id: configuration.oauth2.client.id,
client_secret: configuration.oauth2.client.secret client_secret: configuration.oauth2.client.secret
}) })
const url = new URL(
`?${searchParams.toString()}`,
configuration.oauth2.endpoints.token
)
let res = await fetch(url) // send request to retrieve tokens
if (!res.ok) let res = await fetch(configuration.oauth2.endpoints.token, {
throw error(401, "Couldn't retrieve token for user") method: "POST",
else body: searchParams // this standard sucks, actually
})
if (res.ok)
return (await res.json()) as { access_token: string, expires_in: number, refresh_token?: string } 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) { export async function getRequestUser(request: Request, cookies: Cookies) {
const params = new URLSearchParams(request.url.split("?").slice(1).join("?")) const params = new URLSearchParams(request.url.split("?").slice(1).join("?"))
let token = cookies.get("token") let token = cookies.get("token")
// log user in
if (!token && params.has("code") && params.has("state")) { if (!token && params.has("code") && params.has("state")) {
// check if state is real
if (!states.has(params.get("state")!)) if (!states.has(params.get("state")!))
throw error(401, "bad 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: "/" }) cookies.set("token", token, { path: "/" })
} }
if (!token) return
let userinfo = await getUserInfo(token)
if (!userinfo) {
cookies.delete("token", { path: "/" })
deleteToken(token)
}
return userinfo
} }

View file

@ -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)
}
} }

View file

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import "@fontsource-variable/inter"; import "@fontsource-variable/inter";
import ava from "../assets/ava_icon.svg?raw" import ava from "../assets/ava_icon.svg?raw"
export let data: { user?: { sub: string, username: string } }; import type { User } from "$lib/types";
export let data: { user?: User };
</script> </script>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

View file

@ -1,6 +1,16 @@
import { invalidate } from "$app/navigation";
import { deleteToken } from "$lib";
import configuration from "$lib/configuration.js"; import configuration from "$lib/configuration.js";
import { redirect } from "@sveltejs/kit"; import { redirect } from "@sveltejs/kit";
export function load({}) { export async function load({ cookies }) {
throw redirect(301, configuration.oauth2.endpoints.logout) 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 }
} }

View file

@ -0,0 +1,4 @@
<h1>Logged out</h1>
<p>
If you were previously logged in, you have been logged out of ava.
</p>

View file

@ -1,7 +1,6 @@
import {launchLogin} from "$lib" import {launchLogin} from "$lib"
export async function load({ request, parent }) { export async function load({ request, parent }) {
//const { user } = await parent(); const { user } = await parent();
let user = null
if (!user) if (!user)
launchLogin(request) launchLogin(request)
} }

View file

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
export let data: { user: { sub: string, username: string } }; import type { User } from "$lib/types";
export let data: {user: User};
</script> </script>
<h1>Hi, {data.user.username}</h1> <h1>Hi, {data.user.name}</h1>
<p> <p>
Your identifier is {data.user.sub}. Your identifier is {data.user.sub}.
</p> </p>