feat(catalog): add MFA security controls

This commit is contained in:
2026-05-19 06:24:28 +00:00
parent 02bdc74f2b
commit d6dedb9b9a
+148 -4
View File
@@ -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`<idp-admin-shell global-admin></idp-admin-shell>`;
@@ -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<IIdpAdminTotpVerifyEventDetail>('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<IIdpAdminTotpCodeEventDetail>('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<IIdpAdminTotpCodeEventDetail>('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<IIdpAdminPasskeyRegistrationEventDetail>('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`<div class="cell-actions"><button class="table-action destructive" @click=${() => this.dispatchShellEvent<IIdpAdminPassportDeviceEventDetail>('idp-admin-passport-revoke', { deviceId: deviceArg.id })}>Revoke</button></div>`,
],
}));
const passkeyRows = this.passkeys.map((passkeyArg) => ({
cells: [
html`
<div class="identity-cell">
<span class="identity-avatar">${this.getInitials(passkeyArg.label)}</span>
<div>
<div class="identity-primary">${passkeyArg.label}</div>
<div class="identity-secondary">${passkeyArg.deviceType || 'passkey'}${passkeyArg.backedUp ? ' - backed up' : ''}</div>
</div>
</div>
`,
html`<idp-badge variant=${passkeyArg.status === 'active' ? 'ok' : 'error'}>${passkeyArg.status}</idp-badge>`,
(passkeyArg.transports || []).join(', ') || '-',
passkeyArg.lastUsedAt ? this.formatTimeAgo(passkeyArg.lastUsedAt) : 'never',
html`<div class="cell-actions"><button class="table-action destructive" @click=${() => this.dispatchShellEvent<IIdpAdminPasskeyEventDetail>('idp-admin-passkey-revoke', { passkeyId: passkeyArg.id })}>Revoke</button></div>`,
],
}));
return html`
${this.renderPageHeader('Security', 'Manage how you authenticate and protect your account.')}
@@ -2190,6 +2299,45 @@ export class IdpAdminShell extends DeesElement {
<div class="divider"></div>
<button class="plain-button primary" @click=${() => this.submitPasswordChange()}>Update password</button>
`)}
${this.renderSectionCard('Authenticator app', 'TOTP protects password and magic-link sign-ins with a six-digit code from an authenticator app.', html`
<div class="split-row"><div><div class="section-title">Status</div><div class="muted">${this.totpEnabled ? `${this.backupCodesRemaining} backup codes remaining.` : 'No authenticator app is enrolled.'}</div></div><idp-badge variant=${this.totpEnabled ? 'ok' : 'warn'}>${this.totpEnabled ? 'Enabled' : 'Disabled'}</idp-badge></div>
${this.totpEnrollment ? html`
<div class="divider"></div>
<div class="notice-card"><idp-icon name="qr" size="16"></idp-icon><div><div class="section-title">TOTP setup pending</div><div class="muted">Scan the otpauth URL or enter the manual secret, then verify the current code.</div></div></div>
${this.renderFormRow('Manual secret', '', this.renderCodeBlock(this.totpEnrollment.secret))}
${this.renderFormRow('otpauth URL', '', this.renderCodeBlock(this.totpEnrollment.otpauthUrl))}
<button class="plain-button primary" @click=${() => this.verifyTotpEnrollment()}>Verify setup code</button>
` : html`
<div class="divider"></div>
${this.totpEnabled ? html`
<div class="cell-actions" style="justify-content:flex-start">
<button class="plain-button outline" @click=${() => this.regenerateBackupCodes()}>Regenerate backup codes</button>
<button class="plain-button ghost" style="color:var(--idp-error)" @click=${() => this.disableTotp()}>Disable TOTP</button>
</div>
` : html`<button class="plain-button primary" @click=${() => this.requestTotpEnrollment()}>Enable authenticator app</button>`}
`}
`)}
<idp-data-table
title="Passkeys"
subtitle="WebAuthn credentials for passwordless login and MFA step-up."
badge=${`${this.passkeys.length} total`}
empty-title="No passkeys"
empty-description="Register a passkey to sign in with platform authenticators or security keys."
.columns=${[
{ label: 'Passkey' },
{ label: 'Status' },
{ label: 'Transports', hideBelow: 'mobile' },
{ label: 'Last used', mono: true, hideBelow: 'mobile' },
{ label: 'Action', align: 'right' },
]}
.rows=${passkeyRows}
></idp-data-table>
<div class="section-card">
<div class="section-headline">
<div><div class="section-title">Register passkey</div><div class="section-description">Creates a WebAuthn registration challenge in this browser.</div></div>
<button class="plain-button outline" @click=${() => this.requestPasskeyRegistration()}>Register passkey</button>
</div>
</div>
<idp-data-table
title="Passport devices"
subtitle="Cryptographic IDP Passport devices registered for this account."
@@ -2217,10 +2365,6 @@ export class IdpAdminShell extends DeesElement {
${this.renderFormRow('Signing payload', '', this.renderCodeBlock(this.passportEnrollment.signingPayload))}
` : html`<div class="muted">No active enrollment challenge.</div>`}
</div>
<div class="primary-grid">
${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')}
</div>
</div>
`;
}