3.E.6: Carousell Auth

Learning Objectives

  1. Know how to implement industry-standard authentication in an app with a SQL backend

Introduction

We will implement authentication in a simple Carousell clone that allows users to login, view, list and buy items. Users can browse items without logging in, but will need to login to list or buy items.

Setup

Fork and clone repos

Fork and clone Rocket Academy's Carousell Frontend and Carousell Backend repos.

Rocket has set up starter code such that users can:

  1. View listings from the home page

  2. List new items by clicking on "Sell" from the home page

  3. Buy items by clicking "Buy" at the bottom of item-specific pages

The starter code follows the structure of our Bigfoot frontend and backend. Rocket followed backend setup instructions in Bigfoot SQL to set up the backend.

Setup frontend

Run npm i to install packages

Setup backend

  1. Run npm i to install packages

  2. Create and update a .env to pass crendials to config/database.js Model the sample that is part of the repo, feel free to remove the sample when finished.

  3. Run npx sequelize db:create to create the carousell_development database

  4. Run npx sequelize db:migrate to set up database schema

  5. Run npx sequelize db:seed:all to seed users and listings in the database

  6. Verify seed data was added by viewing the users and listings tables in our SQL client

Verify starter code is working

  1. Start the backend with npm start

  2. Start the frontend with npm start, and select y to start it on port 3001 because the backend is running on port 3000

  3. Verify we do not get errors in our backend or frontend, and we can see 3 seed listings at localhost:3001 in our browser. Click on a listing to view its details. Buy and sell listings to observe what happens, and notice that the buyer and seller IDs have been hard-coded to 1 (the seed user).

Base: Require authentication to create listings and buy items

We will now put the theory we learnt in Rocket's Authentication submodule into practice.

Setup backend API authorisation

Refer to the following official Auth0 Express Authorization guide as a reference.

Setup Auth0 API in Auth0 dashboard

  1. Navigate to the API section of the Auth0 dashboard (under Applications in the left navbar) and click Create API

  2. Choose a name and identifier for our API. Rocket named ours Carousell API and used https://carousell/api as our identifier. Leave the signing algorithm as RS256.

  3. After creating the API we should see a Quick Start page. Ignore sample code on that page because it uses outdated libraries. We will be following setup instructions in the official Auth0 guide instead, which uses a new library that replaced the outdated libraries.

  4. Back in the official Auth0 guide, skip the section on defining permissions for now. By default all authenticated users will be able to list and buy items in our app.

Install Auth0 library in our backend and use Auth0 middleware to verify authentication on protected routes

  1. Install express-oauth2-jwt-bearer, Auth0's new, simplified library for verifying authentication

  2. Import the auth property of express-oauth2-jwt-bearer in our app like in the code example in the Auth0 guide. Note that Rocket's app setup requires import syntax instead of require syntax. Copy the checkJwt variable definition and replace YOUR_API_IDENTIFIER with the API identifier we chose above.

  3. Add checkJwt as a middleware between the route path and route handler function for our create-new-listing and buy-listing routes. See sample code under Protect API Endpoints section of Auth0 guide for reference. checkJwt will validate authentication before Express runs our route handler functions.

  4. No need to check scopes because we are not using scopes for now.

Testing the security of our API independently is more work than it's worth right now, so we will move on and test our API together with our frontend!

Login from frontend

We will follow the following official Auth0 guide for React apps.

Setup Auth0 application in Auth0 dashboard

  1. Create a new Auth0 application in the Auth0 dashboard. Give it the name "Carousell" and choose the Single Page Web Application application type.

  2. Add http://localhost:3001 to Allowed Callback URLs, Allowed Logout URLs and Allowed Web Origins in the new application's Application Settings page. Save changes at bottom of page. If we deploy our app later, we will also need to add the deployed URL to these sections.

Install Auth0 React library and configure Auth0 resources to be available in our app

  1. Install Auth0 React SDK with npm i @auth0/auth0-react

  2. Configure the Auth0Provider higher-order component that wraps all other components in index.js. If we are logged in when viewing the Auth0 guide, we can choose the relevant Auth0 Application and Auth0 will auto-populate the properties we need for Auth0Provider in the guide for us to copy.

Retrieve Auth0 resources from React context and use them to login in our app

We want our users to be able to view all listings and individual listings without logging in. We only want them to login when they have to, in our case when they wish to list or buy items.

We will use Auth0's loginWithRedirect function in 2 places: when unauthenticated users click "Sell" to list items and when unauthenticated users click "Buy" to buy items.

  1. Add a useEffect hook in NewListingForm component that calls loginWithRedirect if the current user is not authenticated. We can check authentication status with isAuthenticated boolean property returned by useAuth0 hook

    1. Retrieve the logged-in user's email with the user object property returned by useAuth0 hook and send that email as the seller's email to our backend in handleSubmit with the other listing data.

    2. Update the relevant method within the controller attached to the routing middleware in our backend to find or create the seller user in our backend before creating a new listing associated with that seller. Use the seller's email to retrieve their user ID in our backend. You may find Sequelize's findOrCreate class method helpful for finding a user with a given email, or creating a new user if no user exists with that email.

  2. Add logic in handleClick in the Listing component to loginWithRedirect a user if they are not yet authenticated. This will allow users to view items without authentication, but force them to authenticate before buying an item.

    1. Retrieve the logged-in user's email with the user object property returned by useAuth0 hook and send that email as the buyer's email to our backend in handleClick.

    2. Update the relevant route controller in our backend listingController to find or create the buyer user in our backend before updating the listing with the specified buyer's ID. Use the buyer's email to retrieve their user ID in our backend. You may find Sequelize's findOrCreate class method helpful.

Even though our users have logged in before selling or buying, our API server will still return 401 errors on sell or buy requests because we have not yet updated our requests to include auth information.

We are now ready to update our "sell" and "buy" API calls to include our auth tokens!

Send auth tokens from frontend

We will follow the following official Auth0 API-calling guide for React apps.

Setup frontend to retrieve auth tokens

  1. Add audience and scope props to Auth0Provider HOC in index.js in our frontend. The audience value should be the API identifier that we used to initialise checkJwt in our backend above. This may be different from the audience value in the Auth0 docs, because the Auth0 docs reference the Auth0 management API, not our custom API for this app.

Update sell and buy functionality on frontend to retrieve and send access token with API requests

  1. Retrieve getAccessTokenSilently function from useAuth0 hook in NewListingForm and Listing components to send the access token in sell and buy API requests respectively

  2. In NewListingForm component, in the handleSubmit function that runs on form submit, call getAccessTokenSilently to retrieve the access token, before passing the access token in a request header to the API call with Axios. See this tutorial and Axios docs on request method aliases and configs for how to set request headers in Axios requests.

    1. The audience parameter value should be the API identifier that we used to initialise checkJwt in our backend above.

    2. We will need to pass the access token as an Authorization header whose value starts with "Bearer ", followed by the access token. Sample code below.

  3. Do the same on handleClick for buy functionality in the Listing component. When the user clicks "buy", we should get the access token and send it with the API request to buy the listing.

Sample code from Rocket's handleSubmit function in NewListingForm component.

NewListingForm.js
// Retrieve access token
const accessToken = await getAccessTokenSilently({
  // TODO: Replace with your own app's audience. Should be same as API identifier in above steps.
  audience: "https://carousell/api",
  scope: "read:current_user",
});

// Send request to create new listing in backend
const response = await axios.post(
  `${BACKEND_URL}/listings`,
  {
    title,
    category,
    condition,
    price,
    description,
    shippingDetails,
    // User is currently logged-in user
    sellerEmail: user.email,
  },
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  }
);

Verify backend can authenticate with access tokens

  1. Run backend and frontend servers locally

  2. List a new item on our frontend and verify that the listing gets created with the seller ID of the current user

  3. Buy the newly-created listing on our frontend and verify that the listing now shows the buyer ID of the current user

Congratulations! We now have an app that allows us to buy and sell items with authenticated users!

Submission

Submit pull requests to the main branches of Rocket's Carousell Frontend and Carousell Backend repos respectively, and share your PR links in your section Slack channel.

If you would like to deploy, follow deployment instructions in Bigfoot SQL M-M.

Reference Solution

Here is reference code for the frontend and the backend for this exercise. You can do better!

Last updated