Adventures Integrating Auth0 - JwT in a Distributed App (React, and Python/Flask)

 

Introduction

I was posed the question in an interview recently, "how do you secure your endpoints on an API?"  To be honest, I never really thought of this as something I needed to know. Up until this point, I've been relying on APIs built by others, and never really dug into how the process worked.  I did understand the overall concept of token based authentication, but I didn't know how to implement, or even how to create a decorator for authentication purposes.  So, I told the interviewer, I would use some form of token based authentication, and hoped that would suffice.  Of course, it didn't.  

So, in the interest of expanding my knowledge base, I decided not to be caught with my pants down in another interview where this topic may be raised again, and began a sample integration of Auth0.  As I began to develop the solution, as I generally do, using a distributed application architecture (where the API is hosted on a separate server from the front end application consuming it), I realized there wasn't any CLEAR documentation as to how to complete that "flow" as Auth0 calls it.  

For this blog post, I will be sharing my sample project/integration, with a simple React front end, served locally on one port and an API, written using Python/Flask, served on a separate port, to simulate a distributed application.  Feel free to use this as a working boiler plate, as of the writing of this post 05/14/2023.

You can clone both projects from my git repo, here...

Shopping Cart Website

Newer developers not familiar with the basics of web development, including tools such as VS Code, Git Repos, and how dependencies work, will probably not be able to follow along easily, as I will be assuming a rudimentary knowledge of web development overall.  

Create Your Auth0 Web Application

To take advantage of Auth0's authentication services, you first need to create an account and then create an application.  After you create your account, which is fairly straight forward, and login, you will be presented with your dashboard.  Click 'Applications' which will expand to show a sub-menu.  Select 'Applications' from the sub-menu, and you will be redirected to the page for viewing/creating your Application(s).



From here you select 'Create Application' and select 'Single Page Web Applications'...


Once you select, and click 'Create' you will be directed to enter some important details regarding your application.  First, under the 'Quick Start' tab, select the technology you are using for integration, and you will be presented with code samples, which will direct you to a page that explains exactly how to setup your project/code.  



The first step you will see for you to complete is to "Configure Auth0."  If you click this link and follow the steps, you will have setup your local project accordingly.  I'm not certain about Angular, and/or Vue, but for React, you will want to set your Client ID and Client Secret Keys in a .env file. The names will need to set preceded with REACT_APP_, such as "REACT_APP_CLIENT_ID, REACT_APP_CLIENT_SECRET, ETC..."

My suggestion is to leave the Quick Start page open, and right click the 'Settings' tab, and select 'Open in New Window'.  This way you can see your next steps, as you configure Auth0.  

Now that you have clicked, 'Settings' you should see the name of your app, domain, client id, client secret and a handful of settings, like Application Logo and Type, and a couple callback urls.  Don't let the callbacks throw you off.  All they want to know, is if a user logs off, on, what page do they send your user back to?  In my case it was the same as my main url, because I use the 'IsAuthenticated' member of the auth0-react library, to determine if a user is logged in/out.  Basically, if you use React, you don't need to handle anything for user login status.




Create .env File

In your .env file, you will be setting these settings, from your auth0 'Settings' page.



Okay, so the REACT_APP_CLIENT_ID and REACT_APP_CLIENT_SECRET should be self explanatory, as they relate to their values in your settings page, at the top.  The REACT_APP_AUTH0_DOMAIN, needs to be assigned the value specified in your Settings page, in the 'Domain' field.  
The REACT_APP_AUDIENCE setting can be any url you would like. Ideally, you will use your application's URL, but it can be anything, as long as it is the same url you set on your API as well.  This is how your API calls to validate the JWT that is passed to it by the client later.  Be careful setting this value to anything other than your app's url, as setting to something broader, could open you up to vulnerabilities later, as this sets a "group," so to speak.  

The REACT_APP_CALLBACK_URL, in this example, is reused for both the, 'Login,' and 'Logout' callbacks. But you can certainly set these values to anything that you'd like for your callbacks for Auth0 to be redirected to, but they must be the same as the values you've set on the Auth0 'Settings' page for your application. Notice how I've set these values to https://127.0.0.1:3000 since I'm developing locally?


The key to keep in mind here, is that in my application, I'm using the home page URL for all of my callbacks.  I prefer to keep it simple, and manage state from the auth0-react library, which provides me with the 'IsAuthenticated' member, which manages user login state.  
Last but not least, you will need to specify which URLs can make api calls of Auth0, for the purpose of CORS.  Learn about CORS here, Cross Origin Resource Sharing.


Once all of these settings are set in Auth0, and your React application, now you need to set these same values in the API project, but first you need to build the Auth0 API instance.  

Create the API Application

 On your left side navigation, click 'API' under Applications, 


Now, select 'Create API' and give it a name that will help you connect it easily with the front end application you've created.  This API application will be connected, not by name, but by it's "Identifier."  But for now, let's name it close to the name of the web app, so we don't question which API goes with which application later.  Assuming we make multiple APIs and apps.  


Remember the Audience setting in our React app?  This is the value that goes in the 'Indentifier' field of the API, and is how the front end application will identify itself by way of the JwT auth token it receives and passes to the API via Authentication header.  So, do yourself a favor and make sure this value matches the 'Audience' setting of your 'Application' in Auth0.

React Integration

With all of this set, you can start development of the front end and API layers of your distributed application.  For your React application, you will need the auth0-react library, as well as a library that allows you to make http(s) calls asynchronously.  I personally like to use Axios client.  Axios Client.  

On your 'Home' component, you can use "IsAuthenticated" from the auth0-react library, like this...

import { useAuth0 } from "@auth0/auth0-react";
...

function App() {
  const { loginWithRedirect, getAccessTokenSilently, logout,
            user, isAuthenticated } = useAuth0();

  useEffect(() => {
    if (!isAuthenticated || !sessionStorage.getItem('auth0AccessToken')) {
      checkLoginStatus();      
    } else {
      getCartCount();
    }

}, [checkLoginStatus, getCartCount, isAuthenticated, countSet]);

On initial pass, the code above will check to see if the 'isAuthenticated' member of auth0-react, is true, and if there is a value in the sessionStorage for 'auth0AccessToken', which is required, in the Authorization header value, in any subsequent calls to the API later.  If the user is not logged in on at the app level, the code will then call, 'checkLoginStatus' which is a method I've created to call for the token silently, according to Auth0's documentation.  (don't ask where I found this, because I cannot remember lol)

If the checkLoginStatus fails as well, it sets the a sessionStorage, which is empty.  If the user clicks, 'Login' on the page, it fires the other method, 'login,' which will call Auth0 for authorization, and Auth0 then takes over the process.  

  const login = async () => {
      await loginWithRedirect({
          appState: {
              returnTo: "/",
          },
          authorizationParams: {
              prompt: "login",
              screen_hint: "signup",
              audience: process.env.REACT_APP_AUTH0_AUDIENCE
          },
      });
  }

  const checkLoginStatus = useCallback(async () => {
      await getAccessTokenSilently({
          audience: process.env.REACT_APP_AUTH0_AUDIENCE,
          scope: ['email']
      })
          .then((response) => {  
            sessionStorage.setItem('auth0AccessToken', response);        
          })
          .catch((ex) => {
          });
  }, [getAccessTokenSilently]);

After clicking, "Login" the user is redirected to this page from Auth0, which then expects them to use some form of credential to identify themselves, 





Upon logging in, either via google, or any email address/pw combination they decide to signup with, they enable/approve themselves on your application and the API they will need to consume to use your application correctly, and Auth0 returns them to the page you set as your login callback.  In my case, I simply callback to the home page, and use the 'isAuthenticated' member of the Auth0 object to determine if the user is authenticated and display a different view based on this setting.  

The 'user' instance of the auth0-react library, and imported at the top of the class, 

import { useAuth0 } from "@auth0/auth0-react";
...

function App() {
    const { loginWithRedirect, getAccessTokenSilently, logout,
        user, isAuthenticated } = useAuth0();

contains the following data, if the user is logged in,

Make API Calls

So, it's great that you've integrated Auth0 into your React app, in that users can now login, logout from your application, but how do you call back to your API that provides xyz data for your front end, React app, to consume?

Well, I for one like to use the Axios Client for making HTTP calls to Rest APIs, for its simplicity mostly.  But use whichever library you wish.  The main thing you need to do, before you can call a secured endpoint, is to set the token you received after logging into Auth0, which is why we set the token in our sessionStorage earlier, remember?  To do this, we create a separate AxiosClient wrapper object, which we import into our Homepage, that sets the access token in the Authorization header, from the sessionStorage variable 'auth0AccessToken' to an instance of axios, and returns that instance, to be used for all subsequent requests for the logged in user.

./src/services/AxiosClient.js

import axios from 'axios';

const AxiosClient = () => {
  const defaultOptions = {
    baseURL: process.env.REACT_APP_API_PATH,
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  };

  // Create instance
  let instance = axios.create(defaultOptions);

  // Set the AUTH token for any request
  instance.interceptors.request.use(function (config) {
    const token = sessionStorage.getItem('auth0AccessToken');
    config.headers.Authorization =  token ? `Bearer ${token}` : '';
    return config;
  });

  return instance;
};

export default AxiosClient();

./src/app.js

import AxiosClient from './services';
import './App.css';

function App() {
...

Now that you've set the token in the header, and imported the AxiosClient wrapper we created above, you can now consume it in your code, and the Authorization header will be included in your future requests.  Ie...

  const getCartCount = useCallback(() => {    
    let url = `${process.env.REACT_APP_API_SERVER_URL}/cart`;

    if (user !== null) {
      const params = {
        "email": user.email
      };
   
      AxiosClient.get(url, {
          params
        }).then((result) => {
            if (result.data != null && result.status === 200) {              
                setCartCount(result.data.length);
            }
          })
          .catch((error) => {  
              if (error.response && error.response.status !== 404) {
                toast.error(error.response);
              }    
          })
          .finally(() => {
            setCountSet(true);
          });
    }
  }, [user]);

API Endpoint Integration - Python 

If you attempted to find any examples of Python integration on Auth0's website, you would be searching a barren wasteland.  I believe the example I worked from initially was provided by a third party, and I still don't remember all of the steps I took, from app inception, to full integration.  However, here is what you need to do in Python...

Set your .env vars.  Same as in your front-end, React project, you will need to create an .env file, where you set your Auth0 - client secret, client id, domain, and audience/identifier, as well as any other environment variables you need.


Next, make sure to pip install the following libraries,
  1. auth0-python
  2. authlib
  3. fastapi[all]
  4. flask
  5. flask-cors
  6. flask_restx
  7. OAuth
  8. pyjwt
  9. python-dotenv
  10. python-jose
  11. requests
Now, create a new folder, ./src/decorators, and a new file in that folder called, 'AuthDecorators.py'.  Paste the following code...

from functools import wraps
import json
from urllib.request import urlopen
from flask import request, _request_ctx_stack, jsonify
from jose import jwt
import os

AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN")
AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE")
AUTH0_ALGORITHM = 'RS256'

# Error handler
class AuthError(Exception):
    def __init__(self, error, status_code):
        self.error = error
        self.status_code = status_code
       
# Format error response and append status code
def get_token_auth_header():
    """Obtains the Access Token from the Authorization Header
    """
    auth = request.headers.get("Authorization", None)
    if not auth:
        raise AuthError({"code": "authorization_header_missing",
                        "description":
                            "Authorization header is expected"}, 401)

    parts = auth.split()

    if parts[0].lower() != "bearer":
        raise AuthError({"code": "invalid_header",
                        "description":
                            "Authorization header must start with"
                            " Bearer"}, 401)
    elif len(parts) == 1:
        raise AuthError({"code": "invalid_header",
                        "description": "Token not found"}, 401)
    elif len(parts) > 2:
        raise AuthError({"code": "invalid_header",
                        "description":
                            "Authorization header must be"
                            " Bearer token"}, 401)

    token = parts[1]
    return token

def requires_auth(f):
    """Determines if the Access Token is valid
    """
    @wraps(f)
    def decorated(*args, **kwargs):
        token = get_token_auth_header()
        jsonurl = urlopen("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json")
        jwks = json.loads(jsonurl.read())
        unverified_header = jwt.get_unverified_header(token)
        rsa_key = {}
        for key in jwks["keys"]:
            if key["kid"] == unverified_header["kid"]:
                rsa_key = {
                    "kty": key["kty"],
                    "kid": key["kid"],
                    "use": key["use"],
                    "n": key["n"],
                    "e": key["e"]
                }
        if rsa_key:
            try:
                payload = jwt.decode(
                    token,
                    rsa_key,
                    algorithms=AUTH0_ALGORITHM,
                    audience=AUTH0_AUDIENCE,
                    issuer="https://"+AUTH0_DOMAIN+"/"
                )
            except jwt.ExpiredSignatureError:
                raise AuthError({"code": "token_expired",
                                "message": "token is expired"}, 401)
            except Exception as ex:
                raise AuthError({"code": "invalid_header",
                                "message":
                                    f'"{str(ex)}"'
                                    " token."}, 401)

            _request_ctx_stack.top.current_user = payload
            return f(*args, **kwargs)
        raise AuthError({"code": "invalid_header",
                        "description": "Unable to find appropriate key"}, 401)
    return decorated

Now, the final step is to include the, @requires_auth() decorator to any endpoint you'd like to secure. Ie...

from flask import Flask, request, json
from flask_restx import Resource, Api
from flask_cors import cross_origin
from decorators.AuthDecorator import requires_auth
from services.CartService import CartService
from bson import json_util
import json

app = Flask(__name__)
api = Api(app)

@api.route("/cart")
class CartController(Resource):  
   
    def marshal_data(self, item):
        item['_id'] = str(item['_id'])
        return item  
 
    @requires_auth
    @cross_origin()
    def get(self):  
        cartService = CartService()
        email = request.args.get("email")  
        itemList: list = None
         
        if email is not None:
            itemList = cartService.get(email)
       
        if itemList is None:
            return "Record not found", 404
        else:
            return json.dumps(itemList)
 
    @requires_auth
    @cross_origin()
    def delete(self):  
        cartService = CartService()
        email = request.args.get("email")  
        inventory_id = request.args.get("inventory_id")  
        data: list = None
         
        if email is not None and inventory_id is not None:
            data = cartService.delete(email, inventory_id)
       
        return data
       
    @requires_auth
    @cross_origin()
    def post(self):
        cartService = CartService()
        email = request.args.get("email")  
        inventory_id = request.args.get("inventory_id")  
        data: list = None
         
        if email is not None and inventory_id is not None:
            data = cartService.add(email, inventory_id)
       
        return data

With the decorator created, and set on the endpoints you'd like secure, now it's time to test your endpoints, by logging into your React application using Auth0 and calling, from the code, using your new Authorization header token you set. 

Comments