Files
gcp-wif-test/vault_oidc_login.mjs
T
2026-10-17 21:14:13 +02:00

92 lines
3.1 KiB
JavaScript

#!/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);