From 8801efc9a3024a1b4c0ebecd1f6eac1a0f3e865f Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 23 May 2024 14:59:54 -0600 Subject: [PATCH] migrate to OIDC for auth --- .../client/src/components/Header/User.tsx | 8 ++- packages/lib/src/net.ts | 10 ++- packages/server/src/api.ts | 67 +++++++++++-------- packages/server/src/index.ts | 9 +++ packages/server/src/lib/oidc.ts | 24 +++++++ packages/server/src/types.ts | 7 ++ 6 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 packages/server/src/lib/oidc.ts diff --git a/packages/client/src/components/Header/User.tsx b/packages/client/src/components/Header/User.tsx index 4dd87c2..0df93db 100644 --- a/packages/client/src/components/Header/User.tsx +++ b/packages/client/src/components/Header/User.tsx @@ -9,7 +9,13 @@ export const User = () => {
{user.user.username}
{user.service.instance.hostname}
- User Avatar + {user.user.picture_url && ( + User Avatar + )} ) : ( <> diff --git a/packages/lib/src/net.ts b/packages/lib/src/net.ts index 51ddc3b..a17f520 100644 --- a/packages/lib/src/net.ts +++ b/packages/lib/src/net.ts @@ -40,6 +40,8 @@ export interface IAppContext { settingsSidebar: boolean; setSettingsSidebar: (v: boolean) => void; undo?: { available: true; expireAt: number }; + loadChat: boolean; + setLoadChat: (v: boolean) => void; } export interface IPalleteContext { @@ -114,13 +116,19 @@ export type AuthSession = { software: { name: string; version: string; + logo_uri?: string; + repository?: string; + homepage?: string; }; instance: { hostname: string; + logo_uri?: string; + banner_uri?: string; + name?: string; }; }; user: { username: string; - profile: string; + picture_url?: string; }; }; diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 614e082..4e499ea 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -1,44 +1,57 @@ import { Router } from "express"; import { prisma } from "./lib/prisma"; +import { OpenID } from "./lib/oidc"; const app = Router(); -const { AUTH_ENDPOINT, AUTH_CLIENT, AUTH_SECRET } = process.env; - app.get("/me", (req, res) => { res.json(req.session); }); app.get("/login", (req, res) => { - const params = new URLSearchParams(); - params.set("service", "canvas"); - - res.redirect(AUTH_ENDPOINT + "/login?" + params); + res.redirect( + OpenID.client.authorizationUrl({ + prompt: "consent", + scope: "openid instance", + }) + ); }); +// TODO: logout endpoint + app.get("/callback", async (req, res) => { - const { code } = req.query; + // const { code } = req.query; - const who = await fetch(AUTH_ENDPOINT + "/api/auth/identify", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${AUTH_CLIENT}:${AUTH_SECRET}`, - }, - body: JSON.stringify({ - code, - }), - }).then((a) => a.json()); - - if (!who.success) { - res.json({ - error: "AUTHENTICATION FAILED", - error_message: who.error || "no error specified", + const params = OpenID.client.callbackParams(req); + const exchange = await OpenID.client.callback( + OpenID.getRedirectUrl(), + params + ); + if (!exchange || !exchange.access_token) { + return res.status(400).json({ + success: false, + error: "FAILED TOKEN EXCHANGE", }); - return; } - const [username, hostname] = who.user.sub.split("@"); + const whoami = await OpenID.client.userinfo<{ + instance: { + software: { + name: string; + version: string; + logo_uri?: string; + repository?: string; + homepage?: string; + }; + instance: { + logo_uri?: string; + banner_uri?: string; + name?: string; + }; + }; + }>(exchange.access_token); + + const [username, hostname] = whoami.sub.split("@"); await prisma.user.upsert({ where: { @@ -52,14 +65,14 @@ app.get("/callback", async (req, res) => { req.session.user = { service: { - ...who.service, + ...whoami.instance, instance: { - ...who.service.instance, + ...whoami.instance.instance, hostname, }, }, user: { - profile: who.user.profile, + picture_url: whoami.picture, username, }, }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b3efaf8..7da56d3 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -4,6 +4,7 @@ import { Redis } from "./lib/redis"; import { Logger } from "./lib/Logger"; import { ExpressServer } from "./lib/Express"; import { SocketServer } from "./lib/SocketServer"; +import { OpenID } from "./lib/oidc"; // Validate environment variables @@ -51,7 +52,15 @@ if (!process.env.AUTH_SECRET) { process.exit(1); } +if (!process.env.OIDC_CALLBACK_HOST) { + Logger.error("OIDC_CALLBACK_HOST is not defined"); + process.exit(1); +} + Redis.connect(); +OpenID.setup().then(() => { + Logger.info("Setup OpenID"); +}); const express = new ExpressServer(); new SocketServer(express.httpServer); diff --git a/packages/server/src/lib/oidc.ts b/packages/server/src/lib/oidc.ts new file mode 100644 index 0000000..cf184ae --- /dev/null +++ b/packages/server/src/lib/oidc.ts @@ -0,0 +1,24 @@ +import { BaseClient, Issuer } from "openid-client"; + +class OpenID_ { + issuer: Issuer = {} as any; + client: BaseClient = {} as any; + + async setup() { + const { AUTH_ENDPOINT, AUTH_CLIENT, AUTH_SECRET } = process.env; + + this.issuer = await Issuer.discover(AUTH_ENDPOINT); + this.client = new this.issuer.Client({ + client_id: AUTH_CLIENT, + client_secret: AUTH_SECRET, + response_types: ["code"], + redirect_uris: [this.getRedirectUrl()], + }); + } + + getRedirectUrl() { + return process.env.OIDC_CALLBACK_HOST + "/api/callback"; + } +} + +export const OpenID = new OpenID_(); diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index de76328..58aa90a 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -25,6 +25,13 @@ declare global { REDIS_HOST: string; REDIS_SESSION_PREFIX: string; + /** + * hostname that is used in the callback + * + * @example http://localhost:3000 + * @example https://canvas.com + */ + OIDC_CALLBACK_HOST: string; /** * If this is set, enable socket.io CORS to this origin *