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 {
|
model Token {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
|
owner String
|
||||||
token String
|
token String
|
||||||
refreshToken String
|
refreshToken String?
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
121
src/lib/index.ts
121
src/lib/index.ts
|
@ -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
|
||||||
}
|
}
|
|
@ -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">
|
<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">
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
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"
|
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)
|
||||||
}
|
}
|
|
@ -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>
|
Loading…
Reference in a new issue