Authentication: Create a Simple Rick and Morty Wiki Page

Written by minarai | Published 2021/09/24
Tech Story Tags: auth0 | sso | reactjs | create-react-app | authentication | context-api | rick-and-morty | hackernoon-top-story

TLDR In this tutorial, I'll use the most popular modern web application stack ReactJS (ReactApp) along with ExpressJS on the server-side for the base of our application. For the authentication part, we'll use Auth0 for a quick demonstration. via the TL;DR App

Photo by Kai Dahms on Unsplash

Authentication is the most crucial part of any application and implementing one would be too much of a pain and waste time and resources.

Luckily for us developers, there are many authentication solutions available ready for integration such as Auth0, KeyCloak, and ORY Hydra. With just a little integration code, we can add SSO to our application easily.

In this tutorial, I'll use the most popular modern web application stack ReactJS (CreateReactApp) along with ExpressJS on the server-side for the base of our application.

For the authentication part, we'll use Auth0 for a quick demonstration, but I will not rely on the package provided by Auth0 to make our code as platform-independent as possible. Doing it this way will help us with plugging another platform in the future.

The Project

Since every developer (including me) seems to love Rick and Morty, let's create a simple Rick and Morty wiki page, where users can create an account and bookmark his or her favorite characters.

We'll start with a standard client-server boilerplate:

  • Server-side: Restful API using Express JS.

  • Create React App on the client-side. We'll use Material UI for web components, reach-router for routing, react-query for performing API calls. There's nothing special about these packages. Most of the setup code is in client/src/App.js

  • For data, We'll use the ne-db package to read/write data from a database file instead of having to set up a fully-fledged MySQL. We'll use the Rick and Morty API and cache the data in our database file in server/data/characters.db

For the overall coding experience, we'll utilize the Yarn workspace for Monorepo and ease of deployment. This is our general source code structure:

.  
├── client  
│   ├── public  
│   ├── src  
│   │   ├── components  
│   │   ├── managers  
│   │   ├── routes  
│   │   ├── App.js  
│   │   ├── config.js  
│   │   ├── index.js  
│   │   └── theme.js  
│   ├── README.md  
│   ├── package.json  
│   └── yarn.lock  
├── server  
│   ├── data  
│   │   ├── characters.db  
│   │   └── likes.db  
│   ├── src  
│   │   ├── controllers  
│   │   ├── data  
│   │   ├── middlewares  
│   │   ├── routes  
│   │   ├── app.js  
│   │   ├── config.js  
│   │   ├── env.js  
│   │   └── helpers.js  
│   ├── package.json  
│   └── yarn.lock  
├── package.json  
└── yarn.lock

When going to the root folder and hit yarn start the code will run both client and server code

  • Client code is accessible at localhost:3000
  • Server API is accessible at localhost:8080

In the next sections we'll:

  • Register and create new Auth0 applications.

  • Implement login flow: this including a Login button that opens Auth0 login page, server-side and client code to handle callback.

  • Finally, an Authentication context to retrieve current users information

Create Auth0 Application

Register new Auth0 application is easy, follow the link Auth0 signup and create a new account if you haven't already had one. After successfully create a new account, go to the dashboard and open the default application.

In this screen, write down 3 pieces of information:

  • Domain: e.g: rickandmorty-wiki.us.auth0.com
  • Client ID: (e.g: Wb5VJ5rB6DKAwlWfI2GNw6prRx82BxHr)
  • Client Secret: should be base64 encoded

And since we're on this screen, let's change the allowed callback URL and logout URL to our server-side code:

  • Allowed Logout URLs: http://localhost:8080/callback
  • Allowed Callback URLs: http://localhost:8080/logout

That's it, quick and easy, and we're done with Auth0. Let's go back and implement some application code right away.

Login Flow

Login Button

The login code start in the client-side code with the implementation of the login URL. We'll need to point to the Auth0 app's login URL. In my case, it looks like this:

https://rickandmorty-wiki.us.auth0.com/authorize?response_type=code&client_id=Wb5VJ5rB6DKAwlWfI2GNw6prRx82BxHr&redirect_uri=http://localhost:8080/callback&scope=openid%20profile

And this is the code:

// client/src/components/urls.js
const createLoginUrl = () => {  
	const scope = "openid profile";  
	return `${ssoDomain}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;  
};
export const loginUrl = createLoginUrl();

LoginButton.js

// client/src/components/Layout/LoginButton.js

<Button component="a" href={loginUrl} variant="outlined">  
	 Login  
</Button>

Exchanging Code to Get Tokens

Clicking this will send the user to the App's login page.

After users log in successfully, they will be redirected to our server-side code specified in the redirect_uri parameter along with the code for exchanging the token.

This is how the callback URL looks like http://localhost:8080/callback?code=WKm4rHG7MCmLBigF

The next step is to exchange this code using our secret key to obtain JWT tokens from the Auth0 server.

Here is the code for that:

// server/src/routes/callback.js

router.get("/", async (req, res) => {
    try {
        const options = {
            method: "POST",
            url: `${authDomain}/oauth/token`,
            headers: {
                "content-type": "application/x-www-form-urlencoded"
            },
            data: queryString.stringify({
                grant_type: "authorization_code",
                client_id: authClientId,
                client_secret: authClientSecret,
                code: req.query.code,
                redirect_uri: authRedirectUri,
            }),
        };
        const response = await axios(options);
        res.redirect(`${clientUrlCallback}?id_token=${response.data.id_token}`);
    } catch (error) {
        res.send(error.response.data);
    }
});

export default router;

In the code above, we:

  • Extracted the code from req.query.code and sent it to Auth0 server along with client_id and client_secret to exchange for the JWT token.

  • JSON serialized the token and encoded it in base64 to send back to client code via redirect to client callback URL which is htpp://localhost:3000/callback

And if you're like me, you must be curious about the response. It's as below:

{
    "access_token": "tHcp7OWjiEebnSl49Jk2jsPnBvZEXZQg",
    "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkQ1WTlzYWtGVE5hQzNwOTlHcnRaeSJ9.eyJuaWNrbmFtZSI6ImplcnJ5X3NtaXRoIiwibmFtZSI6ImplcnJ5X3NtaXRoQGdtYWlsLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci8yMDM0YjAwZjZjMGRjMWI1YzBjMjVmY2Y3NjI3YTdmMD9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRmplLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIxLTA5LTE5VDA1OjAzOjUwLjI0MFoiLCJpc3MiOiJodHRwczovL3JpY2thbmRtb3J0eS13aWtpLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MTQ2MDkxNzY0NzA4YTAwNmE0NTA3Y2YiLCJhdWQiOiJXYjVWSjVyQjZES0F3bFdmSTJHTnc2cHJSeDgyQnhIciIsImlhdCI6MTYzMjAyNzgzMSwiZXhwIjoxNjMyMDYzODMxfQ.r_cbjbqB-8pqQ8bPk0IQT_8aUc1rartilWV1dgb8y7zKaVj-QHk9_5Nb9-oMhEjlgdex6MeSJSXeRbEo0Jm1EeImvyxl97cAKTfmDSRp3T_JnuyfE3bBuLe3p0PbIIdkwalKdllpus_p4ctxEbjgiNjldhCwTJI4SQZpj0XfaQUjV4cK5iFRLqIKl1w6XEpu8uL5yBX36I85DHiq-5eqXdlS3T_kttjF4OtOSZrssmWtqvRvN24tuvfiumJkfL3ZdeFiJbM2gG_bw802rx0V1U7YKY6vlbXy6SKYq8dqmXZX-awIp-smSQ48Qz_ipIaib9Mw2SmsdANfEjfhmDKowg",
    "scope": "openid profile",
    "expires_in": 86400,
    "token_type": "Bearer"
}

The id_token part is encoded. If you want to decode it, it will show the following data:

{
    "nickname": "jerry_smith",
    "name": "[email protected]",
    "picture": "https://s.gravatar.com/avatar/2034b00f6c0dc1b5c0c25fcf7627a7f0?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fje.png",
    "updated_at": "2021-09-19T05:03:50.240Z",
    "iss": "https://rickandmorty-wiki.us.auth0.com/",
    "sub": "auth0|6146091764708a006a4507cf",
    "aud": "Wb5VJ5rB6DKAwlWfI2GNw6prRx82BxHr",
    "iat": 1632027831,
    "exp": 1632063831
}

Implement client code to decode and store the id_token for later requests.

Let's get back to the client code and implement our callback route:

// client/src/routes/callback/CallbackPage.js

const CallbackPage = () => {
    const location = useLocation();
    const query = parse(location.search);
    LocalStorageManager.set(LS_AUTH_KEY, {
        id_token: query.id_token
    });
    return <Redirect to = "/"
    noThrow / > ;
};

export default CallbackPage;

We do have a LocalStorageManager utility to help us with dealing with serializing and deserializing data from the local storage.

// client/src/managers/LocalStorageManager.js

export class LocalStorageManager {
    static set = (key, data) => localStorage.setItem(key, JSON.stringify(data));
    static remove = (key) => localStorage.removeItem(key);

    static get(key) {
        try {
            return JSON.parse(localStorage.getItem(key));
        } catch (error) {
            localStorage.removeItem(key);
        }
    }
}

After this step, the id_token is stored in local storage and ready for consumption on other pages.

Create a Server-Side API to Return User Information

Next step, let's create an API endpoint at localhost:8080/profile to return the current logged in user information

The server API is easy, nothing special here, we only decode the information from JWT and send it back to the client. But depends on your business, we can combine this with data from our owned database if need be.

// server/src/routes/profile.js

router.get("/", checkJwt, (req, res) => {
    res.json({
        user: req.user
    });
});

In here we're using an express-jwt package to decode the JWT token and verify the integrity of the JWT key provided by client code, if the token expires, or is tempered, it will throw an authorization error.

There's some weird line in the code below regards to obtaining jwt secret. This code is specific for Auth0 and using the jwks package.

// server/src/middlewares/checkJwt.js

import jwt from "express-jwt";
import jwksRsa from "jwks-rsa";
import {
    authDomain
} from "../config";

function getAuth0SecretKey() {
    return jwksRsa.expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `${authDomain}/.well-known/jwks.json`,
    });
}

export const checkJwt = jwt({
    secret: getAuth0SecretKey(),
    issuer: [`${authDomain}/`],
    algorithms: ["RS256"],
});

After this step, if we plug the id_token in the previous step into Postman and send the GET request to our endpoint http://localhost:8080/profile we should be receiving our data back in a neatly json formatted manner.

Now we're having our API in place, let's head straight to the client code and implement our Auth Context.

Implement AuthContext Using React Context API to Provide Useful Information for All Pages

Since we will use the authentication data in many places in our application, the wise thing is to use context API to provide all pages with a means to access our user data.

Let's start small by creating an AuthContext object.

// client/src/components/AuthContext/index.js

export const AuthContext = React.createContext({});  

Then, the skeleton of the context provider AuthContextProvider. This will return an object that contains the actual user data, and 2 additional variables to handle the case of data loading or error.

// client/src/components/AuthContext/index.js

const AuthContextProvider = (props) => {
  // Call /profile api to get the user data here
  const context = {
    user: null,
    isLoading: null,
    error: null,
  };

  return (
    <AuthContext.Provider value={context}>
      <div>{props.children}</div>
    </AuthContext.Provider>
  );
};

We'll use react-query and axios to obtain the data.

const AuthContextProvider = (props) => {
  const { data, error, isLoading } = useQuery(["profile"], () => {
    const token = LocalStorageManager.get(LS_AUTH_KEY);
    if (!token) return null;
    const client = axios.create({
      baseURL: apiBaseUrl,
      headers: {
        Authorization: `Bearer ${token.id_token}`,
      },
    });
    return client.get("/profile").then((resp) => resp.data.user);
  });
  const context = {
    user: isLoading || error ? null : data,
    isLoading,
    error,
  };

  return (
    <AuthContext.Provider value={context}>
      <div>{props.children}</div>
    </AuthContext.Provider>
  );
};

Finally, let's throw in some code to handle error cases: when the token is expired and the server returns the 401 status code, let's remove the keys from the local storage. This is the complete code:

// client/src/components/AuthContext/index.js
export const AuthContext = React.createContext({});

const AuthContextProvider = (props) => {
  const { data, error, isLoading } = useQuery(
    ["profile"],
    () => {
      const token = LocalStorageManager.get(LS_AUTH_KEY);
      if (!token) return null;
      const client = axios.create({
        baseURL: apiBaseUrl,
        headers: {
          Authorization: `Bearer ${token.id_token}`,
        },
      });
      return client.get("/profile").then((resp) => resp.data.user);
    },
    {
      retryOnMount: false,
      retry: false,
      onError: (error) => {
        if (error.response && error.response.status === 401) {
          LocalStorageManager.remove(LS_AUTH_KEY);
        } else {
          throw error;
        }
      },
    }
  );
  const context = {
    user: isLoading || error ? null : data,
    isLoading,
    error,
  };

  return (
    <AuthContext.Provider value={context}>
      <div>{props.children}</div>
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);
export default AuthContextProvider;

As you can see, we also added some additional useAuth helpers for easier access to the data.

Next thing is to wire up the AuthProvider to the base of our application so that every page can receive the user.

// client/src/App.js

function App() {
  return (
    <QueryClientProvider client={client}>
      <AuthContextProvider>
        <Layout>
          <Router>
            <HomePage path="/" />
          </Router>
        </Layout>
      </AuthContextProvider>
    </QueryClientProvider>
  );
}

The very last step is to consume our provider and give users beautiful pages in the header:

// client/src/components/Layout/LoginButton.js

const LoginButton = () => {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return (
      <IconButton style={{ color: "white" }}>
        <CircularProgress size={20} />
      </IconButton>
    );
  }
  if (!user) {
    return (
      <Button component="a" href={loginUrl} variant="outlined">
        Login
      </Button>
    );
  }
  return <ProfileMenu user={user} />;
};

So now you've had it, full-featured authentication flow with React, Express, and Auth0 with only several files of code.

Conclusion

In this post, we can see how easy it is for modern applications to integrate pre-built SSO solutions.

The beauty of this is that we've been doing this from scratch without relying on the Auth0 provided libraries, so we can easily swap it with another vendor or open-source application later.

As for the server-side, it's easy to use Python Flask or Go instead of NodeJS.

For the future post in this series, I will implement the rest of the Logout flow, as well as create reusable components for blocking users out if they are not logging in.

Again, this is the complete source code hosted in Github and the application Rick and Morty wiki.


Written by minarai | Enjoy reading and writing about software development
Published by HackerNoon on 2021/09/24