Commit 5f9a0de8 authored by Grant's avatar Grant
Browse files

expiring codes & deleting sent objects & don't create new sessions if one exists

parent 939870b7
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
    "body-parser": "^1.20.2",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "cron": "^4.3.0",
    "express": "^4.19.2",
    "express-session": "^1.18.0",
    "ioredis": "^5.6.1",
+86 −0
Original line number Diff line number Diff line
import { AuthSession as DBAuthSession } from "@prisma/client";
import { prisma } from "../lib/prisma.js";

export class AuthSession {
  static generateCode(): string {
    return "."
      .repeat(5)
      .split("")
      .map(() => Math.floor(Math.random() * 10))
      .join("");
  }
  static async create(handle: `${string}@${string}`): Promise<AuthSession> {
    // day in seconds
    const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);

    const session = await prisma.authSession.create({
      data: {
        one_time_code: this.generateCode(),
        user_sub: handle,
        expiresAt,
      },
    });

    return new AuthSession(session);
  }
  static async getExpired() {
    const sessions = await prisma.authSession.findMany({
      where: {
        expiresAt: {
          lte: new Date(),
        },
      },
    });
    return sessions.map((d) => new AuthSession(d));
  }
  static async getActive(
    handle: `${string}@${string}`
  ): Promise<AuthSession | null> {
    const session = await prisma.authSession.findFirst({
      where: {
        user_sub: handle,
        expiresAt: {
          gt: new Date(),
        },
      },
    });

    if (!session) return null;

    return new AuthSession(session);
  }

  private _id: string;
  private _user_sub: `${string}@${string}`;
  private _createdAt: Date;
  private _expiresAt: Date;
  private _code: string;

  private constructor(session: DBAuthSession) {
    this._id = session.id;
    this._user_sub = session.user_sub as any;
    this._createdAt = session.createdAt;
    this._expiresAt = session.expiresAt;
    this._code = session.one_time_code;
  }

  get id() {
    return this._id;
  }

  get user_sub() {
    return this._user_sub;
  }

  get createdAt() {
    return this._createdAt;
  }

  get expiresAt() {
    return this._expiresAt;
  }

  get code() {
    return this._code;
  }
}
+2 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ import { configure, getConsoleSink } from "@logtape/logtape";
import { app as Express } from "./lib/express.js";
import { oidc } from "./lib/oidc.js";
import { FederationWorker } from "./lib/apub/worker.js";
import { Jobs } from "./jobs/jobs.js";

if (typeof process.env.SESSION_SECRET !== "string") {
  throw new Error("SESSION_SECRET is not defined");
@@ -54,6 +55,7 @@ await configure({
});

if (process.env.NODE_TYPE === "worker") {
  Jobs.start();
  FederationWorker.create().then(() => {
    console.log("FederationWorker started");
  });
+30 −0
Original line number Diff line number Diff line
import { CronJob } from "cron";
import { AuthSession } from "../controllers/AuthSession.js";
import { APub } from "../lib/apub/utils.js";
import { prisma } from "../lib/prisma.js";

export class Jobs {
  private static instance: Jobs;

  static start() {
    this.instance = new Jobs();

    CronJob.from({
      name: "Destroy expired tokens",
      cronTime: "*/5 * * * *",
      onTick: this.instance.handleExpiredTokens.bind(this),
      start: true,
    });
  }

  async handleExpiredTokens() {
    const expired = await AuthSession.getExpired();
    console.debug(`Deleting ${expired.length} expired tokens`);

    for (const session of expired) {
      console.debug(`Deleting ${session.id}`);
      await APub.deleteDM(session);
      await prisma.authSession.delete({ where: { id: session.id } });
    }
  }
}
+19 −12
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import { IProfile, getUserMeta } from "./instance/userMeta.js";
import { IInstance, getInstanceMeta } from "./instance/instanceMeta.js";
import { ShadowAPI, UserLoginError } from "./shadow.js";
import { APub } from "./apub/utils.js";
import { AuthSession } from "../controllers/AuthSession.js";

const app = express.Router();

@@ -130,20 +131,26 @@ app.post("/login/step/username", async (req, res) => {
  // this is the prompt for the user
  req.session.login.prompt = "ENTER_CODE";

  const code = "."
    .repeat(5)
    .split("")
    .map(() => Math.floor(Math.random() * 10))
    .join("");

  // TODO: prevent spam sending codes to someone

  const session = await prisma.authSession.create({
  const existing = await AuthSession.getActive(`${username}@${instance}`);

  if (existing) {
    // if there's an active session, don't create another one
    req.session.login.session_id = existing.id;
    req.session.save(() => {
      res.send({
        success: true,
        data: {
      one_time_code: code,
      user_sub: [username, instance].join("@"),
          session_id: session.id,
          account: APub.accountHandle,
        },
      });
    });
    return;
  }

  const session = await AuthSession.create(`${username}@${instance}`);
  req.session.login.session_id = session.id;

  try {
Loading