The keycloak smoke check now asserts that a password-only grant on the medewerker realm is rejected and that a TOTP code completes it. The e2e medewerker logins move to a shared helper that submits Keycloak's OTP challenge. Both fail against the current realm export, which enforces no MFA. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
28 lines
1.4 KiB
TypeScript
28 lines
1.4 KiB
TypeScript
import { createHmac } from 'node:crypto';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
// The medewerker realm enforces MFA (S-15c), so a staff login is two steps: password, then a TOTP
|
|
// code. The realm export seeds every medewerker with this fixture secret — Keycloak HMACs the raw
|
|
// secret bytes — so the e2e can compute a valid code instead of enrolling an authenticator.
|
|
const OTP_SECRET = 'BIGMEDEWERKEROTPSEED';
|
|
|
|
// RFC 6238 TOTP: HMAC-SHA1 over the 30-second counter, dynamically truncated to 6 digits.
|
|
export function totp(secret = OTP_SECRET, at = Date.now()): string {
|
|
const counter = Buffer.alloc(8);
|
|
counter.writeBigUInt64BE(BigInt(Math.floor(at / 1000 / 30)));
|
|
const mac = createHmac('sha1', secret).update(counter).digest();
|
|
const offset = mac[mac.length - 1] & 0x0f;
|
|
return String((mac.readUInt32BE(offset) & 0x7fffffff) % 1_000_000).padStart(6, '0');
|
|
}
|
|
|
|
export async function loginMedewerker(page: Page, username: string): Promise<void> {
|
|
await page.locator('#username').fill(username);
|
|
await page.locator('#password').fill('test123');
|
|
await page.locator('#kc-login').click();
|
|
|
|
// Keycloak's conditional-OTP step. Its lookAheadWindow accepts the neighbouring counters, so a
|
|
// code computed just before a 30-second boundary still validates — no retry needed.
|
|
await page.locator('#otp').fill(totp());
|
|
await page.locator('#kc-login').click();
|
|
}
|