diff --git a/ui/sign-in/package-lock.json b/ui/sign-in/package-lock.json index f9b7c11..4f2bae1 100644 --- a/ui/sign-in/package-lock.json +++ b/ui/sign-in/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@cameleer/design-system": "^0.1.54", "@simplewebauthn/browser": "^13.3.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -1905,6 +1906,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", diff --git a/ui/sign-in/package.json b/ui/sign-in/package.json index f28d3bd..96b3a54 100644 --- a/ui/sign-in/package.json +++ b/ui/sign-in/package.json @@ -11,6 +11,7 @@ "dependencies": { "@cameleer/design-system": "^0.1.54", "@simplewebauthn/browser": "^13.3.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/ui/sign-in/src/SignInPage.tsx b/ui/sign-in/src/SignInPage.tsx index f8703d9..3713fc7 100644 --- a/ui/sign-in/src/SignInPage.tsx +++ b/ui/sign-in/src/SignInPage.tsx @@ -2,6 +2,7 @@ import { type FormEvent, useEffect, useMemo, useState } from 'react'; import { Eye, EyeOff } from 'lucide-react'; import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system'; import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg'; +import { QRCodeSVG } from 'qrcode.react'; import { startAuthentication, startRegistration as startWebAuthnReg } from '@simplewebauthn/browser'; import { signIn, startRegistration, completeRegistration, @@ -311,7 +312,7 @@ export function SignInPage() { }; // --- MFA enrollment --- - const [totpSetup, setTotpSetup] = useState<{ secret: string; secretQrCode: string; verificationId: string } | null>(null); + const [totpSetup, setTotpSetup] = useState<{ secret: string; otpauthUri: string; verificationId: string } | null>(null); const [totpCode, setTotpCode] = useState(''); async function handleEnrollPasskey() { @@ -343,7 +344,10 @@ export function SignInPage() { setLoading(true); try { const data = await createTotpSecret(); - setTotpSetup(data); + const account = identifier || 'user'; + const issuer = 'Cameleer'; + const otpauthUri = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(account)}?secret=${data.secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`; + setTotpSetup({ secret: data.secret, otpauthUri, verificationId: data.verificationId }); setTotpCode(''); setMode('mfaEnrollTotp'); } catch (err) { @@ -358,7 +362,7 @@ export function SignInPage() { setError(null); setLoading(true); try { - const verifiedId = await verifyTotpSetup(totpCode); + const verifiedId = await verifyTotpSetup(totpCode, totpSetup!.verificationId); await bindMfaProfile('Totp', verifiedId); const bc = await generateBackupCodes(); await bindMfaProfile('BackupCode', bc.verificationId); @@ -848,7 +852,7 @@ export function SignInPage() {