Fix OpenBao login falling back to token auth
Add synchronous redirect-bootstrap, direct KeyCape OIDC on sign-in, and mount watching so the UI no longer lands on ?with=token when netkingdom is hidden from unauthenticated mount listing. Document listing_visibility tune helper.
This commit is contained in:
@@ -330,6 +330,17 @@ After an OpenBao image or chart upgrade, follow
|
||||
`patches/<version>/manifest.sha256` fingerprints if upstream login markup
|
||||
changed.
|
||||
|
||||
OIDC mounts must be visible to the unauthenticated UI listing or Ember falls
|
||||
back to token auth (`?with=token`). Apply once per cluster:
|
||||
|
||||
```bash
|
||||
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \
|
||||
scripts/openbao-tune-auth-listing.sh
|
||||
```
|
||||
|
||||
The login overlay also redirects to `?with=netkingdom/` and starts KeyCape OIDC
|
||||
directly when the operator clicks **Sign in with KeyCape**.
|
||||
|
||||
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
|
||||
returns to:
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ http {
|
||||
|
||||
sub_filter_types text/html;
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<link rel="stylesheet" href="/ui/platform-overlay/overlay.css"><script src="/ui/platform-overlay/overlay.js" defer></script></head>';
|
||||
sub_filter '</head>' '<script src="/ui/platform-overlay/redirect-bootstrap.js"></script><link rel="stylesheet" href="/ui/platform-overlay/overlay.css"><script src="/ui/platform-overlay/overlay.js"></script></head>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ html.keycape-overlay-active label[for="custom-path"],
|
||||
html.keycape-overlay-active #namespace,
|
||||
html.keycape-overlay-active #role,
|
||||
html.keycape-overlay-active #custom-path,
|
||||
html.keycape-overlay-active #token,
|
||||
html.keycape-overlay-active #username,
|
||||
html.keycape-overlay-active #password,
|
||||
html.keycape-overlay-active select[name="auth-method"],
|
||||
html.keycape-overlay-active .auth-form .box.has-slim-padding.is-shadowless,
|
||||
html.keycape-overlay-active .auth-form .has-bottom-margin-s {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
const PRESETS_URL = "/ui/platform-overlay/presets.json";
|
||||
const MAX_APPLY_ATTEMPTS = 40;
|
||||
const APPLY_INTERVAL_MS = 250;
|
||||
const MOUNT_WATCH_MS = 500;
|
||||
const MOUNT_WATCH_MAX = 24;
|
||||
const DEFAULT_PRESETS = {
|
||||
namespace: "",
|
||||
method: "oidc",
|
||||
@@ -18,7 +20,9 @@
|
||||
let presets = { ...DEFAULT_PRESETS };
|
||||
let applyAttempts = 0;
|
||||
let applyTimer = null;
|
||||
let mountWatchTimer = null;
|
||||
let overlayApplied = false;
|
||||
let signInHandlerInstalled = false;
|
||||
|
||||
function isAuthPage() {
|
||||
const path = window.location.pathname;
|
||||
@@ -28,6 +32,40 @@
|
||||
);
|
||||
}
|
||||
|
||||
function normalizedMount(value) {
|
||||
return (value || "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function desiredMount() {
|
||||
return normalizedMount(presets.mount || "netkingdom");
|
||||
}
|
||||
|
||||
function currentMountFromQuery() {
|
||||
return normalizedMount(
|
||||
new URLSearchParams(window.location.search).get("with") || ""
|
||||
);
|
||||
}
|
||||
|
||||
function ensureAuthMountSelected() {
|
||||
const mount = desiredMount();
|
||||
const current = currentMountFromQuery();
|
||||
|
||||
if (current === mount || current === "keycape") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isAuthPage() || window.location.pathname.includes("/oidc/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("with", `${mount}/`);
|
||||
window.location.replace(
|
||||
`${window.location.pathname}?${params.toString()}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideNode(node) {
|
||||
if (!node || node.dataset.keycapeOverlayHidden === "true") return;
|
||||
const field =
|
||||
@@ -45,30 +83,63 @@
|
||||
if (!input || input.dataset.keycapeOverlayPreset === value) return;
|
||||
input.value = value;
|
||||
input.dataset.keycapeOverlayPreset = value;
|
||||
// Fire once so Ember picks up the preset without a mutation feedback loop.
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
|
||||
function ensureAuthMountSelected() {
|
||||
async function redirectToKeyCape() {
|
||||
const mount = presets.mount || "netkingdom";
|
||||
const withValue = mount.endsWith("/") ? mount : `${mount}/`;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const current = params.get("with") || "";
|
||||
const role = presets.role || "platform-admin";
|
||||
const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`;
|
||||
|
||||
if (current.replace(/\/$/, "") === withValue.replace(/\/$/, "")) {
|
||||
return;
|
||||
const response = await fetch(`/v1/auth/${mount}/oidc/auth_url`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
role,
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OIDC auth_url request failed (${response.status})`);
|
||||
}
|
||||
|
||||
if (!isAuthPage() || window.location.pathname.includes("/oidc/")) {
|
||||
return;
|
||||
const payload = await response.json();
|
||||
const authUrl = payload?.data?.auth_url;
|
||||
if (!authUrl) {
|
||||
throw new Error("OIDC auth_url missing from OpenBao response");
|
||||
}
|
||||
|
||||
params.set("with", withValue);
|
||||
const next = `${window.location.pathname}?${params.toString()}`;
|
||||
if (next !== `${window.location.pathname}${window.location.search}`) {
|
||||
window.location.replace(next);
|
||||
}
|
||||
window.location.assign(authUrl);
|
||||
}
|
||||
|
||||
function installKeyCapeSignInHandler() {
|
||||
if (signInHandlerInstalled) return;
|
||||
signInHandlerInstalled = true;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
if (!isAuthPage()) return;
|
||||
|
||||
const button = event.target.closest(
|
||||
'#auth-submit, button[data-test="auth-submit"], form#auth-form button[type="submit"]'
|
||||
);
|
||||
if (!button) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
button.disabled = true;
|
||||
button.classList.add("is-loading");
|
||||
redirectToKeyCape().catch(() => {
|
||||
button.disabled = false;
|
||||
button.classList.remove("is-loading");
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function loginShellReady() {
|
||||
@@ -101,6 +172,12 @@
|
||||
.querySelectorAll('#role, input[name="role"], label[for="role"]')
|
||||
.forEach(hideNode);
|
||||
|
||||
document
|
||||
.querySelectorAll(
|
||||
'#token, input[name="token"], label[for="token"], #username, input[name="username"], #password, input[name="password"]'
|
||||
)
|
||||
.forEach(hideNode);
|
||||
|
||||
document.querySelectorAll("nav.tabs").forEach((el) => {
|
||||
if (el.dataset.keycapeOverlayHidden === "true") return;
|
||||
el.style.display = "none";
|
||||
@@ -151,6 +228,7 @@
|
||||
}
|
||||
|
||||
document.documentElement.classList.add("keycape-overlay-active");
|
||||
installKeyCapeSignInHandler();
|
||||
|
||||
if (loginShellReady()) {
|
||||
overlayApplied = true;
|
||||
@@ -167,6 +245,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
function stopMountWatch() {
|
||||
if (mountWatchTimer !== null) {
|
||||
window.clearInterval(mountWatchTimer);
|
||||
mountWatchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function watchAuthMount() {
|
||||
stopMountWatch();
|
||||
let checks = 0;
|
||||
|
||||
mountWatchTimer = window.setInterval(() => {
|
||||
checks += 1;
|
||||
if (ensureAuthMountSelected() || checks >= MOUNT_WATCH_MAX) {
|
||||
stopMountWatch();
|
||||
}
|
||||
}, MOUNT_WATCH_MS);
|
||||
}
|
||||
|
||||
function scheduleApply() {
|
||||
stopApplyLoop();
|
||||
applyAttempts = 0;
|
||||
@@ -195,10 +292,16 @@
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await loadPresets();
|
||||
if (!isAuthPage()) return;
|
||||
|
||||
ensureAuthMountSelected();
|
||||
await loadPresets();
|
||||
|
||||
if (ensureAuthMountSelected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
watchAuthMount();
|
||||
installKeyCapeSignInHandler();
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", scheduleApply, {
|
||||
|
||||
23
helm/openbao-ui-overlay/redirect-bootstrap.js
Normal file
23
helm/openbao-ui-overlay/redirect-bootstrap.js
Normal file
@@ -0,0 +1,23 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var path = window.location.pathname;
|
||||
if (path.indexOf("/oidc/") !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/\/ui\/vault\/auth(?:\/|$)/.test(path) && !/\/ui\/?$/.test(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var current = (params.get("with") || "").replace(/\/$/, "");
|
||||
if (current === "netkingdom" || current === "keycape") {
|
||||
return;
|
||||
}
|
||||
|
||||
params.set("with", "netkingdom/");
|
||||
window.location.replace(
|
||||
window.location.pathname + "?" + params.toString()
|
||||
);
|
||||
})();
|
||||
49
scripts/openbao-tune-auth-listing.sh
Executable file
49
scripts/openbao-tune-auth-listing.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
|
||||
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
|
||||
MOUNTS="${OPENBAO_AUTH_LISTING_MOUNTS:-netkingdom keycape}"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/openbao-tune-auth-listing.sh
|
||||
|
||||
Sets listing_visibility=unauth on configured OIDC auth mounts so the OpenBao
|
||||
browser UI can discover netkingdom without falling back to token auth.
|
||||
|
||||
Environment:
|
||||
OPENBAO_TOKEN_FILE Token file with platform-admin or root token
|
||||
OPENBAO_AUTH_LISTING_MOUNTS Space-separated mount paths. Default: netkingdom keycape
|
||||
USAGE
|
||||
}
|
||||
|
||||
read_token() {
|
||||
if [ -n "$TOKEN_FILE" ]; then
|
||||
head -n 1 "$TOKEN_FILE"
|
||||
return
|
||||
fi
|
||||
local token
|
||||
read -r -s -p "OpenBao token: " token
|
||||
printf '\n' >&2
|
||||
printf '%s\n' "$token"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pod="${OPENBAO_RELEASE}-0"
|
||||
token="$(read_token)"
|
||||
|
||||
for mount in $MOUNTS; do
|
||||
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
|
||||
bao write "sys/auth/${mount}/tune" listing_visibility=unauth
|
||||
printf '[OK] auth/%s listing_visibility=unauth\n' "$mount"
|
||||
done
|
||||
|
||||
printf '\nVerify unauthenticated UI mount listing:\n'
|
||||
curl -fsS "https://bao.coulomb.social/v1/sys/internal/ui/mounts" | python3 -m json.tool
|
||||
@@ -27,7 +27,7 @@ if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for required in overlay.css overlay.js presets.json nginx.conf VERSION; do
|
||||
for required in overlay.css overlay.js redirect-bootstrap.js presets.json nginx.conf VERSION; do
|
||||
if [ ! -f "$OVERLAY_DIR/$required" ]; then
|
||||
echo "missing overlay asset: $OVERLAY_DIR/$required" >&2
|
||||
exit 1
|
||||
@@ -47,6 +47,7 @@ $KUBECTL create configmap openbao-ui-overlay \
|
||||
--namespace "$OPENBAO_NAMESPACE" \
|
||||
--from-file="$OVERLAY_DIR/overlay.css" \
|
||||
--from-file="$OVERLAY_DIR/overlay.js" \
|
||||
--from-file="$OVERLAY_DIR/redirect-bootstrap.js" \
|
||||
--from-file="$OVERLAY_DIR/presets.json" \
|
||||
--from-file="$OVERLAY_DIR/VERSION" \
|
||||
--dry-run=client -o yaml | $KUBECTL apply -f -
|
||||
|
||||
@@ -58,6 +58,11 @@ overlay_js="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.js")"
|
||||
overlay_css="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.css")"
|
||||
presets_json="$(curl -fsS "$BASE_URL/ui/platform-overlay/presets.json")"
|
||||
|
||||
require_pattern \
|
||||
"index.html injects redirect bootstrap" \
|
||||
"$index_html" \
|
||||
'/ui/platform-overlay/redirect-bootstrap\.js'
|
||||
|
||||
require_pattern \
|
||||
"index.html injects overlay.js" \
|
||||
"$index_html" \
|
||||
@@ -73,6 +78,11 @@ require_pattern \
|
||||
"$overlay_js" \
|
||||
'keycape-overlay-active'
|
||||
|
||||
require_pattern \
|
||||
"overlay.js starts direct KeyCape OIDC redirect" \
|
||||
"$overlay_js" \
|
||||
'oidc/auth_url'
|
||||
|
||||
require_pattern \
|
||||
"presets.json targets netkingdom mount" \
|
||||
"$presets_json" \
|
||||
|
||||
Reference in New Issue
Block a user