Thinking of implementing authentication to your fullstack app? Start here

Don't roll your own crypto.

It is incredibly difficult to be truly "secure" with your own solution, not to mention after you have publicized your application to the web, opening up your vulnerability to the world. Thus creating an unpleasant experience for your users.

So it is best to utilize PhD+++ level libraries to make sure these are up to code with the best of their capabilities. But even then I would not feel so invincible.

The principle is sound, if you don't roll your own crypto, why should you do the same for authentication?

The DIWhy Way

Imagine you're designing a responsive, ultra-fast web app. You already figured out what Javascript framework to use, which component library to design the frontend with and how the backend would serve the frontend. Now you need an auth system to keep track of users and do more user-specific things. So you came up with this checklist:

The checklist

  • New User Table in your Database
  • Frontend login component
  • Register new users component
  • Making sure the user is authenticated until some timeout or logout throughout the site traversal.
  • How to handle log out?
  • Auth injectors to check and secure certain endpoints in the app.
  • ...

After a while, you start to feel overwhelmed and decide to get on this tomorrow instead.
Clearly, this is a lot of work for something that you want as a nice add-on. It's kind of like playing with the Russian Doll, the more you open it up the deeper it goes.

And then you run into conundrums to determine which kind of authentication flow works best and how the heck would you implement it. What about speaking to the backend? Or when you want to secure your API endpoint as well, it keeps going.

Auth0 aims to mitigate all these issues for you.

The Auth0 Way

Firstly, this is not an Ad for Auth0, and I'm simply repurposing and comprehensively compiling information to serve my specific use case. All rights are reserved with Auth0 regarding the blog posts and resources.

Taken straight from their main website:

Auth0 is easy to implement, adaptable authentication and authorization platform.

Basically, we make your login box awesome.

In a nutshell, Auth0 is a 3rd party solution that takes care of 80% of the checklist, and some, on its server.

So how easy is it really?

Implementing Auth0

Let's assume our application is a backend Flask running the API and serving the React frontend (Single Page Application) bundled by Webpack.

So you might start by asking, "why don't I just let the backend handle the auth, propagating credentials and handling all authentication checks for both frontend and backend?". For one, it makes the communication between the two more complicated, as well as not creating a seamless workflow for your users.

A specific example would be that: The frontend directs the user to be authenticated, and since the backend handles communications with Auth0, the login/logout endpoints/views must be served directly by the API. This already is a bad idea since we should separate the view (to Frontend) and the controllers (to Backend). Another big issue is that the authentication flow requires multiple redirections, by which the frontend (who is waiting for the backend to return a valid access_token) will lose its connection.

We mitigate this by actually letting the Frontend(client) deal with asking for access tokens. Which we can use towards authenticating for any API requests to the backend. This workflow is called Implicit Flow.

Auth0 Configuration

Go ahead and create an account with Auth0. And login.

Since Auth0 manages all of our users' credentials and scopes, we can go ahead and create an API, this will help us define scopes like data:read, data:write,... whatever. These scopes are all custom and they can be whatever we want, essentially labels for our business logic.

API

Navigate to API section of the Dashboard then we can Create API

A couple notes:

  • Name: can be anything
  • Identifier: This should just be something like https://<BASE_URL>/api (example: https://127.0.0.1:5000/api). This can not be changed later.

After its creation, we can go to the Permissions tab in the API and create some scopes/permissions for our users. You can add data:read for simplicity, this can be finetuned to as granular as you like.

Single Page Application

Then we create a Single Page Application Type at Applications Page by + Create Application and choosing Single Page Web Applications. You can put whatever name you like.

And we are done for the time being.

Machine-to-Machine application (optional)

This part is completely optional, but it allows the user to request an access token through a CURL request. Which they can use to call protected endpoints of the Flask application.

All we gotta do is create a Machine to Machine App through the Applications Page by + Create Application.

No extra configuration is required.

Frontend

Implementing Auth0 to React is as easy as it gets, for which Auth0 has a great quick start guide here.

NOTE: During the URL configuration in the Auth0 Application settings:

  • Auth0 only accepts HTTPS connections so you would need to either proxy your local instance to self-sign to call Auth0 endpoints. For Flask, you can simply add ssl_context="adhoc" to the run() method of the Flask backend:
app.run(ssl_context="adhoc")
  • Auth0 backend does not accept localhost in Application URIs, for which you can simply use https://127.0.0.1:<PORT> (example: https://127.0.0.1:5000)
  • Additionally, make sure Refresh Token Rotation, Refresh Token Expiration (Absolute and Inactivity) are enabled. Which will allow us to persist tokens with the browser localstorage. These settings can be located below Application URIs

While optional, I recommend to use this wrapper for the Auth0Provider component:

import React from "react";
import { useNavigate } from "react-router-dom";
import { Auth0Provider } from "@auth0/auth0-react";

const Auth0ProviderWithHistory = ({ children }) => {
  const navigate = useNavigate();

  const onRedirectCallback = (appState) => {
    navigate(appState?.returnTo || window.location.pathname);
  };

  return (
    <Auth0Provider
      domain="YOUR_DOMAIN"
      clientId="YOUR_DOMAIN"
      redirectUri={window.location.origin}
      scope="read:data"
      useRefreshTokens
      cacheLocation="localstorage"
      onRedirectCallback={onRedirectCallback}
    >
      {children}
    </Auth0Provider>
  );
};

export default Auth0ProviderWithHistory;

EXPLANATION:

  • domain, clientId, redirectUri are defined like from the quickstart.
  • scope requests that this user be granted the permission that we defined in the API
  • useRefreshTokens tells Auth0 to also return refresh tokens along with the access token. Learn more
  • cacheLocation using localstorage for this means that the tokens are saved locally on the browser. This is secure since we enabled Refresh Token Rotation, Refresh Token Expiration (Absolute and Inactivity). Auth0 explains why very well
  • onRedirectCallback essentially returns the user to the page where they were redirected to login.

NOTE: If you are using any Component library which requires the Provider to wrap the <App>, make sure Auth0 Provider is a child of that element.

Example: Using with Chakra UI with React Router V6. ColorModeScript is a Chakra UI thing where you can set default colour schemes.

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <ChakraProvider>
        <Auth0ProviderWithHistory>
          <ColorModeScript initialColorMode={theme.config.initialColorMode} />
          <App />
        </Auth0ProviderWithHistory>
      </ChakraProvider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

Then to protect your React endpoints, simply return your pages/components wrapped with withAuthenticationRequired
Example: Loading component is simply a looping gif to prevent pages flashing.

import React from "react";
import { withAuthenticationRequired } from "@auth0/auth0-react";

import Loading from "../components/Loading";

function Page() {
  return (
    <div>
      <h1>New Page</h1>
    </div>
  );
}

export default withAuthenticationRequired(Page, {
  onRedirecting: () => <Loading />,
});

After configuring endpoints and authentications, we now would like to call our (to-be) secured backend, simply retrieve the access token and add it as a Bearer token as part of the HTTP request. As also documented here by Auth0 in the quickstart.

Backend

As mentioned we would be authenticated through the access token, which is a JWT, and the backend would need to understand the signing algorithm to decipher it. The signing algorithm can be found with the API that we created in Auth0.

Below is the code that handles errors, checking the validity of token and scope,...

This snippet was taken from my own project by which I used Blueprint to modularize services and reduce circular imports.

Here we defined multiple decorators to attach to endpoints with examples of how to handle each auth case:

# This doesn't need authentication
@bp.route("/test/public")
@cross_origin(headers=["Content-Type", "Authorization"])
def public():
    response = (
        "Hello from a public endpoint! You don't need to be authenticated to see this."
    )
    return jsonify(message=response)


# This needs authentication
@bp.route("/test/private")
@cross_origin(headers=["Content-Type", "Authorization"])
@requires_auth
def private():
    response = (
        "Hello from a private endpoint! You need to be authenticated to see this."
    )
    return jsonify(message=response)


# This needs authorization
@bp.route("/test/private-scoped")
@cross_origin(headers=["Content-Type", "Authorization"])
@requires_auth
def private_scoped():
    if requires_scope("read:data"):
        response = "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this."
        return {"message": response}
    raise AuthError(
        {
            "code": "Unauthorized",
            "description": "You don't have access to this resource",
        },
        403,
    )

And it's as simple as that, cross-origin to enable CORS on a per endpoint basis; requires_auth to extract Bearer tokens and validate it with Auth0.

Conclusion

I hope this gives you a nice and quick way to implement the authentication step so you can focus more on the business logic of the application.

Any question? Leave a comment below and I'll do my best to help out!

What is...?

  • An Access Token: obtained by authenticating the user with an Authorization Server and the user can then, in turn, authorize the application to access the API on their behalf.
  • JWT: Credentials, which can grant access to resources

22