#!/usr/bin/env node import { randomBytes } from "node:crypto"; import { createServer } from "node:http"; import { parseArgs } from "node:util"; const { values } = parseArgs({ options: { role: { type: "string" }, mount: { type: "string", default: "oidc" }, "redirect-uri": { type: "string", default: "http://localhost:8250/oidc/callback" }, timeout: { type: "string", default: "300" }, }, }); const address = process.env.VAULT_ADDR; if (!values.role || !address) { console.error( "Usage: VAULT_ADDR=... vault_oidc_login.mjs --role ROLE [--mount MOUNT] [--redirect-uri URI] [--timeout SECONDS]", ); process.exit(1); } const redirect = new URL(values["redirect-uri"]); const clientNonce = randomBytes(16).toString("hex"); const base = `${address}/v1/auth/${values.mount}/oidc`; // Ask Vault for the sign-in URL of the identity provider. const authResponse = await fetch(`${base}/auth_url`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role: values.role, redirect_uri: values["redirect-uri"], client_nonce: clientNonce, }), }); if (!authResponse.ok) { console.error(`Vault returned ${authResponse.status}: ${await authResponse.text()}`); process.exit(1); } const { data } = await authResponse.json(); console.error(`Open this URL in the browser or in the browser profile of your choice:\n\n ${data.auth_url}\n`); console.error("Waiting for the sign-in to complete..."); // Wait for the identity provider to redirect the browser to the local callback. const params = await new Promise((resolve, reject) => { const server = createServer((request, response) => { const url = new URL(request.url, redirect); if (url.pathname !== redirect.pathname) { response.writeHead(404).end(); return; } response.writeHead(200, { "Content-Type": "text/plain", Connection: "close" }); response.end("The sign-in is complete, you can close this window.\n"); server.close(); server.closeAllConnections(); resolve(url.searchParams); }); server.listen(Number(redirect.port), redirect.hostname); setTimeout(() => { server.close(); server.closeAllConnections(); reject(new Error("Timed out waiting for the sign-in")); }, Number(values.timeout) * 1000).unref(); }).catch((error) => { console.error(error.message); process.exit(1); }); if (params.get("error")) { console.error(`The identity provider returned ${params.get("error")}: ${params.get("error_description")}`); process.exit(1); } // Let Vault exchange the authorization code and issue the token. const callback = new URLSearchParams({ state: params.get("state"), code: params.get("code"), client_nonce: clientNonce, }); const callbackResponse = await fetch(`${base}/callback?${callback}`); if (!callbackResponse.ok) { console.error(`Vault returned ${callbackResponse.status}: ${await callbackResponse.text()}`); process.exit(1); } const { auth } = await callbackResponse.json(); console.error( `Login successful. Policies: ${auth.policies.join(", ")}. The token is valid for ${auth.lease_duration} seconds.`, ); console.log(auth.client_token);