DEV Community

Cover image for Auth0 "Embedded Login" with React
Ammar Raneez
Ammar Raneez

Posted on

Auth0 "Embedded Login" with React

Auth0 is an "Authentication as a Service" provider, which means that it provides an implementation of authentication into your application without you having to implement a full flow yourself. Handling of Id, Access, and Refresh tokens are by Auth0 itself hence, allowing you to focus on the application that you are building and worry less about the storage & access of tokens and security.

In this blog, I will break down how I implemented an authentication flow using Auth0 and React.


You might have come across the "auth0-react" package - a package that is an abstraction of the vanilla "auth0-js" package, which provides a higher-order API that makes the implementation so much simpler at the expense of using an Auth0-provided Authentication page - which handles the registering and log in (you would be redirected to that page). However, it can be customized if you have an account that has billing activated.

I will be using the vanilla "auth0-js" package since I will be using a free account and want the authentication process to happen within my application - an embedded login.


The Setup

There are quite a few steps required in setting things up within the Auth0 dashboard.

  • Navigate to the Auth0 website and create a new "tenant".
    create-tenant

  • Create a new application in the "Applications" sidebar of the created tenant.
    create-app

  • Navigate to the settings tab of the created application.

  • Add the URLs you will be using in development in the following sections. (Do not forget to update this whenever you use a different localhost, or once you deploy the application).
    add-urls

  • Enable refresh token rotation (if not enabled) - we will need this to implement the persistence of the user on refresh. 

  • Scroll down to "Advanced Settings" and click on the "Grant Types" tab. Ensure that the "Password" option is checked.
    enable-rf-token

  • Click on your created Tenant on the top left corner and navigate to "Settings".

  • Click on the "General" tab and scroll till you find "Default Directory" under "API Authorization Settings".

  • Add "Username-Password-Authentication" in the default directory. Make sure that there are not any typos.
    update-tenant-connection

  • Navigate to "Rules" on the sidebar and "Create" a new "Empty" rule. This rule will attach a "role" attribute, which we will specify, to the object that we will obtain on authentication. We will use this attribute to implement authorization.

    • Add your website name within <your-website>. Make sure you do not edit the namespace other than this. (The rule name can be anything you prefer).
    • This rule will run upon a login request, just before the id token is issued, thereby injecting the role into the id token. create-rule
  • Navigate to "Authentication" and create a new database connection, give it the name "Username-Password-Authentication".

  • One last step. Go back to your created application, copy the Domain, Client ID and Client Secret, and paste those values into a file in your project, in my case, I have pasted them into an env file, along with a few other values that are present in the below screenshot.
    env-file

    • Redirect URL refers to the URL you are running the application on; DB Connection is the database that we created; Response Type states in what form do we want the response on a login; Response Mode specifies where would the response show up - in our case, it would be appended into our URL as a fragment, however, this will not be used since we will be using an Embedded Authentication approach.
  • Finally, create a new file instantiating "WebAuth" - which comes from the "auth0-js" package as follows. (We need offline_access to obtain refresh tokens)

import auth0 from 'auth0-js';

export const webAuth = new auth0.WebAuth({
  domain: `${process.env.REACT_APP_AUTH0_DOMAIN}`,
  clientID: `${process.env.REACT_APP_AUTH0_CLIENT_ID}`,
  responseType: `${process.env.REACT_APP_AUTH0_RESPONSE_TYPE}`,
  redirectUri: `${process.env.REACT_APP_REDIRECT_URL}`,
  responseMode: `${process.env.REACT_APP_AUTH0_RESPONSE_MODE}`,
  scope: 'openid profile email offline_access'
});
Enter fullscreen mode Exit fullscreen mode

Register

Now that the base setup is in place, we can get into the meat and potatoes. The below code snippet is an example of a signup process.

const loginUser = async () => {
  webAuth.client.login({
    realm: `${process.env.REACT_APP_AUTH0_DB_CONNECTION}`,
    username: email,
    password: password,
  }, async (err, result) => {
      if (err) {
        return err;
      }
      await authenticate(result);
  });
}

const webAuthLogin = async () => {
  webAuth.signup({
    connection: `${process.env.REACT_APP_AUTH0_DB_CONNECTION}`,
    email,
    password,
    user_metadata: {
      role: UserType.CUSTOMER,
    },
  }, async (err, result) => {
    if (err) {
      return err;
    }
    await loginUser();
  });
}
Enter fullscreen mode Exit fullscreen mode

Sign-ups require an email/username and a password. Along with that, you can send additional metadata to enrich a user's profile within user_metadata. If you recall, this attribute is what we referred to to obtain the role attribute.

If the base setup is all good, this request should be successful and you should be able to view this user in the "Users" tab under "User Management".

The obtained result will be an enriched object containing the id and access tokens. The login function called logs the registered user into the application. I will get into that next.


Login

The login flow is relatively straightforward at first glance, as visible in the snippet above. However, it is a slight bit more work to implement the authenticate function that is being called on a successful response.

The following snippet is the authenticate function.

const authenticate = async (result) => {
  auth0Service.handleAuthentication(result);
  await auth0Service.setUserProfile(result.accessToken, result.idToken, dispatch);
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet, an external service is called that does the behind the scenes functionality necessary to persist the user on a page refresh. If persistence is not necessary, this step is not required - the obtained result would suffice.


handleAuthentication is all about storing the tokens in session storage(local storage would work too).

public handleAuthentication(result: any): void {
  if (result.idToken || result.id_token) {
    this.setSession(result);
  } else {
    History.push('/');
    window.location.reload();
  }
}
private setSession(result: any) {
  const expiresAt = result.expiresIn ?   JSON.stringify(result.expiresIn * 1000 + new Date().getTime())
    : JSON.stringify(result.expires_in * 1000 + new Date().getTime());
  this.setSessionStorage(result, expiresAt);
}
private setSessionStorage(result: any, expiresAt: any): void {
  sessionStorage.setItem('refresh_token', result.refreshToken ? result.refreshToken : result.refresh_token);
  sessionStorage.setItem('expires_at', expiresAt);
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet, the result is passed to setSession that obtains the expiry time of the token, to ensure that only a token that is not expired can be used. setSessionStorage stores the obtained refresh token and the expiry time into session storage. (the checks for result.idToken & result.id_token and result.refreshToken & result.refresh_token is sole because there is a possibility that Auth0 returns them as either camelCase or snake_case)

The reason why the refresh token is stored in session storage and not the id or access tokens is to avoid CSRF attacks (since they contain sensitive information). However, the refresh token does not contain any - it is solely used to obtain other access tokens, thereby having no meaning by itself.


setUserProfile is about storing the authenticated user in memory - in this case, redux.

public async setUserProfile(
  accessToken: string,
  idToken: string,
  dispatch: any,
): Promise<any> {
  webAuth.client.userInfo(accessToken, async (err: any, result: any) => {
    if (err) {
      console.error('Something went wrong: ', err.message);
      return;
    }
    return this.authenticateUser(
      accessToken,
      idToken,
      result,
      dispatch,
    );
  });
}

private async authenticateUser(
  accessToken: string,
  idToken: string,
  result: any,
  dispatch: any,
) {
  dispatch(
    login({
      email: result?.email,
      userType: result?.['https://<your-website>/claims/role'],
      idToken,
      accessToken,
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet, the obtained access token is used to get the user information, that was used to sign up. This information then is dispatched to redux. (In the rule, we specified to return the role attribute in our result object. If more information is required, it is as simple as adding that in the same rule 😁).


Persistence On Refresh

Now that we have integrated a portion of persistence within login, this section will focus on restoring the logged-in user on refresh.

// App.jsx
useEffect(() => {
  const dispatchUserData = (authResult) => {
    const { user } = authResult.data;
    dispatch(
      login({
        email: user?.email,
        accessToken: authResult.access_token,
        idToken: authResult.id_token,
        userType: user?.user_metadata?.role,
      })
    );
  }
  const setAuthenticatedUser = async () => {
    let authResult;
    if (isUserAuthenticated) {
      authResult = await auth0Service.getInitialAuthenticatedUser();
    }
    if (authResult) dispatchUserData(authResult);
  }
  setAuthenticatedUser();
}, [auth0Service, dispatch, isUserAuthenticated]);

// External File
public async getInitialAuthenticatedUser(): Promise<any> {
  if (sessionStorage.getItem('refresh_token')) {
    const isUserAuthenticated = this.isAuthenticated();
    const refreshTokenResponse = await this.getUserWithRefreshToken();
    if (isUserAuthenticated && refreshTokenResponse) {
      this.handleAuthentication(refreshTokenResponse);
      const user = await getUser(refreshTokenResponse.access_token);
      return { ...user, ...refreshTokenResponse };
    }
  }
}

public isAuthenticated(): boolean {
  const date = sessionStorage.getItem('expires_at');
  const refreshToken = sessionStorage.getItem('refresh_token');
  if (date && refreshToken) {
    const expiresAt = JSON.parse(date);
    if (!refreshToken || (new Date().getTime() > expiresAt)) {
      this.removeSessionStorage();
      return false;
    };
    return true;
  }
  return false;
}

private async getUserWithRefreshToken(): Promise<any> {
  const response = await axios.post(`https://${process.env.REACT_APP_AUTH0_DOMAIN}/oauth/token`,
    {
      grant_type: 'refresh_token',
      client_id: `${process.env.REACT_APP_AUTH0_CLIENT_ID}`,
      refresh_token: sessionStorage.getItem('refresh_token'),
      client_secret: `${process.env.REACT_APP_AUTH0_CLIENT_SECRET}`
    },
    { headers: { 'Content-Type': 'application/json', }, },
  );
  return response.data;
}

private async getUser(accessToken: string): Promise<any> {
  webAuth.client.userInfo(accessToken, async (err: any, result: any) => {
    if (err) {
      console.error('Something went wrong: ', err.message);
      return;
    }
    return result;
  });
}
public removeSessionStorage(): void {
  sessionStorage.removeItem('refresh_token');
  sessionStorage.removeItem('expires_at');
}
Enter fullscreen mode Exit fullscreen mode

The snippet above is placed in the App file because it runs on page load. The useEffect defined calls a helper function to obtain the current logged in user and stores them in redux.

getInitialAuthenticatedUser calls a function that checks if the user is authenticated. This function, isUserAuthenticated verifies that the token stored in session storage is not expired (it removes it if so and returns false - that there is no user).
 
The getUserWithRefreshToken function speaks for itself. It calls an API of your created Auth0 application passing the refresh token available in session storage to obtain a response. The same procedure is followed where the newly obtained refresh token is stored in session storage overriding the currently existing one.

getUser is called with the obtained access token that will finally return the user object.


Congratulations! You now have a working Authentication flow implemented using Auth0 😁


Keep Growing!

Top comments (0)