Loading packages/client/src/components/Header/User.tsx +7 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,13 @@ export const User = () => { <div className="user-name">{user.user.username}</div> <div className="user-instance">{user.service.instance.hostname}</div> </div> <img src={user.user.profile} alt="User Avatar" className="user-avatar" /> {user.user.picture_url && ( <img src={user.user.picture_url} alt="User Avatar" className="user-avatar" /> )} </div> ) : ( <></> Loading packages/lib/src/net.ts +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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; }; }; packages/server/src/api.ts +40 −27 Original line number Diff line number Diff line 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", }) ); }); app.get("/callback", async (req, res) => { const { code } = req.query; // TODO: logout endpoint 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()); app.get("/callback", async (req, res) => { // const { code } = req.query; 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: { Loading @@ -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, }, }; Loading packages/server/src/index.ts +9 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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); packages/server/src/lib/oidc.ts 0 → 100644 +24 −0 Original line number Diff line number Diff line import { BaseClient, Issuer } from "openid-client"; class OpenID_ { issuer: Issuer<BaseClient> = {} 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_(); Loading
packages/client/src/components/Header/User.tsx +7 −1 Original line number Diff line number Diff line Loading @@ -9,7 +9,13 @@ export const User = () => { <div className="user-name">{user.user.username}</div> <div className="user-instance">{user.service.instance.hostname}</div> </div> <img src={user.user.profile} alt="User Avatar" className="user-avatar" /> {user.user.picture_url && ( <img src={user.user.picture_url} alt="User Avatar" className="user-avatar" /> )} </div> ) : ( <></> Loading
packages/lib/src/net.ts +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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; }; };
packages/server/src/api.ts +40 −27 Original line number Diff line number Diff line 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", }) ); }); app.get("/callback", async (req, res) => { const { code } = req.query; // TODO: logout endpoint 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()); app.get("/callback", async (req, res) => { // const { code } = req.query; 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: { Loading @@ -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, }, }; Loading
packages/server/src/index.ts +9 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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);
packages/server/src/lib/oidc.ts 0 → 100644 +24 −0 Original line number Diff line number Diff line import { BaseClient, Issuer } from "openid-client"; class OpenID_ { issuer: Issuer<BaseClient> = {} 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_();