obliviously.., duh!

~^▼^~

endpoint.ts

#!/usr/bin/env -S bun

import crypto from "node:crypto";
import puppeteer from "puppeteer";

const RANDOM_BYTES = 32;
const CLIENT_ID = "D50E0C06-32D1-4B41-A137-A9A850C892C2";
const SCOPE = "openid";
const APP = "somtodayleerling";
const ENDPOINT = "https://inloggen.somtoday.nl/oauth2";
const REDIRECT_URI = `${APP}://oauth/callback`;
const STATE = "boobswow";

interface Params {
  tenant_id: string,
  username: string,
  password: string
}

function base64_url_encode(buffer: Buffer): string {
  return buffer.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

function sha256sum(buffer: Buffer | string): Buffer {
  return crypto.createHash("sha256").update(buffer).digest();
}

const server = Bun.serve({
  port: 4000,
  async fetch(req) {
    if (req.method !== "POST") {
      return new Response("Method not supported.", { status: 405 });
    }

    const { tenant_id, username, password }: Params = await req.json();

    if (!tenant_id) return new Response("Missing 'tenant_id'.", { status: 400 });
    if (!username) return new Response("Missing 'username'.", { status: 400 });
    if (!password) return new Response("Missing 'password'.", { status: 400 });

    const code_verifier = base64_url_encode(crypto.randomBytes(RANDOM_BYTES));
    const code_challenge = base64_url_encode(sha256sum(code_verifier));

    const params = new URLSearchParams({
      response_type: "code",
      prompt: "login",
      client_id: CLIENT_ID,
      tenant_uuid: tenant_id,
      redirect_uri: REDIRECT_URI,
      scope: SCOPE,
      code_challenge: code_challenge,
      code_challenge_method: "S256",
      state: STATE,
      session: "no_session"
    });

    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    let code: string | null = null;
    let processed = false;

    await page.on("response", (response) => {
      const location = response.headers()["location"];
      if (!location || !location.startsWith(APP)) return;

      const url = new URL(location);
      processed = true;
      code = url.searchParams.get("code");
    });

    const url = `${ENDPOINT}/authorize?${params.toString()}`;

    process.stdout.write(`Visiting ${url}`);
    await page.goto(url);

    await page.type("#usernameField", username);
    await page.click("a[type='submit']");
    await page.type("#password-field", password);
    await page.click("a[type='submit']");

    while (!processed) {
      await Bun.sleep(5000);
      process.stdout.write("Waiting for the code... (it's taking a while)\n");
    }

    await browser.close();

    if (!code) {
      return new Response("Could not find 'code'. Sowwy!", { status: 500 });
    }

    return fetch(`${ENDPOINT}/token`, {
      method: "POST",
      body: new URLSearchParams({
        grant_type: "authorization_code",
        code: code,
        code_verifier: code_verifier,
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scope: SCOPE,
        session: "no_session"
      })
    });
  }
});

console.log(`Listening on ${server.url}`);

API that handles authentication with Somtoday API.