From d6dedb9b9a182b2609614229c71e1ff094f8ba6c Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 19 May 2026 06:24:28 +0000 Subject: [PATCH] feat(catalog): add MFA security controls --- ts_web/elements/idp-admin-shell.ts | 152 ++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 4 deletions(-) diff --git a/ts_web/elements/idp-admin-shell.ts b/ts_web/elements/idp-admin-shell.ts index ce6c578..7f33a23 100644 --- a/ts_web/elements/idp-admin-shell.ts +++ b/ts_web/elements/idp-admin-shell.ts @@ -163,6 +163,24 @@ export interface IIdpAdminPassportEnrollment { expiresAt: number; } +export interface IIdpAdminPasskey { + id: string; + label: string; + credentialId?: string; + status: string; + backedUp?: boolean; + deviceType?: string; + transports?: string[]; + createdAt: number; + lastUsedAt?: number | null; +} + +export interface IIdpAdminTotpEnrollment { + credentialId: string; + secret: string; + otpauthUrl: string; +} + export interface IIdpAdminSessionEventDetail { sessionId: string; } @@ -237,6 +255,23 @@ export interface IIdpAdminPassportDeviceEventDetail { deviceId: string; } +export interface IIdpAdminTotpVerifyEventDetail { + credentialId: string; + code: string; +} + +export interface IIdpAdminTotpCodeEventDetail { + code: string; +} + +export interface IIdpAdminPasskeyRegistrationEventDetail { + label: string; +} + +export interface IIdpAdminPasskeyEventDetail { + passkeyId: string; +} + @customElement('idp-admin-shell') export class IdpAdminShell extends DeesElement { public static demo = () => html``; @@ -1239,6 +1274,18 @@ export class IdpAdminShell extends DeesElement { @property({ type: Object }) public accessor passportEnrollment: IIdpAdminPassportEnrollment | null = null; + @property({ type: Boolean, attribute: 'totp-enabled' }) + public accessor totpEnabled = false; + + @property({ type: Number, attribute: 'backup-codes-remaining' }) + public accessor backupCodesRemaining = 0; + + @property({ type: Object }) + public accessor totpEnrollment: IIdpAdminTotpEnrollment | null = null; + + @property({ type: Array }) + public accessor passkeys: IIdpAdminPasskey[] = []; + @property({ type: String, attribute: 'credential-message' }) public accessor credentialMessage = ''; @@ -1523,6 +1570,51 @@ export class IdpAdminShell extends DeesElement { }); } + private requestTotpEnrollment() { + this.dispatchShellEvent('idp-admin-totp-start', {}); + } + + private verifyTotpEnrollment() { + if (!this.totpEnrollment) { + return; + } + const code = globalThis.prompt?.('Authenticator code')?.trim(); + if (!code) { + return; + } + this.dispatchShellEvent('idp-admin-totp-verify', { + credentialId: this.totpEnrollment.credentialId, + code, + }); + } + + private disableTotp() { + const code = globalThis.prompt?.('Enter your current authenticator code to disable TOTP')?.trim(); + if (!code) { + return; + } + this.dispatchShellEvent('idp-admin-totp-disable', { code }); + } + + private regenerateBackupCodes() { + const code = globalThis.prompt?.('Enter your current authenticator code to regenerate backup codes')?.trim(); + if (!code) { + return; + } + this.dispatchShellEvent('idp-admin-backup-codes-regenerate', { code }); + } + + private requestPasskeyRegistration() { + const fallbackLabel = typeof navigator !== 'undefined' + ? navigator.userAgent.includes('Mobile') ? 'Mobile passkey' : 'Desktop passkey' + : 'Passkey'; + const label = globalThis.prompt?.('Passkey label', fallbackLabel)?.trim(); + if (!label) { + return; + } + this.dispatchShellEvent('idp-admin-passkey-register', { label }); + } + private setPage(pageArg: TIdpAdminPage) { this.page = pageArg; this.orgMenuOpen = false; @@ -2172,6 +2264,23 @@ export class IdpAdminShell extends DeesElement { html`
`, ], })); + const passkeyRows = this.passkeys.map((passkeyArg) => ({ + cells: [ + html` +
+ ${this.getInitials(passkeyArg.label)} +
+
${passkeyArg.label}
+
${passkeyArg.deviceType || 'passkey'}${passkeyArg.backedUp ? ' - backed up' : ''}
+
+
+ `, + html`${passkeyArg.status}`, + (passkeyArg.transports || []).join(', ') || '-', + passkeyArg.lastUsedAt ? this.formatTimeAgo(passkeyArg.lastUsedAt) : 'never', + html`
`, + ], + })); return html` ${this.renderPageHeader('Security', 'Manage how you authenticate and protect your account.')} @@ -2190,6 +2299,45 @@ export class IdpAdminShell extends DeesElement {
`)} + ${this.renderSectionCard('Authenticator app', 'TOTP protects password and magic-link sign-ins with a six-digit code from an authenticator app.', html` +
Status
${this.totpEnabled ? `${this.backupCodesRemaining} backup codes remaining.` : 'No authenticator app is enrolled.'}
${this.totpEnabled ? 'Enabled' : 'Disabled'}
+ ${this.totpEnrollment ? html` +
+
TOTP setup pending
Scan the otpauth URL or enter the manual secret, then verify the current code.
+ ${this.renderFormRow('Manual secret', '', this.renderCodeBlock(this.totpEnrollment.secret))} + ${this.renderFormRow('otpauth URL', '', this.renderCodeBlock(this.totpEnrollment.otpauthUrl))} + + ` : html` +
+ ${this.totpEnabled ? html` +
+ + +
+ ` : html``} + `} + `)} + +
+
+
Register passkey
Creates a WebAuthn registration challenge in this browser.
+ +
+
No active enrollment challenge.`} -
- ${this.renderStateCard('TOTP controls not connected', 'No TOTP secret, enrollment, or verification endpoints exist in this backend yet, so no fake TOTP toggle is shown.', 'lock')} - ${this.renderStateCard('WebAuthn passkeys not connected', 'No WebAuthn passkey credential model or assertion endpoints exist yet. Passport devices are the available cryptographic credential path.', 'key')} -
`; }