feat(catalog): add MFA security controls
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user