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.
This commit is contained in:
142
helm/openbao-ui-overlay/callback.js
Normal file
142
helm/openbao-ui-overlay/callback.js
Normal file
@@ -0,0 +1,142 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in New Issue
Block a user