Kyoo/api/src/auth.ts
2025-04-08 09:34:52 +02:00

121 lines
3.0 KiB
TypeScript

import { TypeCompiler } from "@sinclair/typebox/compiler";
import { Value } from "@sinclair/typebox/value";
import Elysia, { t } from "elysia";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { KError } from "./models/error";
import type { Prettify } from "./utils";
const jwtSecret = process.env.JWT_SECRET
? new TextEncoder().encode(process.env.JWT_SECRET)
: null;
const jwks = createRemoteJWKSet(
new URL(
".well-known/jwks.json",
process.env.AUTH_SERVER ?? "http://auth:4568",
),
);
const Settings = t.Object(
{
preferOriginal: t.Boolean({ default: true }),
},
{ additionalProperties: true },
);
type Settings = typeof Settings.static;
const Jwt = t.Object({
sub: t.String({ description: "User id" }),
sid: t.String({ description: "Session id" }),
username: t.String(),
permissions: t.Array(t.String()),
settings: t.Optional(t.Partial(Settings, { default: {} })),
});
type Jwt = typeof Jwt.static;
const validator = TypeCompiler.Compile(Jwt);
export const auth = new Elysia({ name: "auth" })
.guard({
headers: t.Object(
{
authorization: t.TemplateLiteral("Bearer ${string}"),
},
{ additionalProperties: true },
),
})
.resolve(async ({ headers: { authorization }, error }) => {
const bearer = authorization?.slice(7);
if (!bearer) {
return error(500, {
status: 500,
message: "No jwt, auth server configuration error.",
});
}
try {
// @ts-expect-error ts can't understand that there's two overload idk why
const { payload } = await jwtVerify(bearer, jwtSecret ?? jwks, {
issuer: process.env.JWT_ISSUER,
});
const raw = validator.Decode(payload);
const jwt = Value.Default(Jwt, raw) as Prettify<
Jwt & { settings: Settings }
>;
return { jwt };
} catch (err) {
return error(403, {
status: 403,
message: "Invalid jwt. Verification vailed",
details: err,
});
}
})
.macro({
permissions(perms: string[]) {
return {
beforeHandle: ({ jwt, error }) => {
for (const perm of perms) {
if (!jwt!.permissions.includes(perm)) {
return error(403, {
status: 403,
message: `Missing permission: '${perm}'.`,
details: { current: jwt!.permissions, required: perms },
});
}
}
},
};
},
})
.as("plugin");
const User = t.Object({
id: t.String({ format: "uuid" }),
username: t.String(),
email: t.String({ format: "email" }),
createdDate: t.String({ format: "date-time" }),
lastSeen: t.String({ format: "date-time" }),
claims: t.Record(t.String(), t.Any()),
oidc: t.Record(
t.String(),
t.Object({
id: t.String({ format: "uuid" }),
username: t.String(),
profileUrl: t.Nullable(t.String({ format: "url" })),
}),
),
});
const UserC = TypeCompiler.Compile(t.Union([User, KError]));
export async function getUserInfo(
id: string,
headers: { authorization: string },
) {
const resp = await fetch(
new URL(`/auth/users/${id}`, process.env.AUTH_SERVER ?? "http://auth:4568"),
{ headers },
);
return UserC.Decode(await resp.json());
}