92 lines
3.1 KiB
JavaScript
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);
|