Commit 3587424a authored by Grant's avatar Grant
Browse files

add devhomeutils and initial apub stub

parent 12393c64
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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": {
+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;
+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
+7 −0
Original line number Diff line number Diff line
@@ -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();

@@ -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);
@@ -150,6 +155,8 @@ if (process.env.NODE_ENV === "development") {
        break;
    }
  });

  app.use("/api/dev", DevController);
}

app.use("/interaction/:uid", async (req, res, next) => {
+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";
@@ -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