authentication

This commit is contained in:
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 {
id String @id @unique @default(uuid())
owner String
token String
refreshToken String
refreshToken String?
}

View file

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

View file

@ -8,7 +8,7 @@ const prisma = new PrismaClient()
// Map of OAuth2 states
const states = new Map<string, { redirect_uri: string, timeout: ReturnType<typeof setTimeout> }>()
// Cache of userinfo
const usercache = new Map<string, User>()
const userInfoCache = new Map<string, User>()
/**
* @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
}

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">
import "@fontsource-variable/inter";
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>
<!DOCTYPE html>
<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 { 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 }
}

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"
export async function load({ request, parent }) {
//const { user } = await parent();
let user = null
const { user } = await parent();
if (!user)
launchLogin(request)
}

View file

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