Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. Setup. GitHub and Git. Visual Studio code editor. Node, Yarn. package.json. TypeScript. ESLint, Prettier. Next.js. Server-side rendering. Project structure. Document HOC. App HOC. Index page. Testing. Environmental variables.
  3. Material-UI. Client-side and server-side rendered pages. Dark theme, CssBaseline. Shared layout. Adding styles. Shared components. MenuWithLinks. Notifier. Confirmer. Nprogress. Mobile browser.
  4. HTTP, request, response. APP project. Fetch method. API method at Index page. Next-Express server. Express route. Asynchronous function, Promise, async/await. API server. New project API. Updating APP.
  5. Infrastructure for User. MongoDB database. MongoDB index. Jest testing for TypeScript. Your Settings page. API infrastructure for uploading file.
  6. Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.
  7. AWS SES API. Passwordless OAuth API. Mailchimp API.
  8. Application state, App HOC, store and MobX. Toggle theme API. Team API. Invitation API.
  9. Discussion API. Post API. Websockets for Discussion and Post.
  10. Stripe API - API project. Stripe API - APP project. Setup at Stripe dashboard and environmental variables. Email notification for new post API - API project. Amazon API Gateway and AWS Lambda.
  11. Environmental variables, production/development. Logger. APP server. API server. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. Testing application in production. AWS Elastic Beanstalk.

Chapter 5: Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.

We keep our book up to date with recent libraries and packages.


The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.


In Chapter 5, you will start with the codebase in the 5-begin folder of our saas repo and end up with the codebase in the 5-end folder.

We will cover the following topics in this chapter:

  • Login page
    - LoginButton

  • Session and cookie
    - Configure session and mount middleware
    - Save session to database
    - Configure and create cookie

  • Google OAuth API
    - Express routes for Google OAuth API
    - Configure passport
    - verify function
    - passport and session
    - Static methods publicFields and signInOrSignUpViaGoogle
    - getUserApiMethod API method and '/get-user' Express route
    - Google Cloud Platform
    - Testing Google OAuth API

  • Authentication HOC withAuth
    - getUserApiMethod in withAuth
    - Redirect logic in withAuth
    - Render logic in withAuth HOC
    - Testing withAuth HOC

  • firstGridItem logic in App HOC


We learned many useful topics in Chapter 4. A particularly useful topic was creating "two project" architecture and building internal and external API infrastructures within this architecture. For example, we defined and built "Getting user by slug API". For this API, req-res cycles are between app, api, and a MongoDB server. We built the more complicated "Uploading file API" that consists of two sub-APIs. The first sub-API, called "Getting signed request API", involves app, api, and an AWS S3 server. The second sub-API, called "Uploading file using signed request API", involves only app and an AWS S3 server, without api.

As you progress through this book, we will practice our API-building skills many more times. We will build both "internal" and "external" API infrastructures. Internal APIs will have req-res cycles in between app, api, and a MongoDB server. External APIs involve external, third-party servers other than MongoDB server. In Chapter 4, we built "Uploading file API", which is an external API, since it involves AWS S3 server.

Building various API infrastructures is one of key skills of a web developer.

Currently, our web application has no way to differentiate beetween a guest (or logged-out) user and a logged-in user. Our primary goal in Chapter 5 is to, finally, add user authentication to our web application. We will implement user authentication using Google OAuth API infrastructure. Then in Chapter 6, we will implement an alternative user authentication method using Passwordless API.

So you can see that adding user authentication to our web application amounts to adding a new external API infrastructure that involves Google OAuth server. As you may have concluded at this point, adding any new data exchange to our web application means adding a new internal or external API infrastructure.

In the first section of this chapter, we will discuss and create a Login page. Then we will discuss the concepts of session and cookie. After that, we will discuss details of the Google OAuth API infrastructure. After that, we will add user authentication by defining a new higher-order component, withAuth.


Login page link

In this section, we will create a new page: Login. The page is relatively simply to implement, since you already implemented the Index and YourSettings pages in previous chapters. Check up the code for the Index page at book/5-begin/app/pages/index.tsx and the code for the YourSettings page at book/5-begin/app/pages/your-settings.tsx. The pattern for these pages can be summarized like this. Let's call such pattern a blueprint for pages:

// some imports go here

type Props = { user: { email: string; displayName: string } };

class Index extends React.Component<Props> {
    public static async getInitialProps() {
        // some JS/TS code goes here
    }

    public render() {
        return (
            <Layout {...this.props}>
                <Head>
                    <title>Index page</title>
                    <meta name="description" content="This is a description of the Index page" />
                </Head>
                // some HTML/React/Material-UI code goes here
            </Layout>
        );
    }
}

export default Index;

The Login page is similar to both Index and YourSettings pages in the following aspects:
- We define data types for Props of the page component.
- The Login page contains a Head element and main div element, and the page is wrapped with a Layout component.

The Login page differs from the Index and YourSettings pages in the following aspects: - We don't need to define getInitialProps on the Login page, since we don't need any dynamic data to populate the page's props - Instead of having Material-UI's Button component, the Login page will contain an imported custom component called LoginButton. LoginButton will indeed use the Button component. but we define this component in the next subsection.

The Login page is even more straightforward to define than the Index page:

import Head from 'next/head';
import React from 'react';
import LoginButton from '../components/common/LoginButton';
import Layout from '../components/layout';

class Login extends React.Component {
    public render() {
        return (
            <Layout {...this.props}>
                <div style={{ textAlign: 'center', margin: '0 20px' }}>
                    <Head>
                        <title>Log in to SaaS boilerplate by Async</title>
                        <meta
                            name="description"
                            content="Login and signup page for SaaS boilerplate demo by Async"
                        />
                    </Head>
                    <br />
                    <p style={{ margin: '45px auto', fontSize: '44px', fontWeight: 400 }}>Log in</p>
                    <p>You’ll be logged in for 14 days unless you log out manually.</p>
                    <br />

                    <LoginButton />
                </div>
            </Layout>
        );
    }
}

export default Login;

Place the above code into a new file, book/5-begin/app/pages/login.tsx.

Note how we wrote a descriptive SEO title and description for the Login page. Why did we do it? It's because, unlike the Index and YourSettings pages, we want search engine bots to crawl the Login page. We want people who search on Google or other search engines to be able to discover our Login page. When we deploy our web app in Chapter 10, the crawled URL for the Login page will be https://saas-app.async-await.com/login. We will also set up proper robots.txt and sitemap.xml files for our app project in Chapter 10.


LoginButton link

Unlike other non-page components that we created so far in this book (Notifier, Confirmer, MenuWithLinks), the LoginButton component has no need for state, because there is no need for storing any temporary data. If you need to remember how we created these non-page components, check up any file inside the book/5-begin/app/components/common/ folder.

The LoginButton component needs no methods except the LoginButton.render method. Create a new file, book/5-begin/app/components/common/LoginButton.tsx, with the following code:

import Button from '@material-ui/core/Button';
import React from 'react';

class LoginButton extends React.Component {
    public render() {
        const url = `${process.env.NEXT_PUBLIC_URL_API}/auth/google`;

        console.log(url);

        return (
            <React.Fragment>
            <Button variant="contained" color="secondary" href={url}>
                <img
                    src="https://storage.googleapis.com/async-await-all/G.svg"
                    alt="Log in with Google"
                />
                    Log in with Google
            </Button>
            <p />
            <br />
        </React.Fragment>
    );
}
}

export default LoginButton;

is called a "non-breaking space" HTML entity. On the browser, it renders as one empty space. HTML entities are special (reserved) strings of characters that start with & and render as some difficult-to-type characters:
https://developer.mozilla.org/en-US/docs/Glossary/Entity

We could have wrapped the text Log in with Google with <span>...</span> and applied styles to it to create space to the left of the text. Instead, we used the "non-breaking space" HTML entity.

We did not discuss the code for the LoginButton component in detail, since we already know how to build even more complicated components. However, there is one important fact to point out. So far in this book, every page we built had a page method that calls an API method to get some dynamic data inside the page's getInitialProps method. We have no getInitialProps method for Login page. Instead of an API method, we use a Button component with an href prop from Material-UI's library. This component renders into an HTML element: anchor <a>. When a user clicks on this anchor element, the browser automatically sends a request with the method GET to the API endpoint ${process.env.URL_API}/auth/google. No need for us to define a page method and API method in addition to <a> anchor element, all we need to do is to define the Express route /auth/google on our api server.

Since we already imported LoginButton to the Login page and used it, it's time to see if our page renders as expected. Since the Login page does not have any methods that send requests to the api server, we can load the Login page without starting our api project.

Start your app project with yarn dev and navigate to http://localhost:3000/login:
Builder Book

The page looks good, but the grid item on the left looks out of place. Later in this chapter, we will update our App HOC so that firstGridItem has a value of false for the Login page and we do not see the left grid item on the Login page. The Login page will ultimately have only one grid instead of two.

The terminal window for your app project prints:

http://localhost:8000/auth/google

This output is from:

console.log(url);

The API endpoint has a proper value; however, we haven't implemented an Express route /auth/google at api project to process this request. We will create this Express route later, in the section "Google OAuth API".

In the next section, we will discuss the concept of session and cookie. It's important to understand these concepts before we discuss and add user authentication to our web application. After that, we will build our user authentication using Google OAuth API.

Session and cookie link

Before we build an infrastructure for user authentication, it is critical to understand the concept of session and cookie.How does a web application identify an end user who loads a page? How does a web application keep an end user logged-in? You can log in to any web application on the internet, close the tab, reopen the tab, and find that you are still logged in. This is called persistent login session and its a common feature of most of web applications on the web today.

The short answer, persistent login session can be achieved by setting up session on the server and cookie on the browser.

When an end user logs in to web application for the first time, the web application saves a so-called cookie object to the end user's browser. When this user later comes back and loads the web application's page into browser's tab, the cookie's name and value are sent to the server with an initial request. The server uses cookie's value to find a matching session document in the database. The session document will contain the user's id, which the server uses to find and send back matching user data back to the browser.

Ok, we understand how a returning user can remain logged in, at least, in theory for now. Let's discuss details of implementation for session/cookie infrastructure.

What is session? As we already mentioned, session is an object that a server can create to store unique user-identifying information. The session object gets created by the server and is not accessible on the browser. When an end user logs in to your web application, the server can create a unique session object. For example, you can save a user's id into session. Then your server can save session object to your database as Session MongoDB document. You could make a persistent login session last for 14 days, thus sparing any user from having to re-log in to your web application for 14 days. You simply need some code on your server that removes Session MongoDB document from database after 14 days.

  • When a logged-out or completely new user logs in or signs up, respectively, on a web application, our server generates a unique session object that contains a user's id and saves a Session MongoDB document to our database. Then our server creates a unique cookie object that has name and value. The value for value is generated from the session's id. Our server sends a response to the browser that contains cookie, and cookie gets saved to a user's browser.
    Builder Book

  • Later, a logged-in user can close a tab within our web application, come back later, and open our web application in a new browser tab. The user's browser will send a request to our server. This request contains cookie. Our server decodes value of the received cookie. Our server finds a matching Session MongoDB document by the session id in our database. Inside this session object, there is a user id. Our server finds a matching User MongoDB document by the user id and sends user data back to the browser. Thus, the user remains in a logged-in state in a new browser tab; in other words, our web application provides users with a persistent login session.
    Builder Book

So how does cookie, that is already saved to the browser, gets sent from the browser to the server so that end user can enjoy a persisten login session? It's always important to remember that app project is Next.js and is capable of rendering pages on both the server and the browser. cookie (name and value) gets sent from the browser to the server:

  • For client-side rendered pages, because credentials: 'include' for all requests, we specified this value inside the definition of sendRequestAndGetResponse method that all API methods use in our web application. Open book/5-begin/app/lib/api/sendRequestAndGetResponse.ts file and find line with:

    Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }),

    Setting credentials value to include makes request to contain cookies even for cross-origin calls (and calls between app and api projects is cross-origin):
    https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

  • For server-side rendered page, there is request from the browser to app server and then there is request between two servers, app server and api server. Original request for page comes from the browser to the app server first. So app server has cookie because of credentials: 'include'. But how to ensure that request from app server to api server includes cookie as well? Actually, it already does! Because of the code we wrote earlier but promised to explained later. Open book/5-begin/app/lib/api/sendRequestAndGetResponse.ts file and find the following code block:

    if (request && request.headers && request.headers.cookie) {
      headers.cookie = request.headers.cookie;
    }

    And then find how headers that contain cookie are used:

    Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }),

    So what does the above code mean? It means that we take cookie from the very first request (from the browser to the app server), which is request.headers.cookie. Then we save this cookie value to headers by assigning value to headers.cookie. Then we use these headers with cookie for the second request (from the app server to the api server), thus ensuring that api server has cookie value. We still did not add any code that translates cookie value into matching session and user but we will do so below.

Builder Book

Let's create our very first session and cookie in this book. Look at the above diagram again. Let's outline our work:

  • When the YourSettings page loads, getUserBySlugApiMethod executes, and the browser sends a request to the API server. We don't have to add any code here. We already built the "getting user by slug" API.

  • api server gets a request (from the browser or from the server) and creates a session object, as long as we configured session Express middleware to our api Express server. For session Express middleware to work, we need to configure session and mount session Express middleware on api server. You already know about Express middleware, for example, you mounted cors and parser Express middleware earlier in this book like this:


You've reached the end of the Chapter 5 preview. To continue reading, you will need to purchase the book.

We keep our book up to date with recent libraries and packages.