learning_ai_common_plat/packages/auth-ui/src/MfaChallenge.tsx
saravanakumardb1 53f2a97d40 feat(auth): SmartAuth SDK packages — OAuth, MFA, passkeys, devices, RS256, auth-ui
Phase 1C: @bytelyst/auth-client + @bytelyst/react-auth Google Sign-In
- loginWithGoogle/Microsoft/Apple(idToken) → POST /auth/oauth/:provider
- getProviders/linkProvider/unlinkProvider → provider management
- React context: loginWithGoogle, providers state, refreshProviders

Phase 2D: MFA + Social Login SDK + Auth UI
- verifyMfa/setupTotp/verifyTotpSetup/disableMfa/getMfaStatus
- regenerateRecoveryCodes → recovery code management
- React context: mfaRequired/mfaChallenge/mfaMethods state, verifyMfa action
- login() handles MfaLoginResult (returns false, sets MFA state)
- NEW @bytelyst/auth-ui: LoginForm, MfaChallenge, SocialButtons components

Phase 3: Passkeys + Device SDK
- getPasskeyRegisterOptions/verifyPasskeyRegistration
- getPasskeyAuthOptions/verifyPasskeyAuth/listPasskeys/deletePasskey
- listDevices/trustDevice/revokeDevice/revokeAllDevices

Phase 4C: @bytelyst/auth RS256 support
- createJwtUtils({ algorithm: 'RS256', rsaPrivateKey, rsaPublicKey })
- Dual verification: RS256 first, HS256 fallback (migration-safe)
- Remote JWKS support via jwksUrl option
- Backward-compatible: HS256 remains default

Phase 5B: Admin security endpoints
- getSecurityOverview/unlockUser/exportAuthData/cancelDeletion

Tests: 101 total (36 auth-client + 21 react-auth + 13 auth-ui + 31 auth)
Builds: all 4 packages pass tsc
2026-03-12 10:50:56 -07:00

115 lines
3.1 KiB
TypeScript

import { useState, type FormEvent } from 'react';
import type { MfaChallengeProps } from './types.js';
/**
* MFA code entry form (6-digit TOTP or recovery code).
* Styled via CSS custom properties (inherits --bl-* from host app).
*/
export function MfaChallenge({
onSubmit,
onUseRecovery,
methods,
isLoading = false,
error,
className,
}: MfaChallengeProps) {
const [code, setCode] = useState('');
function handleSubmit(e: FormEvent) {
e.preventDefault();
onSubmit(code);
}
return (
<div className={className} data-testid="bl-mfa-challenge">
<form
onSubmit={handleSubmit}
style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}
>
<div style={{ fontSize: '14px', color: 'var(--bl-text, #333)' }}>
Enter your authentication code
</div>
{methods && methods.length > 0 && (
<div
data-testid="bl-mfa-methods"
style={{ fontSize: '12px', color: 'var(--bl-muted, #999)' }}
>
Available methods: {methods.join(', ')}
</div>
)}
<input
type="text"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="000000"
value={code}
onChange={e => setCode(e.target.value)}
required
disabled={isLoading}
maxLength={8}
data-testid="bl-mfa-code"
style={{
padding: '12px',
border: '1px solid var(--bl-border, #ccc)',
borderRadius: 'var(--bl-radius, 6px)',
fontSize: '24px',
textAlign: 'center',
letterSpacing: '4px',
fontFamily: 'monospace',
}}
/>
{error && (
<div
data-testid="bl-mfa-error"
style={{ color: 'var(--bl-error, #dc3545)', fontSize: '13px' }}
>
{error}
</div>
)}
<button
type="submit"
disabled={isLoading || code.length < 6}
data-testid="bl-mfa-submit"
style={{
padding: '10px 16px',
border: 'none',
borderRadius: 'var(--bl-radius, 6px)',
background: 'var(--bl-primary, #0066ff)',
color: '#fff',
cursor: isLoading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: 600,
opacity: isLoading || code.length < 6 ? 0.6 : 1,
}}
>
{isLoading ? 'Verifying...' : 'Verify'}
</button>
{onUseRecovery && (
<button
type="button"
onClick={onUseRecovery}
disabled={isLoading}
data-testid="bl-mfa-recovery"
style={{
padding: '8px',
border: 'none',
background: 'transparent',
color: 'var(--bl-link, #0066ff)',
cursor: 'pointer',
fontSize: '13px',
textDecoration: 'underline',
}}
>
Use a recovery code
</button>
)}
</form>
</div>
);
}