#!/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.