issuerd-server 0.1.6

HTTP server bootstrap, middleware, TLS and OIDC endpoints for the Issuerd IAM server
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
<!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>