fix(openbao-ui): serve standalone KeyCape login at /ui/vault/auth
Ember's auth route bounces between ?with=netkingdom/ and ?with=token when OIDC mounts are hidden from the unauthenticated listing. Bypass Ember on the bare auth path with a static login page that calls auth_url directly; OIDC callbacks still proxy to the OpenBao UI.
This commit is contained in:
@@ -338,8 +338,11 @@ 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 gateway serves a standalone KeyCape login page at `/ui/vault/auth` so Ember
|
||||
never handles the bare auth route (avoids `?with=token` / `?with=netkingdom/`
|
||||
bounce when OIDC mounts are hidden from the unauthenticated listing). Clicking
|
||||
**Sign in with KeyCape** calls `auth_url` and redirects to KeyCape directly.
|
||||
OIDC callbacks under `/ui/vault/auth/<mount>/oidc/` still proxy to the OpenBao UI.
|
||||
|
||||
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
|
||||
returns to:
|
||||
|
||||
@@ -31,8 +31,9 @@ behaviour.
|
||||
| `VERSION` | OpenBao image tag this overlay targets (`openbao-values.yaml`) |
|
||||
| `presets.json` | Hidden login defaults (`netkingdom`, `platform-admin`, …) |
|
||||
| `overlay.css` | Hide raw OpenBao login fields |
|
||||
| `overlay.js` | Apply presets, branding, direct KeyCape OIDC sign-in |
|
||||
| `nginx.conf` | Gateway proxy + HTML injection |
|
||||
| `overlay.js` | Apply presets, branding on post-login Ember pages |
|
||||
| `login.html` / `login.js` / `login.css` | Standalone KeyCape login at `/ui/vault/auth` |
|
||||
| `nginx.conf` | Gateway proxy + standalone auth page + HTML injection |
|
||||
| `patches/<version>/manifest.sha256` | Upstream UI fingerprints for drift detection |
|
||||
|
||||
## Deploy
|
||||
|
||||
85
helm/openbao-ui-overlay/login.css
Normal file
85
helm/openbao-ui-overlay/login.css
Normal file
@@ -0,0 +1,85 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f0f2f5;
|
||||
--card: #ffffff;
|
||||
--text: #1f2a37;
|
||||
--muted: #5b6b7c;
|
||||
--border: #d9dee3;
|
||||
--accent: #1565c0;
|
||||
--accent-hover: #0d47a1;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(100%, 26rem);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.08);
|
||||
padding: 2rem 1.75rem 1.75rem;
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.login-card p {
|
||||
margin: 0 0 1.5rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 0.8rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border-radius: 6px;
|
||||
background: #fdecea;
|
||||
color: #8a1c13;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-error.is-visible {
|
||||
display: block;
|
||||
}
|
||||
22
helm/openbao-ui-overlay/login.html
Normal file
22
helm/openbao-ui-overlay/login.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sign in with KeyCape</title>
|
||||
<link rel="stylesheet" href="/ui/platform-overlay/login.css" />
|
||||
<script src="/ui/platform-overlay/login.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-card">
|
||||
<h1 id="login-title">Sign in with KeyCape</h1>
|
||||
<p id="login-banner">
|
||||
Platform operators authenticate through KeyCape at kc.coulomb.social.
|
||||
</p>
|
||||
<button id="login-submit" class="login-button" type="button">
|
||||
Sign in with KeyCape
|
||||
</button>
|
||||
<div id="login-error" class="login-error" role="alert"></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
86
helm/openbao-ui-overlay/login.js
Normal file
86
helm/openbao-ui-overlay/login.js
Normal file
@@ -0,0 +1,86 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const PRESETS_URL = "/ui/platform-overlay/presets.json";
|
||||
const DEFAULT_PRESETS = {
|
||||
mount: "netkingdom",
|
||||
role: "platform-admin",
|
||||
title: "Sign in with KeyCape",
|
||||
signInLabel: "Sign in with KeyCape",
|
||||
banner:
|
||||
"Platform operators authenticate through KeyCape at kc.coulomb.social.",
|
||||
};
|
||||
|
||||
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 redirectToKeyCape(presets) {
|
||||
const mount = presets.mount || "netkingdom";
|
||||
const role = presets.role || "platform-admin";
|
||||
const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`;
|
||||
|
||||
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})`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const authUrl = payload?.data?.auth_url;
|
||||
if (!authUrl) {
|
||||
throw new Error("OIDC auth_url missing from OpenBao response");
|
||||
}
|
||||
|
||||
window.location.assign(authUrl);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const error = document.getElementById("login-error");
|
||||
if (!error) return;
|
||||
error.textContent = message;
|
||||
error.classList.add("is-visible");
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const presets = await loadPresets();
|
||||
const title = document.getElementById("login-title");
|
||||
const banner = document.getElementById("login-banner");
|
||||
const button = document.getElementById("login-submit");
|
||||
|
||||
if (title) title.textContent = presets.title;
|
||||
if (banner) banner.textContent = presets.banner;
|
||||
if (button) button.textContent = presets.signInLabel;
|
||||
|
||||
if (!button) return;
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
await redirectToKeyCape(presets);
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
showError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Sign-in failed. Contact your administrator."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
@@ -26,6 +26,13 @@ http {
|
||||
add_header Cache-Control "public, max-age=300";
|
||||
}
|
||||
|
||||
# Standalone KeyCape login page — bypasses Ember auth route and ?with= bounce.
|
||||
location = /ui/vault/auth {
|
||||
alias /etc/nginx/overlay/login.html;
|
||||
default_type text/html;
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
# Static UI bundles and API calls bypass HTML injection and stay compressed.
|
||||
location ~ ^/(v1|ui/assets|ui/engines-dist|ui/favicon\.svg) {
|
||||
proxy_pass http://openbao_upstream;
|
||||
|
||||
@@ -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 login.css login.html login.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,9 @@ $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/login.css" \
|
||||
--from-file="$OVERLAY_DIR/login.html" \
|
||||
--from-file="$OVERLAY_DIR/login.js" \
|
||||
--from-file="$OVERLAY_DIR/presets.json" \
|
||||
--from-file="$OVERLAY_DIR/VERSION" \
|
||||
--dry-run=client -o yaml | $KUBECTL apply -f -
|
||||
|
||||
@@ -52,6 +52,19 @@ require_pattern() {
|
||||
ok "$label"
|
||||
}
|
||||
|
||||
step "Standalone login page"
|
||||
auth_html="$(curl -fsS "$BASE_URL/ui/vault/auth")"
|
||||
require_pattern \
|
||||
"auth page serves standalone KeyCape login" \
|
||||
"$auth_html" \
|
||||
'id="login-submit"|Sign in with KeyCape'
|
||||
|
||||
if grep -Eq 'vault-|engines-dist' <<<"$auth_html"; then
|
||||
err "auth page still serves Ember shell (expected standalone login.html)"
|
||||
exit 1
|
||||
fi
|
||||
ok "auth page is standalone login.html (no Ember shell)"
|
||||
|
||||
step "Overlay asset endpoints"
|
||||
index_html="$(curl -fsS "$BASE_URL/ui/")"
|
||||
overlay_js="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.js")"
|
||||
|
||||
Reference in New Issue
Block a user