authentication
This commit is contained in:
parent
348cb6c204
commit
c62056f433
|
@ -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;
|
|
@ -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;
|
|
@ -12,6 +12,7 @@ datasource db {
|
|||
|
||||
model Token {
|
||||
id String @id @unique @default(uuid())
|
||||
owner String
|
||||
token String
|
||||
refreshToken String
|
||||
refreshToken String?
|
||||
}
|
|
@ -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,
|
||||
|
|
121
src/lib/index.ts
121
src/lib/index.ts
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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 }
|
||||
}
|
4
src/routes/logout/+page.svelte
Normal file
4
src/routes/logout/+page.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<h1>Logged out</h1>
|
||||
<p>
|
||||
If you were previously logged in, you have been logged out of ava.
|
||||
</p>
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
Loading…
Reference in a new issue