How to Setup Passwordless Authentication with Auth0

How to Setup Passwordless Authentication with Auth0

Code Memoirs's photo
Code Memoirs
·Feb 15, 2021·

9 min read

Subscribe to my newsletter and never miss my upcoming articles

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.

passwordless-auth-2.gif

Preview of Sample Project

Prerequisites:
  • Node.js installation (v12+)
  • Basic knowledge of JavaScript and React.
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.

  1. Create a tenant

    Creating Auth0 tenant as a new user Creating a tenant as a new user Creating a tenant as a new user Creating a tenant as a new user
  2. Create an application and select Single Page Web Applications as the application type

    Creating Auth0 application Creating Auth0 application Creating an application
  3. With the application successfully created, navigate to the settings tab and note down the Domain, Client ID and Client Secret. Scroll down to the Application 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. Click Save Changes when you are done.

    Basic Auth0 application information Basic Auth0 application information Application URLs 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 New Users. (credits: Auth0)

Passwordless Authentication Flow for Existing Users. (credits: Auth0) Passwordless Authentication Flow for Existing Users. (credits: Auth0)

Configure the Passwordless connection with email by following these easy steps.

  1. From the dashboard, Go to the Connection > Passwordless and toggle on the email option.
  2. Customise settings and options such as the email From, Subject, Body, OTP Expiry and OTP Length. Click Save when you are done.
  3. 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 connection Passwordless Connections

Passwordless Email Connections Settings (credits: Auth0) Passwordless Email Connections Settings (credits: Auth0)

Passwordless Email Conections Settings 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.

 
Share this