Remembering passwords can be a pain, especially if you have to do that for tens or hundreds of websites and applications you interact with. In this article, I will show you how to set up a passwordless authentication flow for your React web app using Auth0. Trust me, your users will thank you for it.
Preview of Sample Project
Table of Contents:
Create a React App
For this tutorial, I have created a starter React app to get the user interface development out of the way using Chakra UI, a simple modular component library. To begin, clone the starter branch of the repository.
git clone -b starter https://github.com/krizten/passwordless-auth.git
cd passwordless-auth
npm install
Set up Auth0 Account and Application
Auth0 is an easy-to-implement, adaptable authentication and authorization platform. Creating an Auth0 account is straightforward and you can do that just by signing up. If you are already a registered user, you can login here.
Next, you need to create a tenant and an application to handle JWT authentication for the React app.
-
Create a tenant
Creating a tenant as a new user
Creating a tenant as a new user
-
Create an application and select
Single Page Web Applications
as the application typeCreating an application
-
With the application successfully created, navigate to the settings tab and note down the
Domain, Client ID and Client Secret
. Scroll down to theApplication URLs
section and add the necessary URIs. To support multiple environments use a comma-separated list of URLs. See the images below to understand better. Other settings you can tweak are the settings for the ID and Refresh tokens. ClickSave Changes
when you are done.Basic Auth0 application information
Application URLs
Passwordless Connection
Auth0 Passwordless connections can be done using email and one-time-use code, using email and magic links or using SMS. The users enter their phone number or email address and receive a one-time-use code or magic link, which will be used to log the user in. The authentication method we will be using is the email and one-time-use code. You can find the implementation of other authentication methods here.
For Passwordless authentication with email, users profiles are created for new users before they are authenticated by Auth0. For existing users, Auth0 goes straight away to authenticate the user.
Passwordless Authentication Flow for New Users. (credits: Auth0)
Passwordless Authentication Flow for Existing Users. (credits: Auth0)
Configure the Passwordless connection with email by following these easy steps.
- From the dashboard, Go to the
Connection > Passwordless
and toggle on the email option. - Customise settings and options such as the email
From, Subject, Body, OTP Expiry and OTP Length
. ClickSave
when you are done. - Navigate to the Applications tab and toggle on the application you would like to apply the passwordless email connection to. Ensure you save when done.
In the event you prefer to use a different email provider other than the default Auth0 SMTP provider, here is a good place to start from.
Passwordless Connections
Passwordless Email Connections Settings (credits: Auth0)
Passwordless Email Conections Settings
Connecting Auth0 to The React App
With Auth0 set up and ready to go, let us head back to the React app and start the integration. Open the React project in your favourite code editor. I will be using Visual Studio Code.
Install dependencies
Next, we will install the Auth0 Client-Side JavaScript toolkit dependency and store2 utility library for easier handling of the localStorage.
npm install auth0-js store2
Passwordless connection start and login
Create a .env
file in the project root folder and add the Auth0 credentials generated in previous steps. Only the Domain and Client ID fields are necessary.
REACT_APP_DOMAIN=domain_here
REACT_APP_CLIENT_ID=client_id_here
To better manage the Auth0 library integration, create a helper services.js
file under the /src
folder and add the following methods.
import auth0 from "auth0-js";
export const webAuth = new auth0.WebAuth({
domain: process.env.REACT_APP_DOMAIN,
clientID: process.env.REACT_APP_CLIENT_ID,
responseType: "token id_token",
redirectUri: `${window.location.origin}/authorize`,
});
export const otpStart = ({ email }) => {
return new Promise((resolve, reject) => {
const variables = { email, connection: "email", send: "code" };
webAuth.passwordlessStart(variables, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
};
export const otpLogin = ({ email, otp }) => {
return new Promise((resolve, reject) => {
webAuth.passwordlessLogin(
{ email, connection: "email", verificationCode: otp },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
};
export const logout = () => {
webAuth.logout({
returnTo: `${window.location.origin}`,
clientID: process.env.REACT_APP_CLIENT_ID,
});
};
src/services.js
Integrate the helper methods to the Auth component. The first step is to call the otpStart
method to generate and send the one-time-use code to the email supplied. Once the user has received the code and entered it, call the otpLogin
method to log in the user.
...
import { useToasts } from "react-toast-notifications";
import { otpStart, otpLogin } from "../services";
const Auth = () => {
...
const { addToast } = useToasts();
const onChange = (e) => {
setFormState({ ...formState, [e.target.name]: e.target.value });
};
const requestOtp = async (email) => {
if (email && validateEmail(email)) {
setFormState({ ...formState, isSubmitting: true, otpAvailable: false });
try {
await otpStart({ email });
addToast(`OTP Code has been sent to ${email}`, {appearance: "success",
autoDismiss: true});
setFormState({ ...formState, isSubmitting: false, otpAvailable: true });
} catch (err) {
addToast(
`${err?.error_description || "Something went wrong. Please try again." }`,
{appearance: "error", autoDismiss: true,}
);
setFormState({ ...formState, isSubmitting: false });
}
}
};
const login = async ({ email, otp }) => {
setFormState({ ...formState, isSubmitting: true });
try { await otpLogin({ otp, email });
} catch (err) {
addToast(
`${err?.error_description || "Something went wrong. Please try again." }`,
{appearance: "error", autoDismiss: true,}
);
}
setFormState({ ...formState, isSubmitting: false });
};
const onSubmit = async (e) => {
e.preventDefault();
const { email, otp, otpAvailable } = formState;
if (otpAvailable) {
login({ email, otp });
} else {
requestOtp(email);
}
};
const displayOtpInput = () => {
setFormState({ ...formState, otpAvailable: true });
};
return (
<Layout hideHeader={true}>
<Box>
<Box>
<form onSubmit={onSubmit}>
<FormControl isInvalid={formFieldsErrors.email ? true : false} >
<Box>
<FormLabel htmlFor="email">Email Address</FormLabel>
{formState.otpAvailable && formState.email && validateEmail(formState.email)
&& (<Button to="/auth" onClick={() => requestOtp(formState.email)}>Resend
</Button>
)}
</Box>
<Input name="email" value={formState.email} onChange={onChange} />
<FormErrorMessage>{formFieldsErrors.email}</FormErrorMessage>
</FormControl>
{formState.otpAvailable && (<FormControl>
<FormLabel htmlFor="otp">OTP Code</FormLabel>
<OtpInput value={formState.otp} onChange={(otp) => setFormState({
...formState, otp })} numInputs={6} isInputNum={true} isDisabled={!true} />
</FormControl>
)}
<Button isLoading={formState.isSubmitting} isDisabled={formState.isSubmitting}
onClick={onSubmit}
>
<span>{formState.otpAvailable ? "Login" : "Request Access"}</span>
</Button>
</form>
</Box>
<span>Already have a code?</span>{" "}
<ButtononClick={displayOtpInput}>Use Now</Button>
</Box>
</Layout>
);
};
export default Auth;
src/pages/Auth.js (complete code here)
Extract ID token and User details
After the user successfully authenticates, the user is redirected to the redirect URL provided (/authorize in our case). The URL contains several hash parameters. To parse the URL for hash parameters, the Auth0 library comes equipped with parseHash
method. Open the Authorize
component handles the redirects.
import React, { useEffect } from "react";
import { Redirect } from "react-router-dom";
import Loader from "../Loader";
import { logout, webAuth } from "../services";
const Authorize = () => {
const parseAuthToken = () => {
if (window.location.hash) {
webAuth.parseHash({ hash: window.location.hash }, (err, res) => {
if (err) {
// handle errors here
return;
}
const { idToken } = res;
const { email, name, picture, sub: id } = res.idTokenPayload;
});
}
};
useEffect(parseAuthToken, []);
return <Loader />;
};
export default Authorize;
Manage application state
Data such as the user details, ID token and authentication state need to share across several components at different levels. Managing the state using React Context at an application layer level is an elegant approach to resolving this. Create a store.js
file under the /src folder.
import React, { createContext, useReducer } from "react";
import storage from "store2";
const initialState = {
isAuthenticated: false,
authToken: storage.has("auth_token") ? storage.get("auth_token") : "",
user: undefined,
};
const types = {
AUTHENTICATE_USER: "AUTHENTICATE_USER",
SET_USER_DATA: "SET_USER_DATA",
RESET_AUTH: "RESET_AUTH",
};
const reducer = (state, action) => {
switch (action.type) {
case types.AUTHENTICATE_USER:
storage.set("auth_token", action.payload);
return {...state, isAuthenticated: true, authToken: action.payload};
case types.SET_USER_DATA:
return {...state, user: action.payload};
case types.RESET_AUTH:
storage.clearAll();
return {...state, isAuthenticated: false, authToken: undefined,user: undefined,}
default:
return state;
}
};
const StoreContext = createContext(initialState);
const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatches = {
authenticateUser(authToken) {
dispatch({type: types.AUTHENTICATE_USER, payload: authToken});
},
setUserData(data) {
dispatch({type: types.SET_USER_DATA,payload: data});
},
resetAuth() {
dispatch({type: types.RESET_AUTH});
},
};
return (<StoreContext.Provider value={{ ...state, ...dispatches }}>
{children}
</StoreContext.Provider>);
};
export { StoreContext, StoreProvider };
src/store.js
Add the StoreProvider to index.js
.
...
import { StoreProvider } from "./store";
ReactDOM.render(
<StoreProvider>
<App />
</StoreProvider>,
document.getElementById("root")
);
src/index.js
In the Authorize
component, we can now set the user's details and ID token and update the application state.
import React, { useEffect, useContext } from "react";
import { Redirect } from "react-router-dom";
import Loader from "../Loader";
import { logout, webAuth } from "../services";
import { StoreContext } from "../store";
const Authorize = () => {
const {
authenticateUser,
setUserData,
isAuthenticated,
resetAuth,
} = useContext(StoreContext);
const parseAuthToken = () => {
if (window.location.hash) {
webAuth.parseHash({ hash: window.location.hash }, (err, res) => {
if (err) {
resetAuth();
logout();
return;
}
const { email, name, picture, sub: id } = res.idTokenPayload;
setUserData({ email, name, picture, id });
authenticateUser(res.idToken);
});
}
};
useEffect(parseAuthToken, []);
if (isAuthenticated) {
return <Redirect to="/dashboard" />;
}
return <Loader />;
};
export default Authorize;
src/pages/Authorize.js
Similarly, we can now set properties in other components by fetching data from the global store.
...
import { useDocTitle } from "../useDocTitle";
import { StoreContext } from "../store";
const Register = () => {
const { user } = useContext(StoreContext);
useDocTitle("Dashboard | Passwordless Authentication");
return (
<Layout>
<Box>
<Heading>Hi, {user?.email}!</Heading>
</Box>
</Layout>
);
};
*src/pages/Dashboard.js
import React, { useContext } from "react";
import { Redirect, Route } from "react-router-dom";
import { StoreContext } from "./store";
const ProctectedRoute = ({
component: Component = null,
render: Render = null,
path,
exact,
...rest
}) => {
const { isAuthenticated } = useContext(StoreContext);
const routeComponent = (props) =>
isAuthenticated ? (
Render ? (
Render(props)
) : Component ? (
<Component {...props} />
) : null
) : (
<Redirect to={{ pathname: "/", state: { from: props.location } }} />
);
return <Route path={path} exact={exact} {...rest} render={routeComponent} />;
};
export default ProctectedRoute;
src/ProtectedRoute.js
You can check out the complete source code here to see all the store integrations.
Conclusion
The Auth0 platform seamlessly manages your authentication and authorization flow. In this tutorial, we have covered how to set up an Auth0 connection to manage the authentication flow of a web application without the need for a password.
You can find the source code for this example on Github at krizten/passwordless-auth.
Resources
Please share this tutorial if you have found it valuable. If you are new around here or yet to get added to our mailing list, please subscribe to get a notification as soon new and exciting contents drops.