Skip to content

Commit

Permalink
EML and SMS OTP for MFA
Browse files Browse the repository at this point in the history
New recipe featuring email magic links as a first factor of authentication and SMS OTPs as a step-up factor of authentication
  • Loading branch information
ashWeaver-Stytch committed May 17, 2024
1 parent a820257 commit b180a6c
Show file tree
Hide file tree
Showing 6 changed files with 30 additions and 35 deletions.
18 changes: 5 additions & 13 deletions components/EmailSMS/LoginWithEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,34 @@ const STATUS = {
ERROR: 2,
};

const auth_domain = {
login: '/recipes/api-sms-mfa/magic-link-authenticate',
signup: '/recipes/api-sms-mfa/magic-link-authenticate'
}
const EML_REDIRECT = '/recipes/api-sms-mfa/magic-link-authenticate';

const LoginWithSMSMFA = () => {
const [emlSent, setEMLSent] = useState(STATUS.INIT);
const [email, setEmail] = useState('');
const [isDisabled, setIsDisabled] = useState(true);
const path = "webauthn";

const isValidEmail = (emailValue: string) => {
// Overly simple email address regex
const regex = /\S+@\S+\.\S+/;
return regex.test(emailValue);
};

const onEmailChange: ChangeEventHandler<HTMLInputElement> = (e) => {
setEmail(e.target.value);
if (isValidEmail(e.target.value)) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
setIsDisabled(!isValidEmail(e.target.value));
};

const onSubmit: FormEventHandler = async (e) => {
e.preventDefault();
// Disable button right away to prevent sending emails twice
if (isDisabled) {
return;
} else {
setIsDisabled(true);
}
setIsDisabled(true);

if (isValidEmail(email)) {
const resp = await sendEML(email, auth_domain.login, auth_domain.signup);
const resp = await sendEML(email, EML_REDIRECT, EML_REDIRECT);
if (resp.status === 200) {
setEMLSent(STATUS.SENT);
} else {
Expand Down
25 changes: 19 additions & 6 deletions components/EmailSMS/SMSOTPButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { sendOTP, authOTP } from '../../lib/otpUtils';
import * as webauthnJson from '@github/webauthn-json';
import { useRouter } from 'next/router';

interface SMSOTPButtonProps {
Expand All @@ -18,14 +17,15 @@ function formatPhoneNumber(phoneNumber: string): string {

function SMSOTPButton({ phoneNumber }: SMSOTPButtonProps) {
const router = useRouter();
const [openModal, setOpenModal] = useState(false);
const [otp, setOTP] = useState('');
const [methodId, setMethodId] = useState('');
const [openModal, setOpenModal] = useState(false); // State variable to control the modal visibility
const [otp, setOTP] = useState(''); // State variable to store the OTP input by the user
const [methodId, setMethodId] = useState(''); // State variable to store the method ID

const authenticate = async () => {
try {
const response = await sendOTP(phoneNumber);

// Check if response is empty
if (!response) {
console.error('Empty response received from sendOTP');
return;
Expand All @@ -34,33 +34,47 @@ function SMSOTPButton({ phoneNumber }: SMSOTPButtonProps) {
const responseData = await response;
setMethodId(responseData.phone_id);

// Set state to open the modal
setOpenModal(true);

} catch (error) {
// Handle errors here, e.g., display an error message
console.error('Failed to send OTP:', error);
}
};

const handleModalClose = () => {
// Clear OTP input and close the modal
setOTP('');
setOpenModal(false);
};

const handleOTPSubmit = async () => {
try {
// Call the authOTP function with methodID and otp
console.log('METHOD', methodId);
await authOTP(methodId, otp);

// Redirect to profile page
router.push('./profile');
} catch (error) {
// Handle errors here, e.g., display an error message
console.error('Failed to authenticate OTP:', error);
}
};

const handleResend = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
authenticate();
};

return (
<div>
<button className="full-width" onClick={authenticate}>
Authenticate
</button>

{/* Modal for OTP input */}
{openModal && (
<div style={styles.modalOverlay}>
<div style={styles.modal}>
Expand All @@ -77,7 +91,7 @@ function SMSOTPButton({ phoneNumber }: SMSOTPButtonProps) {
placeholder="Enter OTP"
/>
<p style={styles.smsDisclaimer}>
Didn&apos;t receive a code? <a style={styles.smsDisclaimer} href="#" onClick={(e) => { e.preventDefault(); authenticate(); }}>Resend</a>
Didn&apos;t receive a code? <a style={styles.smsDisclaimer} href="#" onClick={handleResend}>Resend</a>
</p>
<button className="full-width" onClick={handleOTPSubmit}>Submit</button>
</div>
Expand All @@ -87,7 +101,6 @@ function SMSOTPButton({ phoneNumber }: SMSOTPButtonProps) {
);
}


const styles: Record<string, React.CSSProperties> = {
modalOverlay: {
position: 'fixed',
Expand Down
2 changes: 0 additions & 2 deletions components/EmailSMS/SMSRegister.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ function SMSRegister() {
}

const responseData = await response;
console.log('Response data from sendOTP:', responseData);
setMethodId(responseData.phone_id);
console.log('Method id', responseData.phone_id);

setOpenModalPhone(false);
setOpenModal(true);
Expand Down
7 changes: 2 additions & 5 deletions components/EmailWebAuthn/LoginWithEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const STATUS = {
ERROR: 2,
};

const auth_domain = {
login: '/recipes/api-webauthn/magic-link-authenticate',
signup: '/recipes/api-webauthn/magic-link-authenticate'
}
const EML_REDIRECT = "/recipes/api-webauthn/magic-link-authenticate";

const LoginWithEmail = () => {
const [emlSent, setEMLSent] = useState(STATUS.INIT);
Expand Down Expand Up @@ -42,7 +39,7 @@ const LoginWithEmail = () => {
}

if (isValidEmail(email)) {
const resp = await sendEML(email, auth_domain.login, auth_domain.signup);
const resp = await sendEML(email, EML_REDIRECT, EML_REDIRECT);
if (resp.status === 200) {
setEMLSent(STATUS.SENT);
} else {
Expand Down
11 changes: 3 additions & 8 deletions pages/recipes/api-sms-mfa/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,28 +110,24 @@ const styles: Record<string, React.CSSProperties> = {
};

export const getServerSideProps: GetServerSideProps = async (context) => {
// Get session from cookie
const cookies = new Cookies(context.req, context.res);
const storedSession = cookies.get('api_session');
// If session does not exist display an error

if (!storedSession) {
return { props: { error: 'No user session found.' } };
}

try {
const stytchClient = loadStytch();
// Validate Stytch session

const { session } = await stytchClient.sessions.authenticate({ session_token: storedSession });
// Get the Stytch user object to display on page

const user = await stytchClient.users.get({ user_id: session.user_id });

// Determine from the user object if this user has registered a webauthn device at this domain
const hasRegisteredPhone = user.phone_numbers.length > 0;

// Set phoneNumber with optional chaining to handle potential undefined
const phoneNumber = user.phone_numbers[0]?.phone_number ?? '';

// Determine if user has access to the super secret area data
let superSecretData = null;
if (
session.authentication_factors.length === 2 &&
Expand All @@ -152,7 +148,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
},
};
} catch (error) {
// If session authentication fails display the error.
return { props: { error: JSON.stringify(error) } };
}
};
Expand Down
2 changes: 1 addition & 1 deletion pages/recipes/api-sms-mfa/sms-register.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import LoginWithSMS from '../../../components/EmailSMS/LoginWithSMS';
import LoginWithSMS from '../../../components/EmailSMS/LoginWithEmail';

export { LoginWithSMS as default };

0 comments on commit b180a6c

Please sign in to comment.