OpenBao's Ember UI expects OIDC to complete in a popup and postMessage to window.opener. The standalone KeyCape login uses a full-page redirect, so the callback now exchanges the authorization code directly, persists the UI token in localStorage, and redirects into the vault UI. Unauthenticated /ui/ loads also redirect to the standalone login page to avoid ?with= bounce loops.
142 lines
4.3 KiB
JavaScript
142 lines
4.3 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const PRESETS_URL = "/ui/platform-overlay/presets.json";
|
|
const TOKEN_PREFIX = "vault-";
|
|
const TOKEN_SEPARATOR = "☃";
|
|
const CLUSTER_ID = "1";
|
|
const DEFAULT_PRESETS = { mount: "netkingdom", role: "platform-admin" };
|
|
const OIDC_BACKEND = {
|
|
type: "oidc",
|
|
typeDisplay: "OIDC",
|
|
description: "Authenticate using JWT or OIDC provider.",
|
|
tokenPath: "client_token",
|
|
displayNamePath: "display_name",
|
|
formAttributes: ["role", "jwt"],
|
|
};
|
|
const POST_LOGIN_PATH = "/ui/vault/vault/secrets";
|
|
|
|
function parseCallbackContext() {
|
|
const match = window.location.pathname.match(
|
|
/\/ui\/vault\/auth\/(.+)\/oidc\/callback\/?$/
|
|
);
|
|
if (!match) {
|
|
throw new Error("Unsupported OIDC callback path");
|
|
}
|
|
|
|
const mount = decodeURIComponent(match[1]).replace(/\/$/, "");
|
|
const params = new URLSearchParams(window.location.search);
|
|
let state = params.get("state") || "";
|
|
let namespace = "";
|
|
|
|
if (state.includes(",ns=")) {
|
|
const parts = state.split(",ns=");
|
|
state = parts[0];
|
|
namespace = parts[1] || "";
|
|
}
|
|
|
|
const code = params.get("code");
|
|
if (!mount || !state || !code) {
|
|
throw new Error("OIDC callback missing required parameters");
|
|
}
|
|
|
|
return { mount, state, code, namespace };
|
|
}
|
|
|
|
async function loadPresets() {
|
|
try {
|
|
const response = await fetch(PRESETS_URL, { cache: "no-store" });
|
|
if (!response.ok) return { ...DEFAULT_PRESETS };
|
|
return { ...DEFAULT_PRESETS, ...(await response.json()) };
|
|
} catch (_error) {
|
|
return { ...DEFAULT_PRESETS };
|
|
}
|
|
}
|
|
|
|
async function exchangeOidc({ mount, state, code }) {
|
|
const query = new URLSearchParams({ state, code });
|
|
const response = await fetch(
|
|
`/v1/auth/${encodeURIComponent(mount)}/oidc/callback?${query}`,
|
|
{ method: "GET", headers: { Accept: "application/json" } }
|
|
);
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const detail =
|
|
payload?.errors?.[0] ||
|
|
`OIDC callback exchange failed (${response.status})`;
|
|
throw new Error(detail);
|
|
}
|
|
|
|
const auth = payload.auth || payload.data?.auth || payload.data;
|
|
if (!auth?.client_token) {
|
|
throw new Error("OIDC callback did not return a client token");
|
|
}
|
|
|
|
return auth;
|
|
}
|
|
|
|
function persistAuthToken(auth, presets) {
|
|
const mount = presets.mount || "netkingdom";
|
|
const selectedAuth = mount.endsWith("/") ? mount : `${mount}/`;
|
|
const tokenName = `${TOKEN_PREFIX}oidc${TOKEN_SEPARATOR}${CLUSTER_ID}`;
|
|
const namespacePath = auth.namespace_path?.replace(/\/$/, "") || "";
|
|
const tokenData = {
|
|
userRootNamespace: namespacePath,
|
|
displayName:
|
|
auth.display_name ||
|
|
auth.metadata?.name ||
|
|
auth.metadata?.username ||
|
|
"KeyCape",
|
|
backend: OIDC_BACKEND,
|
|
token: auth.client_token,
|
|
policies: auth.policies || [],
|
|
renewable: Boolean(auth.renewable),
|
|
entity_id: auth.entity_id,
|
|
};
|
|
|
|
if (tokenData.renewable && auth.lease_duration) {
|
|
tokenData.ttl = auth.lease_duration;
|
|
tokenData.tokenExpirationEpoch =
|
|
Date.now() + auth.lease_duration * 1000;
|
|
}
|
|
|
|
window.localStorage.setItem("selectedAuth", selectedAuth);
|
|
window.localStorage.setItem(tokenName, JSON.stringify(tokenData));
|
|
}
|
|
|
|
function showError(message) {
|
|
const root = document.getElementById("callback-root");
|
|
if (!root) return;
|
|
root.innerHTML = `
|
|
<main class="login-card">
|
|
<h1>Sign-in failed</h1>
|
|
<p class="login-error is-visible">${message}</p>
|
|
<button class="login-button" type="button" id="callback-retry">
|
|
Back to sign in
|
|
</button>
|
|
</main>
|
|
`;
|
|
document.getElementById("callback-retry")?.addEventListener("click", () => {
|
|
window.location.assign("/ui/vault/auth");
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
const presets = await loadPresets();
|
|
const context = parseCallbackContext();
|
|
const auth = await exchangeOidc(context);
|
|
persistAuthToken(auth, presets);
|
|
window.location.replace(POST_LOGIN_PATH);
|
|
} catch (error) {
|
|
showError(
|
|
error instanceof Error
|
|
? error.message
|
|
: "OIDC sign-in failed. Contact your administrator."
|
|
);
|
|
}
|
|
}
|
|
|
|
init();
|
|
})(); |