{{- /* Keycloak theme `big-demo`: keycloak.v2 plus one login script. It is always mounted and always the default theme (KC_SPI_THEME_DEFAULT), so the only thing `demo.otpAutofill` switches is what that script does. Off, it is empty and the login is exactly keycloak.v2. KC_SPI_THEME_DEFAULT covers every theme type, and Keycloak does not fall back for a type the theme lacks (the account console 500s), so account, admin and email are declared too, each a plain child of Keycloak 26's own default. On, the medewerker OTP step computes the code from the realm fixture secret (docs/runbooks/keycloak.md) and submits it: the demo still shows MFA being enforced without anyone needing an authenticator. The secret is committed and shared, so this is a demo convenience only — never enable it anywhere real. */ -}} --- apiVersion: v1 kind: ConfigMap metadata: name: kc-theme labels: {{- include "big.labels" (dict "root" $ "name" "kc-theme") | nindent 4 }} data: login.properties: | parent=keycloak.v2 import=common/keycloak scripts=js/otp-autofill.js account.properties: | parent=keycloak.v3 admin.properties: | parent=keycloak.v2 email.properties: | parent=keycloak otp-autofill.js: | {{- if .Values.demo.otpAutofill }} // RFC 6238 with Keycloak's default policy (HmacSHA1, 6 digits, 30 s) over the // raw bytes of the medewerker fixture secret — same as tests/e2e/keycloak-login.ts. document.addEventListener('DOMContentLoaded', async () => { const input = document.querySelector('input[name="otp"]'); if (!input || !input.form) return; const key = await crypto.subtle.importKey('raw', new TextEncoder().encode('BIGMEDEWERKEROTPSEED'), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']); // A code is single-use, so a second login in the same window spends the next // counter (Keycloak's look-ahead accepts it). Past that, fill but don't submit, // so a rejected code can't turn into a submit loop. const now = Math.floor(Date.now() / 30000); let last = -1; try { last = Number(sessionStorage.getItem('big-otp-counter')) || -1; } catch {} const counter = Math.max(now, last + 1); const msg = new DataView(new ArrayBuffer(8)); msg.setBigUint64(0, BigInt(counter)); const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg.buffer)); const o = mac[19] & 0x0f; const n = ((mac[o] & 0x7f) << 24 | mac[o + 1] << 16 | mac[o + 2] << 8 | mac[o + 3]) % 1e6; input.value = String(n).padStart(6, '0'); if (counter > now + 1) return; try { sessionStorage.setItem('big-otp-counter', String(counter)); } catch {} input.form.requestSubmit(); }); {{- else }} // demo.otpAutofill is off: the OTP step is entered by hand. {{- end }}