obliviously.., duh!

~^▼^~

auth.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";

// TODO(robin): automate this. 
// See https://servers.somtoday.nl/organisaties.json
const TENANT_ID = "ec284fda-3d0f-4f54-a77e-91d94b94ff1a";

let params;

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 username = Bun.argv[2];
const password = Bun.argv[3];

if(!username || !password) {
    process.stderr.write("You wanna log in or what?\n");
    process.exit(1);
}

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

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()}`;
await page.goto(url);

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

// Submit the final page, triggering a redirect to REDIRECT_URI
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) {
    process.stderr.write("Could not find code. Sowwy!\n");
    process.exit(1);
}

const response = await 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"
    })
});

if(!response.ok) {
    process.stderr.write("Failed to obtain access token. Got:\n");
    console.log(response);
}

const body = await response.text()
process.stdout.write(body);

TypeScript script for authenticating with the Somtoday API. Utilizes the amazing Bun runtime.