Loading backend/package.json +1 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,7 @@ "express-session": "^1.19.0", "ioredis": "^5.11.0", "oidc-provider": "^8.8.1", "openid-client": "^5.7.1", "openid-client": "^6.8.4", "string-strip-html": "^13.5.3" }, "devDependencies": { Loading backend/src/controllers/DevController.ts 0 → 100644 +155 −0 Original line number Diff line number Diff line import e, { Router } from "express"; import * as OIDClient from "openid-client"; import { prisma } from "../lib/prisma.js"; import { OidcTypes } from "../lib/adapter.js"; const DEV_CLIENT = { client_id: "dev", client_secret: "dev", grant_types: ["authorization_code"], subject_type: "public", redirect_uris: [], response_types: ["code"], application_type: "web", require_auth_time: false, client_id_issued_at: Date.now(), client_secret_expires_at: 0, post_logout_redirect_uris: [], token_endpoint_auth_method: "client_secret_basic", id_token_signed_response_alg: "RS256", require_pushed_authorization_requests: false, }; const OPENID_CONFIG = new OIDClient.Configuration( { issuer: "http://localhost:5173" }, "dev", { client_secret: "dev", }, ); const app = Router(); app.get("/test-exchange", (req, res) => { // oauth4webapi complains as it's a non-https url // const url = OIDClient.buildAuthorizationUrl(OPENID_CONFIG, { // redirect_uri: "http://localhost:5173/api/dev/callback", // scope: "openid instance", // prompt: "consent", // }); const url = new URL("http://localhost:5173/api/oidc/auth"); url.searchParams.set( "redirect_uri", "http://localhost:5173/api/dev/callback", ); url.searchParams.set("prompt", "consent"); url.searchParams.set("scope", "openid instance"); url.searchParams.set("client_id", "dev"); url.searchParams.set("response_type", "code"); res.redirect(String(url)); }); /** * Auth flow demo callback */ app.get("/callback", async (req, res) => { const send = (res: e.Response, data: unknown) => { res.contentType("html").send(` <h1>Dev Callback</h1> <code><pre>${JSON.stringify(data, null, 2)}</pre></code> <a href="/">Back Home</a>`); }; const exchange = await OIDClient.authorizationCodeGrant( OPENID_CONFIG, new URL(req.url), {}, new URLSearchParams(JSON.stringify(req.query)), ).catch((e) => { console.warn("/api/dev/callback authorizationCodeGrant request error", e); return String(e); }); if (typeof exchange === "string") { return send(res.status(400), { state: "token exchange failed", exchange, }); } const userInfo = await OIDClient.fetchUserInfo( OPENID_CONFIG, exchange.access_token, OIDClient.skipSubjectCheck, ).catch((e) => { console.warn("/api/dev/callback userInfo request error", e); return String(e); }); if (typeof userInfo === "string") { return send(res.status(400), { state: "userinfo request failed", exchange, userInfo, }); } send(res, { state: "success", exchange, userInfo, }); }); /** * create a development client */ app.post("/ensure-dev-client", async (req, res) => { const DEFAULT_REDIRECT_URIS = [ "http://localhost:3000", "http://localhost:8008/_synapse/client/oidc/callback", "http://localhost:5173/api/dev/callback", ]; if ("redirect_uri" in req.body) { if (!Array.isArray(req.body.redirect_uri)) { req.body.redirect_uri = [String(req.body.redirect_uri)]; } for (const input of req.body.redirect_uri) { DEFAULT_REDIRECT_URIS.push(String(input)); } } await prisma.oidcModel .upsert({ where: { id: DEV_CLIENT.client_id, }, create: { id: DEV_CLIENT.client_id, type: OidcTypes["Client"], payload: { ...DEV_CLIENT, redirect_uris: DEFAULT_REDIRECT_URIS }, }, update: { payload: { ...DEV_CLIENT, redirect_uris: DEFAULT_REDIRECT_URIS }, }, }) .then(() => { console.log("Created/upserted dev oidc client"); res.status(201).json({ success: true, }); }) .catch((e) => { console.error("Failed to create dev oidc client", e); res.status(500).json({ success: false, error: String(e), }); }); }); export default app; backend/src/lib/apub/utils.stub.ts 0 → 100644 +73 −0 Original line number Diff line number Diff line // a stub implementation of the activitypub utils // the stub implementation is intended for non-federation development use // to avoid having to setup full TLS-termination reverse proxy, this will // supply the responses to authenticate as any user import { Actor, ChatMessage, Context, Note, Person, ResourceDescriptor, } from "@fedify/fedify"; import { IProfile } from "../instance/userMeta.js"; import { USER_IDENTIFIER } from "./federation.js"; import { APub } from "./utils.js"; import { AuthSession } from "../../controllers/AuthSession.js"; export class APubStub extends APub { constructor() { super(null as any); } static options = () => ({}); static get accountHandle() { return USER_IDENTIFIER + "@localhost"; } static get() { return new APubStub(); } static async buildProfile( identifier: string | URL, ): Promise<IProfile | null> { const profile: IProfile = { sub: String(identifier), }; return profile; } static async lookupWebfinger( identifier: string | URL, ): Promise<ResourceDescriptor | null> { return { subject: String(identifier), }; } static async lookupActor(identifier: string | URL): Promise<Actor | null> { return new Person({ id: new URL(identifier), }); } static async sendDM(session: AuthSession) {} static async deleteDM(session: AuthSession) {} async sendChatMessage(id: string, target: Actor, content: ChatMessage) {} async sendNote(id: string, target: Actor, content: Note) {} build( type: "ChatMessage" | "Note", opts: { id: string; one_time_code: string; createdAt: Date; target: Actor }, ): any {} } const _staticTypeCheck: typeof APub = APubStub; _staticTypeCheck.get(); // to ignore "unused variable" but still trigger type check No newline at end of file backend/src/lib/express.ts +7 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import { APub } from "./apub/utils.js"; import { APIAdminRouter } from "./api_admin.js"; import { Handoff } from "../handoff/index.js"; import { DocLinks } from "./DocLinks.js"; import DevController from "../controllers/DevController.js"; export const app = express(); Loading Loading @@ -126,6 +127,10 @@ const interactionMiddleware = ( }; if (process.env.NODE_ENV === "development") { console.log( "Development Mode: /_dev & /api/dev endpoints have been activated", ); // expose the internals of the interaction middleware for the vite dev server to access app.post("/_dev/interaction/:uid", async (req, res) => { const middleware = await interactionMiddleware(req, res); Loading @@ -150,6 +155,8 @@ if (process.env.NODE_ENV === "development") { break; } }); app.use("/api/dev", DevController); } app.use("/interaction/:uid", async (req, res, next) => { Loading backend/src/lib/oidc.ts +10 −4 Original line number Diff line number Diff line import Provider from "oidc-provider"; import fs from "node:fs"; import * as OIDClient from "openid-client"; import { PrismaAdapter } from "./adapter.js"; import { Issuer } from "openid-client"; import { IInstance, getInstanceMeta } from "./instance/instanceMeta.js"; import { IProfile, getUserMeta } from "./instance/userMeta.js"; import { ShadowAPI, UserLoginError } from "./shadow.js"; Loading Loading @@ -231,15 +231,21 @@ oidc.use(async (ctx, next) => { * @returns */ export const doesInstanceSupportOIDC = async (instance_hostname: string) => { let issuer: Issuer; let issuer: OIDClient.Configuration; try { issuer = await Issuer.discover("https://" + instance_hostname); // we pass an empty string into the client_id as we will // not be using this instance to perform requests, // only to check if the instance supports openid issuer = await OIDClient.discovery( new URL("https://" + instance_hostname), "", ); } catch (e) { return false; } if (typeof issuer.metadata.registration_endpoint === "undefined") { if (typeof issuer.serverMetadata().registration_endpoint === "undefined") { return false; } Loading Loading
backend/package.json +1 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,7 @@ "express-session": "^1.19.0", "ioredis": "^5.11.0", "oidc-provider": "^8.8.1", "openid-client": "^5.7.1", "openid-client": "^6.8.4", "string-strip-html": "^13.5.3" }, "devDependencies": { Loading
backend/src/controllers/DevController.ts 0 → 100644 +155 −0 Original line number Diff line number Diff line import e, { Router } from "express"; import * as OIDClient from "openid-client"; import { prisma } from "../lib/prisma.js"; import { OidcTypes } from "../lib/adapter.js"; const DEV_CLIENT = { client_id: "dev", client_secret: "dev", grant_types: ["authorization_code"], subject_type: "public", redirect_uris: [], response_types: ["code"], application_type: "web", require_auth_time: false, client_id_issued_at: Date.now(), client_secret_expires_at: 0, post_logout_redirect_uris: [], token_endpoint_auth_method: "client_secret_basic", id_token_signed_response_alg: "RS256", require_pushed_authorization_requests: false, }; const OPENID_CONFIG = new OIDClient.Configuration( { issuer: "http://localhost:5173" }, "dev", { client_secret: "dev", }, ); const app = Router(); app.get("/test-exchange", (req, res) => { // oauth4webapi complains as it's a non-https url // const url = OIDClient.buildAuthorizationUrl(OPENID_CONFIG, { // redirect_uri: "http://localhost:5173/api/dev/callback", // scope: "openid instance", // prompt: "consent", // }); const url = new URL("http://localhost:5173/api/oidc/auth"); url.searchParams.set( "redirect_uri", "http://localhost:5173/api/dev/callback", ); url.searchParams.set("prompt", "consent"); url.searchParams.set("scope", "openid instance"); url.searchParams.set("client_id", "dev"); url.searchParams.set("response_type", "code"); res.redirect(String(url)); }); /** * Auth flow demo callback */ app.get("/callback", async (req, res) => { const send = (res: e.Response, data: unknown) => { res.contentType("html").send(` <h1>Dev Callback</h1> <code><pre>${JSON.stringify(data, null, 2)}</pre></code> <a href="/">Back Home</a>`); }; const exchange = await OIDClient.authorizationCodeGrant( OPENID_CONFIG, new URL(req.url), {}, new URLSearchParams(JSON.stringify(req.query)), ).catch((e) => { console.warn("/api/dev/callback authorizationCodeGrant request error", e); return String(e); }); if (typeof exchange === "string") { return send(res.status(400), { state: "token exchange failed", exchange, }); } const userInfo = await OIDClient.fetchUserInfo( OPENID_CONFIG, exchange.access_token, OIDClient.skipSubjectCheck, ).catch((e) => { console.warn("/api/dev/callback userInfo request error", e); return String(e); }); if (typeof userInfo === "string") { return send(res.status(400), { state: "userinfo request failed", exchange, userInfo, }); } send(res, { state: "success", exchange, userInfo, }); }); /** * create a development client */ app.post("/ensure-dev-client", async (req, res) => { const DEFAULT_REDIRECT_URIS = [ "http://localhost:3000", "http://localhost:8008/_synapse/client/oidc/callback", "http://localhost:5173/api/dev/callback", ]; if ("redirect_uri" in req.body) { if (!Array.isArray(req.body.redirect_uri)) { req.body.redirect_uri = [String(req.body.redirect_uri)]; } for (const input of req.body.redirect_uri) { DEFAULT_REDIRECT_URIS.push(String(input)); } } await prisma.oidcModel .upsert({ where: { id: DEV_CLIENT.client_id, }, create: { id: DEV_CLIENT.client_id, type: OidcTypes["Client"], payload: { ...DEV_CLIENT, redirect_uris: DEFAULT_REDIRECT_URIS }, }, update: { payload: { ...DEV_CLIENT, redirect_uris: DEFAULT_REDIRECT_URIS }, }, }) .then(() => { console.log("Created/upserted dev oidc client"); res.status(201).json({ success: true, }); }) .catch((e) => { console.error("Failed to create dev oidc client", e); res.status(500).json({ success: false, error: String(e), }); }); }); export default app;
backend/src/lib/apub/utils.stub.ts 0 → 100644 +73 −0 Original line number Diff line number Diff line // a stub implementation of the activitypub utils // the stub implementation is intended for non-federation development use // to avoid having to setup full TLS-termination reverse proxy, this will // supply the responses to authenticate as any user import { Actor, ChatMessage, Context, Note, Person, ResourceDescriptor, } from "@fedify/fedify"; import { IProfile } from "../instance/userMeta.js"; import { USER_IDENTIFIER } from "./federation.js"; import { APub } from "./utils.js"; import { AuthSession } from "../../controllers/AuthSession.js"; export class APubStub extends APub { constructor() { super(null as any); } static options = () => ({}); static get accountHandle() { return USER_IDENTIFIER + "@localhost"; } static get() { return new APubStub(); } static async buildProfile( identifier: string | URL, ): Promise<IProfile | null> { const profile: IProfile = { sub: String(identifier), }; return profile; } static async lookupWebfinger( identifier: string | URL, ): Promise<ResourceDescriptor | null> { return { subject: String(identifier), }; } static async lookupActor(identifier: string | URL): Promise<Actor | null> { return new Person({ id: new URL(identifier), }); } static async sendDM(session: AuthSession) {} static async deleteDM(session: AuthSession) {} async sendChatMessage(id: string, target: Actor, content: ChatMessage) {} async sendNote(id: string, target: Actor, content: Note) {} build( type: "ChatMessage" | "Note", opts: { id: string; one_time_code: string; createdAt: Date; target: Actor }, ): any {} } const _staticTypeCheck: typeof APub = APubStub; _staticTypeCheck.get(); // to ignore "unused variable" but still trigger type check No newline at end of file
backend/src/lib/express.ts +7 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import { APub } from "./apub/utils.js"; import { APIAdminRouter } from "./api_admin.js"; import { Handoff } from "../handoff/index.js"; import { DocLinks } from "./DocLinks.js"; import DevController from "../controllers/DevController.js"; export const app = express(); Loading Loading @@ -126,6 +127,10 @@ const interactionMiddleware = ( }; if (process.env.NODE_ENV === "development") { console.log( "Development Mode: /_dev & /api/dev endpoints have been activated", ); // expose the internals of the interaction middleware for the vite dev server to access app.post("/_dev/interaction/:uid", async (req, res) => { const middleware = await interactionMiddleware(req, res); Loading @@ -150,6 +155,8 @@ if (process.env.NODE_ENV === "development") { break; } }); app.use("/api/dev", DevController); } app.use("/interaction/:uid", async (req, res, next) => { Loading
backend/src/lib/oidc.ts +10 −4 Original line number Diff line number Diff line import Provider from "oidc-provider"; import fs from "node:fs"; import * as OIDClient from "openid-client"; import { PrismaAdapter } from "./adapter.js"; import { Issuer } from "openid-client"; import { IInstance, getInstanceMeta } from "./instance/instanceMeta.js"; import { IProfile, getUserMeta } from "./instance/userMeta.js"; import { ShadowAPI, UserLoginError } from "./shadow.js"; Loading Loading @@ -231,15 +231,21 @@ oidc.use(async (ctx, next) => { * @returns */ export const doesInstanceSupportOIDC = async (instance_hostname: string) => { let issuer: Issuer; let issuer: OIDClient.Configuration; try { issuer = await Issuer.discover("https://" + instance_hostname); // we pass an empty string into the client_id as we will // not be using this instance to perform requests, // only to check if the instance supports openid issuer = await OIDClient.discovery( new URL("https://" + instance_hostname), "", ); } catch (e) { return false; } if (typeof issuer.metadata.registration_endpoint === "undefined") { if (typeof issuer.serverMetadata().registration_endpoint === "undefined") { return false; } Loading