Files
railiance-platform/helm/openbao-ui-overlay/callback.js
tegwick 50799938db fix(openbao-ui): handle OIDC callback without Ember popup flow
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.
2026-06-19 21:18:34 +02:00

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();
})();