Skip to content

Commit

Permalink
fix: add mfa for webauthn routes
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 committed Nov 11, 2024
1 parent c7fef3a commit 20e47db
Show file tree
Hide file tree
Showing 6 changed files with 617 additions and 6,444 deletions.
1 change: 1 addition & 0 deletions example/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"lucide-react": "^0.454.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^6.27.0",
Expand Down
4 changes: 4 additions & 0 deletions example/react/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AuthClient } from '@supabase/auth-js'
import './tailwind.output.css'
import { Routes, Route, useNavigate } from 'react-router-dom';
import MFAPage from './mfa'
import MFASelectionPage from './mfa-selection-page'
import MFAWebAuthn from './mfa-webauthn'


const supabaseURL = process.env.REACT_APP_SUPABASE_URL
Expand Down Expand Up @@ -422,6 +424,8 @@ function App() {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/mfa" element={<MFAPage />} />
<Route path="/mfa-selection" element={<MFASelectionPage/>}/>
<Route path="/mfa-webauthn" element={<MFAWebAuthn/>}/>
</Routes>
);
}
Expand Down
174 changes: 174 additions & 0 deletions example/react/src/mfa-selection-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useState } from 'react'
import { Smartphone, Key } from 'lucide-react'

const styles = {
container: {
minHeight: '100vh',
backgroundColor: '#f3f4f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
},
card: {
backgroundColor: 'white',
borderRadius: '0.5rem',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
width: '100%',
maxWidth: '28rem',
padding: '1.5rem',
},
title: {
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
},
description: {
color: '#6b7280',
marginBottom: '1.5rem',
},
radioGroup: {
display: 'flex',
flexDirection: 'column',
gap: '1rem',
},
radioOption: {
display: 'flex',
alignItems: 'center',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
cursor: 'pointer',
},
radioInput: {
marginRight: '0.75rem',
},
radioLabel: {
display: 'flex',
alignItems: 'center',
flex: 1,
},
radioText: {
marginLeft: '0.75rem',
},
radioTitle: {
fontWeight: 'bold',
},
radioDescription: {
fontSize: '0.875rem',
color: '#6b7280',
},
checkboxContainer: {
display: 'flex',
alignItems: 'center',
marginTop: '1rem',
},
checkbox: {
marginRight: '0.5rem',
},
button: {
backgroundColor: '#3b82f6',
color: 'white',
padding: '0.5rem 1rem',
borderRadius: '0.25rem',
border: 'none',
cursor: 'pointer',
width: '100%',
marginTop: '1.5rem',
},
buttonDisabled: {
backgroundColor: '#9ca3af',
cursor: 'not-allowed',
},
link: {
color: '#3b82f6',
textDecoration: 'none',
fontSize: '0.875rem',
marginTop: '1rem',
display: 'inline-block',
},
}

export default function MFASelectionPage() {
const [selectedMethod, setSelectedMethod] = useState(undefined)
const [rememberMethod, setRememberMethod] = useState(false)

return (
<div style={styles.container}>
<div style={styles.card}>
<h2 style={styles.title}>Multi-factor Authentication</h2>
<p style={styles.description}>
Your account is protected with multi-factor authentication (MFA).
To finish signing in, select a method to authenticate with.
</p>
<div style={styles.radioGroup}>
<label style={styles.radioOption}>
<input
type="radio"
value="authenticator"
checked={selectedMethod === 'authenticator'}
onChange={() => setSelectedMethod('authenticator')}
style={styles.radioInput}
/>
<div style={styles.radioLabel}>
<Smartphone size={20} />
<div style={styles.radioText}>
<div style={styles.radioTitle}>Authenticator app</div>
<div style={styles.radioDescription}>
Authenticate using a code generated by an app installed on your mobile device or computer.
</div>
</div>
</div>
</label>
<label style={styles.radioOption}>
<input
type="radio"
value="passkey"
checked={selectedMethod === 'passkey'}
onChange={() => setSelectedMethod('passkey')}
style={styles.radioInput}
/>
<div style={styles.radioLabel}>
<Key size={20} />
<div style={styles.radioText}>
<div style={styles.radioTitle}>Passkey or security key</div>
<div style={styles.radioDescription}>
Authenticate using your fingerprint, face, or PIN on your mobile device, computer or FIDO2 security key.
</div>
</div>
</div>
</label>
</div>
<div style={styles.checkboxContainer}>
<input
type="checkbox"
id="remember"
checked={rememberMethod}
onChange={(e) => setRememberMethod(e.target.checked)}
style={styles.checkbox}
/>
<label htmlFor="remember">Remember this method</label>
</div>
<button
style={{
...styles.button,
...(selectedMethod ? {} : styles.buttonDisabled),
}}
disabled={!selectedMethod}
onClick={() => {
// Handle continue action
console.log('Continuing with method:', selectedMethod)
}}
>
Continue
</button>
<a href="#" style={styles.link} onClick={() => {
// Handle sign in to different account
console.log('Signing in to a different account')
}}>
Sign in to a different account
</a>
</div>
</div>
)
}
156 changes: 156 additions & 0 deletions example/react/src/mfa-webauthn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react'
import { Shield, AlertCircle } from 'lucide-react'

const styles = {
container: {
minHeight: '100vh',
backgroundColor: '#f3f4f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
},
card: {
backgroundColor: 'white',
borderRadius: '0.5rem',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
width: '100%',
maxWidth: '28rem',
padding: '1.5rem',
},
header: {
display: 'flex',
alignItems: 'center',
marginBottom: '1rem',
},
title: {
fontSize: '1.5rem',
fontWeight: 'bold',
marginLeft: '0.5rem',
},
description: {
color: '#6b7280',
marginBottom: '1.5rem',
},
errorContainer: {
backgroundColor: '#FEE2E2',
border: '1px solid #F87171',
borderRadius: '0.375rem',
padding: '1rem',
marginBottom: '1.5rem',
display: 'flex',
alignItems: 'flex-start',
},
errorIcon: {
color: '#DC2626',
marginRight: '0.5rem',
flexShrink: 0,
},
errorMessage: {
color: '#DC2626',
fontSize: '0.875rem',
},
button: {
backgroundColor: '#3b82f6',
color: 'white',
padding: '0.5rem 1rem',
borderRadius: '0.25rem',
border: 'none',
cursor: 'pointer',
width: '100%',
marginTop: '1rem',
},
link: {
color: '#3b82f6',
textDecoration: 'none',
fontSize: '0.875rem',
marginTop: '1rem',
display: 'inline-block',
},
}

// Simulated WebAuthn API call
const simulateWebAuthnAuthentication = (): Promise<void> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate a 50% chance of success
if (Math.random() < 0.5) {
resolve()
} else {
reject(new Error('The operation either timed out or was not allowed.'))
}
}, 2000) // Simulate a 2-second delay
})
}

export default function MFAWebAuthn() {
const [error, setError] = useState(null)
const [isAuthenticating, setIsAuthenticating] = useState(false)

useEffect(() => {
startAuthentication()
}, [])

const startAuthentication = async () => {
setIsAuthenticating(true)
setError(null)
try {
await simulateWebAuthnAuthentication()
// If successful, you would typically redirect the user or update the app state
console.log('Authentication successful')
} catch (err) {
setError((err).message)
} finally {
setIsAuthenticating(false)
}
}

return (
<div style={styles.container}>
<div style={styles.card}>
<div style={styles.header}>
<Shield size={24} />
<h2 style={styles.title}>Keeping you secure</h2>
</div>
<p style={styles.description}>
Your account is protected with a passkey or security key for multi-factor authentication (MFA).
To finish signing in, follow the instructions from your browser or you can select another MFA method.
</p>
{error && (
<div style={styles.errorContainer}>
<AlertCircle style={styles.errorIcon} size={20} />
<div style={styles.errorMessage}>
<strong>Unable to authenticate.</strong><br />
{error} See: <a href="https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client" target="_blank" rel="noopener noreferrer">W3C WebAuthn Specification</a>.
</div>
</div>
)}
<button
style={styles.button}
onClick={startAuthentication}
disabled={isAuthenticating}
>
{isAuthenticating ? 'Authenticating...' : 'Try Again'}
</button>
<a href="#" style={styles.link} onClick={() => {
// Handle selecting another MFA method
console.log('Selecting another MFA method')
}}>
Select another MFA method
</a>
<br></br>

<a href="#" style={styles.link} onClick={() => {
console.log('Signing in to a different account')
}}>
Sign in to a different account
</a>
<a href="#" style={styles.link} onClick={() => {
console.log('Trouble signing in')
}}>
Trouble signing in?
</a>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion example/react/src/mfa.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const MFAPage = ({ onSuccess, onCancel, auth }) => {
const code = verificationCode.join('');
// Enroll, Challenge, and Verify

nSuccess?.();
onSuccess?.();
} catch (err) {
setError(err.message || 'Verification failed. Please try again.');
} finally {
Expand Down
Loading

0 comments on commit 20e47db

Please sign in to comment.