<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Issuerd — Sign In</title>
<link rel="icon" type="image/svg+xml" href="/brand/logo-small.svg" media="(prefers-color-scheme: light)" />
<link rel="icon" type="image/svg+xml" href="/brand/logo-small-white.svg" media="(prefers-color-scheme: dark)" />
<link rel="stylesheet" href="/fonts/fonts.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--issuerd-font-body, 'Inter', system-ui, -apple-system, sans-serif);
background: var(--issuerd-bg, #0a0a0a);
color: var(--issuerd-fg, #fafafa);
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
width: 100%;
max-width: 384px;
padding: 16px;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 32px;
}
.logo svg {
height: 44px;
width: auto;
color: var(--issuerd-fg, #fafafa);
}
.card {
background: var(--issuerd-card-bg, #141414);
border: 1px solid var(--issuerd-card-border, #262626);
border-radius: var(--issuerd-card-radius, 12px);
padding: 32px;
}
.card-title {
font-family: var(--issuerd-font-display, 'Space Grotesk', system-ui, sans-serif);
font-size: 18px;
font-weight: 500;
color: var(--issuerd-fg, #fafafa);
text-align: center;
margin-bottom: 8px;
}
.realm-display {
text-align: center;
font-size: 13px;
color: var(--issuerd-accent, #00E5FF);
font-weight: 600;
margin-bottom: 20px;
letter-spacing: 0.02em;
}
.realm-display:empty {
display: none;
}
.banner {
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
text-align: center;
margin-bottom: 16px;
display: none;
}
.banner.success {
display: block;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
color: var(--issuerd-success, #22c55e);
}
.banner.error {
display: block;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: var(--issuerd-error, #ef4444);
}
label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--issuerd-muted, #a3a3a3);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
input {
width: 100%;
padding: 10px 14px;
background: var(--issuerd-input-bg, rgba(255, 255, 255, 0.03));
border: 1px solid var(--issuerd-input-border, #262626);
border-radius: var(--issuerd-input-radius, 8px);
color: var(--issuerd-fg, #fafafa);
font-size: 14px;
font-family: inherit;
margin-bottom: 16px;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
input:focus {
border-color: var(--issuerd-accent, #00E5FF);
box-shadow: 0 0 0 2px rgba(0, 229, 255, 0.18);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--issuerd-accent, #00E5FF) 25%, transparent);
}
input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button {
width: 100%;
padding: 12px;
background: var(--issuerd-button-bg, #ffffff);
color: var(--issuerd-button-fg, #0a0a0a);
border: none;
border-radius: var(--issuerd-button-radius, 8px);
font-size: 13px;
font-weight: 600;
font-family: inherit;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
transition: box-shadow 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 8px;
}
button:hover:not(:disabled) {
box-shadow: 0 0 20px var(--issuerd-button-glow, rgba(255, 255, 255, 0.15));
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(10, 10, 10, 0.3);
border-color: color-mix(in srgb, var(--issuerd-button-fg, #0a0a0a) 30%, transparent);
border-top-color: var(--issuerd-button-fg, #0a0a0a);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner.cyan {
border-color: rgba(255, 255, 255, 0.2);
border-color: color-mix(in srgb, var(--issuerd-accent, #00E5FF) 25%, transparent);
border-top-color: var(--issuerd-accent, #00E5FF);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.redirecting {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 40px;
}
.redirecting .spinner {
width: 32px;
height: 32px;
}
.redirecting span {
font-size: 14px;
color: var(--issuerd-muted, #a3a3a3);
}
.userinfo-card {
display: none;
}
.userinfo-card.active {
display: block;
}
.userinfo-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--issuerd-card-border, #262626);
font-size: 13px;
}
.userinfo-row:last-child {
border-bottom: none;
}
.userinfo-label {
color: var(--issuerd-muted, #a3a3a3);
font-weight: 500;
}
.userinfo-value {
color: var(--issuerd-fg, #fafafa);
word-break: break-all;
text-align: right;
max-width: 60%;
}
.secondary-btn {
background: transparent;
color: var(--issuerd-fg, #fafafa);
border: 1px solid var(--issuerd-fg, #fafafa);
margin-top: 16px;
}
.secondary-btn:hover:not(:disabled) {
box-shadow: 0 0 20px var(--issuerd-button-glow, rgba(255, 255, 255, 0.15));
}
.remember-me {
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 13px;
font-weight: 400;
color: var(--issuerd-muted, #a3a3a3);
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.remember-me input[type="checkbox"] {
width: auto;
margin-bottom: 0;
accent-color: var(--issuerd-accent, #00E5FF);
cursor: pointer;
}
.login-links {
justify-content: space-between;
align-items: center;
gap: 16px;
margin-top: 20px;
font-size: 13px;
}
.login-links a {
color: var(--issuerd-muted, #a3a3a3);
text-decoration: none;
transition: color 0.2s;
}
.login-links a:hover {
color: var(--issuerd-fg, #fafafa);
}
.idp-section {
margin-top: 24px;
}
.idp-divider {
display: flex;
align-items: center;
gap: 12px;
color: var(--issuerd-muted, #a3a3a3);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 16px;
}
.idp-divider::before,
.idp-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--issuerd-card-border, #262626);
}
.idp-btn {
display: block;
width: 100%;
padding: 12px;
background: transparent;
color: var(--issuerd-fg, #fafafa);
border: 1px solid var(--issuerd-card-border, #262626);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
text-align: center;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: border-color 0.2s, background 0.2s;
margin-bottom: 8px;
}
.idp-btn:last-child {
margin-bottom: 0;
}
.idp-btn:hover {
border-color: var(--issuerd-accent, #00E5FF);
background: var(--issuerd-input-bg, rgba(255, 255, 255, 0.05));
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1037 253" fill="currentColor" role="img" aria-label="issuerd">
<path d="m 35.978971,13.494282 c 4.606,-1.15 13.2717,-0.4855 17.6797,1.3555 4.7098,1.9692 10.407,7.3037 12.7949,11.9834 1.1399,2.2362 2.2926,6.7961 2.5606,10.1318 0.9204,11.4746 -6.4458,22.1514 -17.8184,25.8242 -6.3706,2.0579 -10.1241,2.0986 -16.5898,0.1788 -11.68861,-3.471 -18.36293,-12.3807 -18.322315,-24.4561 0.042,-12.6619 7.339495,-21.9305 19.695315,-25.0176 z"/>
<path d="m 756.35333,90.488182 c 0,23.904998 0.356,23.000998 -8.239,20.869998 -2.894,-0.718 -8.747,-1.293 -13.005,-1.28 -6.819,0.022 -8.547,0.435 -14.462,3.452 -5.374,2.742 -7.324,4.411 -9.755,8.348 l -3.039,4.92 -0.28,44.731 c -0.225,36.014 -0.542,44.951 -1.626,45.85 -0.929,0.768 -8.339,1.034 -23.72,0.848 l -22.374,-0.27 V 70.957182 h 43 l 1,17.609 3.621,-4.303 c 4.505,-5.357 13.071,-10.57 21.186,-12.8919 4.088,-1.169 9.846,-1.7958 16.943,-1.8428 l 10.75,-0.0713 z"/>
<path d="m 18.945761,75.106182 c 0.376,-0.377 11.06421,-0.564 23.75001,-0.417 l 23.0654,0.268 V 217.95718 l -22.9287,0.27 c -17.67261,0.208 -23.11262,-0.022 -23.73534,-1 -0.9748,-1.536 -1.12371,-141.091998 -0.15137,-142.120998 z"/>
<path d="M 566.14453 69.845703 C 563.24453 69.874253 561.10156 69.957844 560.41406 70.089844 C 533.60509 75.246039 513.37793 89.974758 502.96094 111.92773 C 497.21694 124.02972 495.953 130.21605 496 145.95703 C 496.037 158.48302 496.33097 160.83244 498.79297 168.27344 C 504.62796 185.91042 518.47502 201.64142 536.25 210.81641 C 549.65099 217.7354 558.98508 219.83561 576.41406 219.84961 C 593.48605 219.86361 600.59208 218.38338 613.41406 212.15039 C 620.11406 208.89339 623.61341 206.31632 629.94141 199.98633 C 638.2964 191.62934 641.06495 187.59644 640.12695 185.14844 C 639.62095 183.83144 612.19008 164.45803 610.83008 164.45703 C 610.50708 164.45703 607.64947 167.09155 604.48047 170.31055 C 601.19347 173.64854 596.1844 177.40655 592.81641 179.06055 C 587.34341 181.74654 586.07705 181.95703 575.41406 181.95703 C 564.84807 181.95703 563.40696 181.72326 557.66797 179.07227 C 550.64598 175.82927 544.27576 169.59748 541.38477 163.14648 L 539.50781 158.95703 L 594.49805 158.45703 C 641.873 158.02603 649.625 157.74903 650.5 156.45703 C 651.963 154.28703 650.67545 136.60602 648.31445 126.45703 C 642.12546 99.85406 623.32888 79.949758 597.25391 72.384766 C 591.79091 70.799567 586.46405 70.174594 576.41406 69.933594 C 572.70157 69.844594 569.04453 69.817153 566.14453 69.845703 z M 571.63086 98.060547 C 577.23229 97.939599 583.26141 98.775395 587.01953 100.44727 C 589.52153 101.56126 593.21366 103.91569 595.22266 105.67969 C 599.19865 109.17068 604.96775 119.67488 604.96875 123.42188 L 604.96875 125.76172 L 539.71875 125.76172 L 540.27734 123.01172 C 542.53634 111.91973 550.86781 103.08184 562.5918 99.339844 C 565.06304 98.550845 568.27 98.133115 571.63086 98.060547 z "/>
<path d="m 127.84647,70.494282 c 8.047,-1.269 30.141,-0.4783 39.915,1.4289 14.605,2.85 30.983,9.028 32.176,12.137 0.418,1.091 -2.01,6.871 -6.761,16.094998 -8.332,16.176 -6.982,15.351 -17.73,10.828 -9.117,-3.836 -18.223,-5.492 -30.185,-5.487 -12.548,0.006 -17.875,1.829 -19.946,6.828 -1.105,2.671 -1.058,3.481 0.336,5.607 2.555,3.899 6.086,5.168 23.674,8.504 26.083,4.945 35.955,8.839 45.241,17.836 8.267,8.01 12.647,22.041 10.732,34.384 -2.986,19.245 -16.933,32.641 -40.537,38.939 -10.965,2.925 -36.922,3.202 -49.5003,0.529 -16.314899,-3.468 -36.876499,-11.702 -38.967799,-15.605 -0.8069,-1.508 0.4844,-4.903 6.4571,-16.972 4.106,-8.299 8.0222,-15.089 8.7012,-15.089 0.6803,0.001 4.8524,1.78 9.272399,3.952 11.1889,5.503 21.1274,8.089 33.6414,8.754 17.321,0.92 25.445,-2.3 24.718,-9.798 -0.474,-4.878 -4.328,-6.596 -23.556,-10.499 -9.187,-1.866 -19.6437,-4.359 -23.235,-5.54 -16.012699,-5.268 -25.871999,-12.735 -31.249999,-23.668 -3.1319,-6.366 -3.2813,-7.192 -3.2813,-18.184 0,-11.184 0.0973,-11.696 3.375,-17.739998 7.892,-14.551 23.327099,-23.553 46.710299,-27.2399 z"/>
<path d="m 340.88974,71.960927 c 0.34463,-0.374837 10.14115,-0.560764 21.769,-0.414608 l 21.14204,0.266065 0.9166,88.489316 2.55726,5.65039 c 2.90651,6.41796 7.36298,11.09497 13.4363,14.10062 3.2163,1.59083 6.06415,2.07105 12.43993,2.09392 7.52338,0.0268 8.81943,-0.26448 14.38497,-3.23632 6.65077,-3.5525 10.4326,-7.88053 13.80016,-15.79485 1.9881,-4.67303 2.05499,-5.95861 2.50137,-48.05268 l 0.4583,-43.250396 H 485.5422 V 217.96889 h -41.24653 l -0.26581,-8.20266 c -0.14664,-4.50997 -0.49588,-8.19968 -0.77635,-8.20266 -0.28048,0 -2.42897,1.83839 -4.77543,4.08244 -4.95416,4.74263 -13.96699,9.90484 -21.73507,12.44915 -7.60221,2.49063 -26.94773,2.5483 -35.23371,0.1054 -10.45919,-3.08321 -17.26211,-7.35357 -24.9633,-15.66857 -5.32996,-5.75679 -7.89002,-9.48427 -10.31713,-15.02927 -5.67736,-12.96816 -5.93308,-15.88732 -5.95142,-68.04829 -0.009,-25.733484 0.26765,-47.097788 0.61229,-47.493503 z"/>
<path d="m 256.76147,70.108582 c 1.65,-0.265 10.425,-0.3419 19.5,-0.1699 14.6,0.278 17.843,0.6669 28.163,3.3765 12.079,3.172 24.616,8.406 25.471,10.634 0.268,0.707 -2.953,7.858 -7.16,15.894 -7.487,14.303998 -9.271,16.336998 -12.024,13.707998 -2.104,-2.011 -18.899,-7.078 -27.052,-8.163 -8.888,-1.183 -19.192,-0.612 -22.849,1.266 -5.59,2.872 -7.646,6.781 -5.926,11.264 1.172,3.051 8.089,5.434 24.693,8.506 23.35,4.319 33.676,8.108 42.627,15.639 7.863,6.616 13.057,18.707 13.057,30.394 -10e-1,21.997 -15.365,38.633 -41.774,45.229 -11.676,2.916 -36.449,3.127 -49.226,0.42 -18.682,-3.959 -38.227,-12.24 -38.826,-16.448 -0.29,-2.045 9.527,-23.996 13.367,-29.891 l 1.426,-2.191 8.454,4.314 c 12.325,6.289 24.236,9.049 39.079,9.059 16.86,0.011 22.122,-2.89 20.071,-11.064 -0.907,-3.615 -6.137,-5.627 -24.432,-9.396 -8.447,-1.741 -18.348,-4.079 -22,-5.196 -19.844,-6.071 -31.703,-18.341 -34.368,-35.562 -2.041,-13.182 3.774,-28.807998 13.796,-37.067998 8.064,-6.648 22.077,-12.323 35.933,-14.5546 z"/>
<path d="m 977.17583,38.062914 c 4.50705,-0.159544 8.45873,0.2035 8.78142,0.807313 0.58927,1.127681 -21.7042,30.106419 -24.60801,31.984859 -1.22057,0.777851 -46.56644,1.834764 -55.33674,1.289457 -1.05662,-0.06607 -1.42477,-4.179918 -1.42477,-15.911749 0,-8.695707 0.12777,-15.937427 0.28472,-16.099332 0.16978,-0.153217 14.6526,-0.615003 32.1962,-1.028874 17.55119,-0.414 35.60009,-0.883474 40.10718,-1.041674 z"/>
<path d="m 905.34795,87.438721 c 1.11497,-1.28136 51.65873,-1.07915 51.66522,0.20626 -0.008,0.60261 -5.13671,7.630909 -11.4087,15.621769 l -11.40928,14.53453 -13.77976,0.85739 c -7.57489,0.47116 -14.23432,0.52711 -14.80142,0.12403 -1.1739,-0.83852 -1.42127,-30.002609 -0.26606,-31.343979 z"/>
<path d="M 899.36133 13.261719 C 898.44682 13.272319 888.56236 18.365151 877.39648 24.582031 L 857.0957 35.882812 L 857.0957 57.007812 C 857.0957 76.585283 856.92648 78.066754 854.76758 77.240234 C 850.1023 75.454946 839.2416 73.892578 831.48242 73.892578 C 813.31728 73.892638 796.11808 81.42039 781.88672 95.597656 C 766.28888 111.13651 759.42022 127.4502 759.38672 149.05078 C 759.35364 170.47069 767.22066 188.55017 782.49023 202.14844 C 789.73447 208.5984 802.82226 215.44543 811.83789 217.50195 C 820.57688 219.49579 838.08229 219.23079 846.8418 216.9707 C 865.66384 212.11486 883.60747 198.35796 892.16602 182.22461 C 901.04329 165.49067 901.02539 165.71244 901.02539 85.482422 C 901.02539 28.557522 900.67251 13.245879 899.36133 13.261719 z M 829.60352 106.69141 C 832.17886 106.65402 834.95092 107.04084 838.76367 107.83984 C 851.68 110.54651 861.45591 122.99324 859.17383 133.82227 C 857.86328 140.0367 855.0152 144.16669 848.91797 148.68945 L 844.04492 152.30078 L 845.66211 168.55859 C 846.5499 177.4984 846.89783 185.27881 846.43359 185.85156 C 845.96698 186.42068 838.26766 186.88672 829.32227 186.88672 L 813.05469 186.88672 L 813.20117 180.68164 C 813.28228 177.26872 813.80706 169.50517 814.37109 163.42969 L 815.39844 152.38281 L 809.62891 147.78125 C 799.86642 139.99662 798.01667 129.81135 804.55859 119.85352 C 808.5722 113.74361 813.10824 110.61766 821.62305 108.08984 C 824.64805 107.19206 827.02817 106.72879 829.60352 106.69141 z "/>
</svg>
</div>
<div id="login-card" class="card">
<h2 class="card-title" id="card-title">Sign In</h2>
<p id="realm-display" class="realm-display"></p>
<div id="banner" class="banner"></div>
<form id="login-form" method="POST" action="/api/v1/auth/login">
<input type="hidden" id="execution_id" name="execution_id" value="">
<input type="hidden" id="realm" name="realm" value="">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus autocomplete="username">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
<label class="remember-me" id="remember-me-container" style="display:none;">
<input type="checkbox" id="remember_me" name="remember_me" value="on">
<span>Remember me</span>
</label>
<button type="submit" id="login-submit">Sign In</button>
<div class="login-links" id="login-links" style="display:none;">
<a id="forgot-password-link" href="#" style="display:none;">Forgot password?</a>
<a id="register-link" href="#" style="display:none;">Register</a>
</div>
</form>
<div id="idp-section" class="idp-section" style="display:none;">
<div class="idp-divider"><span>or</span></div>
</div>
<div id="pkce-redirect" class="redirecting" style="display:none;">
<div class="spinner cyan"></div>
<span>Redirecting to sign in...</span>
</div>
<div id="userinfo-card" class="userinfo-card">
<div id="userinfo-content"></div>
<button type="button" class="secondary-btn" id="sign-out-btn">Sign Out</button>
</div>
</div>
</div>
<script>
(function () {
// redirect_uri is followed by this page's own JavaScript after
// sign-in (and sent to the authorize/token endpoints, which
// validate it against the client registration). Keep it
// same-origin: a crafted login.html?redirect_uri=javascript:… or
// cross-origin URL must not turn the post-login navigation into
// XSS or an open redirect. Anything else falls back to the
// console default below.
function sanitizeRedirectUri(uri) {
if (!uri) {
return null;
}
try {
var u = new URL(uri, window.location.origin);
if (u.origin === window.location.origin) {
return u.href;
}
} catch (e) {
// fall through to null
}
return null;
}
var params = new URLSearchParams(window.location.search);
var executionId = params.get('execution_id');
var code = params.get('code');
var loggedOut = params.get('logged_out') === '1';
var urlRealm = params.get('realm');
var urlClientId = params.get('client_id');
var urlRedirectUri = sanitizeRedirectUri(params.get('redirect_uri'));
var realm = urlRealm || 'master';
var banner = document.getElementById('banner');
var form = document.getElementById('login-form');
var pkceRedirect = document.getElementById('pkce-redirect');
var realmDisplay = document.getElementById('realm-display');
var userinfoCard = document.getElementById('userinfo-card');
var userinfoContent = document.getElementById('userinfo-content');
var cardTitle = document.getElementById('card-title');
function setRealmDisplay(r) {
if (realmDisplay && r) {
realmDisplay.textContent = 'Realm: ' + r;
}
}
setRealmDisplay(realm);
if (loggedOut) {
banner.textContent = 'You have been signed out.';
banner.classList.add('success');
}
var error = params.get('error');
var errorDescription = params.get('error_description');
var errorMessages = {
'invalid_grant': 'Invalid username or password.',
'access_denied': 'Access denied.',
'invalid_request': 'Invalid request.',
'authentication_challenge': 'Additional authentication required.',
'unauthorized_client': 'Unauthorized client.',
'invalid_client': 'Unknown or unauthorized client.',
'invalid_scope': 'Invalid scope.',
'server_error': 'Server error. Please try again later.',
'unsupported_operation': 'Unsupported operation.',
'login_required': 'Login required.',
'request_not_supported': 'Request not supported.',
'realm_not_found': 'Realm not found. Please check the URL.',
'missing_realm': 'Missing realm. Please check the URL.'
};
if (error) {
var msg = errorMessages[error] || 'Authentication failed. Please try again.';
if (errorDescription) {
msg += ' (' + errorDescription + ')';
}
banner.textContent = msg;
banner.classList.add('error');
}
// Form login: populate the hidden fields and point the form at the
// realm-scoped login endpoint FIRST — before any enhancement below
// can throw (e.g. a JS engine without `fetch`), so the plain form
// POST always carries ?realm=.
if (executionId && !code) {
document.getElementById('execution_id').value = executionId;
document.getElementById('realm').value = realm;
document.getElementById('login-form').action = '/api/v1/auth/login?realm=' + encodeURIComponent(realm);
}
// Login context: one request for the theme stylesheet, localized
// strings, and the self-service affordances. Fired before the
// early-return paths below (code callback, PKCE redirect,
// error-only page) so their banners and spinner text localize
// too; the PKCE redirect never waits on it. Any failure leaves
// the static page exactly as-is. Guarded so JS engines without
// `fetch` (e.g. HtmlUnit in the OIDC conformance suite) never
// throw here — the form above is already fully functional.
var contextUrl = '/realms/' + encodeURIComponent(realm) + '/login/context';
if (executionId) {
contextUrl += '?execution_id=' + encodeURIComponent(executionId);
}
function applyContextText(text) {
var ctx;
try {
ctx = JSON.parse(text);
} catch (e) {
return;
}
if (ctx) applyLoginContext(ctx);
}
try {
if (typeof window.fetch === 'function') {
window.fetch(contextUrl, {
headers: { 'Accept': 'application/json' }
}).then(function (res) {
return res.ok ? res.text() : null;
}).then(function (text) {
if (text) applyContextText(text);
}).catch(function () {
// Endpoint unavailable (older server, network error) —
// the page stays exactly as rendered.
});
} else if (window.XMLHttpRequest) {
var xhr = new XMLHttpRequest();
xhr.open('GET', contextUrl, true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
applyContextText(xhr.responseText);
}
};
xhr.send();
}
} catch (ignored) {
// No context available — the static page stays as-is.
}
// Handle OAuth2 callback: exchange code for tokens and show userinfo
if (code) {
handleCallback(code, realm, urlClientId, urlRedirectUri);
return;
}
document.getElementById('sign-out-btn').addEventListener('click', function () {
sessionStorage.removeItem('issuerd_code_verifier');
sessionStorage.removeItem('issuerd_oidc_state');
sessionStorage.removeItem('issuerd_access_token');
sessionStorage.removeItem('issuerd_tokens');
document.cookie = 'issuerd_code_verifier=; Max-Age=0; Path=/; Secure';
window.location.href = window.location.pathname + '?realm=' + encodeURIComponent(realm);
});
if (!executionId) {
// The authorization endpoint already rejected this request and
// sent us here with an error (e.g. unknown client or realm).
// Redirecting into the same authorize request would just fail
// again and loop forever — show the error and stop.
if (error) {
form.style.display = 'none';
pkceRedirect.style.display = 'none';
return;
}
// PKCE entry point: hide form, show spinner, generate PKCE and redirect
form.style.display = 'none';
pkceRedirect.style.display = 'flex';
var apiBase = window.location.origin;
var clientId = urlClientId || 'admin-cli';
var effectiveRealm = urlRealm || 'master';
var redirectUri = urlRedirectUri || (window.location.origin + '/admin/console/callback');
setRealmDisplay(effectiveRealm);
function base64UrlEncode(buffer) {
var bytes = new Uint8Array(buffer);
var binary = '';
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function generateRandomString(length) {
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
var text = '';
var randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (var i = 0; i < length; i++) {
text += possible[randomValues[i] % possible.length];
}
return text;
}
function setCookie(name, value) {
document.cookie = name + '=' + encodeURIComponent(value) + '; Path=/; SameSite=Lax; Secure';
}
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
var verifier = sessionStorage.getItem('issuerd_code_verifier') || getCookie('issuerd_code_verifier');
var state = sessionStorage.getItem('issuerd_oidc_state');
if (!verifier || !state) {
verifier = generateRandomString(128);
state = generateRandomString(32);
sessionStorage.setItem('issuerd_code_verifier', verifier);
sessionStorage.setItem('issuerd_oidc_state', state);
sessionStorage.setItem('issuerd_oidc_realm', effectiveRealm);
setCookie('issuerd_code_verifier', verifier);
}
var encoder = new TextEncoder();
crypto.subtle.digest('SHA-256', encoder.encode(verifier)).then(function (digest) {
var challenge = base64UrlEncode(digest);
var authUrl = new URL(apiBase + '/realms/' + encodeURIComponent(effectiveRealm) + '/protocol/openid-connect/auth');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', state);
window.location.href = authUrl.toString();
}).catch(function (err) {
pkceRedirect.style.display = 'none';
form.style.display = 'block';
var banner = document.getElementById('banner');
banner.textContent = 'PKCE redirect failed: ' + (err && err.message ? err.message : String(err));
banner.classList.add('error');
});
return;
}
// (Form-login hidden fields + action are set right after the error
// banner handling above, before any enhancement that could throw.)
// Applies the realm's login context: theme stylesheet, localized
// strings, and the self-service affordances. Every field falls
// back to the hardcoded English/palette when missing.
function applyLoginContext(ctx) {
// Theme stylesheet — inserted after the inline <style> so
// the theme's :root custom properties win over the var()
// fallbacks in the inline rules.
var themeLink = document.createElement('link');
themeLink.rel = 'stylesheet';
themeLink.href = '/realms/' + encodeURIComponent(realm) + '/theme/login.css';
var inlineStyle = document.getElementsByTagName('style')[0];
if (inlineStyle && inlineStyle.parentNode) {
inlineStyle.parentNode.insertBefore(themeLink, inlineStyle.nextSibling);
} else {
document.getElementsByTagName('head')[0].appendChild(themeLink);
}
if (ctx.locale) {
document.documentElement.lang = ctx.locale;
}
var msg = function (key, fallback) {
return (ctx.messages && ctx.messages[key]) || fallback;
};
if (cardTitle) cardTitle.textContent = msg('login.title', 'Sign In');
var usernameLabel = document.querySelector('label[for="username"]');
if (usernameLabel) usernameLabel.textContent = msg('login.username', 'Username');
var passwordLabel = document.querySelector('label[for="password"]');
if (passwordLabel) passwordLabel.textContent = msg('login.password', 'Password');
var submitBtn = document.getElementById('login-submit');
if (submitBtn) submitBtn.textContent = msg('login.submit', 'Sign In');
var rememberMeSpan = document.querySelector('#remember-me-container span');
if (rememberMeSpan) rememberMeSpan.textContent = msg('login.rememberMe', 'Remember me');
var forgotLink = document.getElementById('forgot-password-link');
if (forgotLink) forgotLink.textContent = msg('login.forgotPassword', 'Forgot password?');
var registerLink = document.getElementById('register-link');
if (registerLink) registerLink.textContent = msg('login.register', 'Register');
var idpDivider = document.querySelector('#idp-section .idp-divider span');
if (idpDivider) idpDivider.textContent = msg('login.or', 'or');
if (realmDisplay) {
realmDisplay.textContent = msg('login.realmPrefix', 'Realm:') + ' ' + realm;
}
var signOutBtn = document.getElementById('sign-out-btn');
if (signOutBtn) signOutBtn.textContent = msg('login.signOut', 'Sign Out');
if (loggedOut) {
banner.textContent = msg('login.signedOut', 'You have been signed out.');
}
if (error) {
var errorText = (ctx.messages && ctx.messages['login.error.' + error])
|| errorMessages[error]
|| msg('login.authFailed', 'Authentication failed. Please try again.');
if (errorDescription) {
errorText += ' (' + errorDescription + ')';
}
banner.textContent = errorText;
}
// Not in the code-callback path: handleCallback already
// replaced the spinner text with "Completing sign in...".
if (!code && pkceRedirect) {
var redirectingSpan = pkceRedirect.querySelector('span');
if (redirectingSpan) {
redirectingSpan.textContent = msg('login.redirecting', 'Redirecting to sign in...');
}
}
// Self-service affordances: reveal the matching controls
// for the features the realm has enabled.
if (ctx.remember_me_enabled) {
var rememberMe = document.getElementById('remember-me-container');
if (rememberMe) rememberMe.style.display = 'flex';
}
var showLinks = false;
if (ctx.reset_password_allowed) {
var forgot = document.getElementById('forgot-password-link');
if (forgot) {
forgot.href = '/realms/' + encodeURIComponent(realm) + '/login/reset-credentials';
forgot.style.display = 'inline';
showLinks = true;
}
}
if (ctx.registration_enabled) {
var register = document.getElementById('register-link');
if (register) {
register.href = '/realms/' + encodeURIComponent(realm) + '/login/register'
+ (executionId ? '?execution_id=' + encodeURIComponent(executionId) : '');
register.style.display = 'inline';
showLinks = true;
}
}
if (showLinks) {
var links = document.getElementById('login-links');
if (links) links.style.display = 'flex';
}
// Passwordless email-code mode: the realm logs in with a
// one-time code mailed to the user's address, so no password
// is collected. Runs after the generic label/reveal wiring
// above so these overrides win.
if (ctx.email_code_login === true) {
var passwordInput = document.getElementById('password');
if (passwordInput) {
// A hidden-but-required input would block submission.
passwordInput.required = false;
passwordInput.style.display = 'none';
}
if (passwordLabel) passwordLabel.style.display = 'none';
var rememberMeBox = document.getElementById('remember-me-container');
if (rememberMeBox) rememberMeBox.style.display = 'none';
if (forgotLink) forgotLink.style.display = 'none';
var usernameInput = document.getElementById('username');
if (usernameInput) {
usernameInput.type = 'email';
usernameInput.autocomplete = 'email';
}
if (usernameLabel) usernameLabel.textContent = msg('login.email', 'Email');
if (submitBtn) submitBtn.textContent = msg('login.sendCode', 'Send code');
}
// External identity providers (brokers) — only relevant
// mid-flow, when an execution_id is present.
var idps = ctx.identity_providers;
if (executionId && Array.isArray(idps) && idps.length > 0) {
var idpSection = document.getElementById('idp-section');
if (idpSection) {
var added = 0;
idps.forEach(function (idp) {
if (!idp || !idp.alias) return;
var link = document.createElement('a');
link.className = 'idp-btn';
link.href = '/realms/' + encodeURIComponent(realm)
+ '/broker/' + encodeURIComponent(idp.alias)
+ '/login?flow=' + encodeURIComponent(executionId);
link.textContent = msg('login.continueWith', 'Continue with {0}')
.replace('{0}', idp.display_name || idp.alias);
idpSection.appendChild(link);
added += 1;
});
if (added > 0) idpSection.style.display = 'block';
}
}
}
function handleCallback(code, urlRealm, urlClientId, urlRedirectUri) {
form.style.display = 'none';
pkceRedirect.style.display = 'flex';
pkceRedirect.querySelector('span').textContent = 'Completing sign in...';
var apiBase = window.location.origin;
var clientId = urlClientId || 'admin-cli';
var redirectUri = urlRedirectUri || (window.location.origin + '/admin/console/callback');
var verifier = sessionStorage.getItem('issuerd_code_verifier');
var realm = sessionStorage.getItem('issuerd_oidc_realm') || urlRealm || 'master';
if (!verifier) {
pkceRedirect.style.display = 'none';
form.style.display = 'block';
banner.textContent = 'Session expired. Please sign in again.';
banner.classList.add('error');
return;
}
var tokenUrl = apiBase + '/realms/' + encodeURIComponent(realm) + '/protocol/openid-connect/token';
var tokenBody = 'grant_type=authorization_code'
+ '&code=' + encodeURIComponent(code)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&client_id=' + encodeURIComponent(clientId)
+ '&code_verifier=' + encodeURIComponent(verifier);
fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenBody
}).then(function (res) {
return res.json().then(function (data) {
return { ok: res.ok, status: res.status, data: data };
});
}).then(function (result) {
if (!result.ok) {
throw new Error(result.data.error_description || result.data.error || 'Token exchange failed (' + result.status + ')');
}
var accessToken = result.data.access_token;
var refreshToken = result.data.refresh_token || '';
var idToken = result.data.id_token || '';
var expiresIn = result.data.expires_in || 300;
var rawTokens = sessionStorage.getItem('issuerd_tokens');
var existingTokens = rawTokens ? JSON.parse(rawTokens) : {};
if (existingTokens && typeof existingTokens === 'object' && existingTokens.accessToken) {
// Legacy single-token format: migrate.
var legacyRealm = localStorage.getItem('issuerd_realm') || 'master';
var legacyToken = existingTokens;
existingTokens = {};
existingTokens[legacyRealm] = legacyToken;
} else if (!existingTokens || typeof existingTokens !== 'object' || Array.isArray(existingTokens)) {
existingTokens = {};
}
existingTokens[realm] = {
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
expiresAt: Date.now() + expiresIn * 1000
};
sessionStorage.setItem('issuerd_tokens', JSON.stringify(existingTokens));
sessionStorage.removeItem('issuerd_oidc_realm');
localStorage.setItem('issuerd_realm', realm);
window.location.href = redirectUri;
}).catch(function (err) {
pkceRedirect.style.display = 'none';
form.style.display = 'block';
banner.textContent = 'Sign in failed: ' + err.message;
banner.classList.add('error');
});
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();
</script>
</body>
</html>