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
:
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 aSession
MongoDB document to our database. Then our server creates a uniquecookie
object that hasname
andvalue
. The value forvalue
is generated from the session's id. Our server sends a response to the browser that containscookie
, andcookie
gets saved to a user's browser.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 decodesvalue
of the receivedcookie
. Our server finds a matchingSession
MongoDB document by the session id in our database. Inside thissession
object, there is a user id. Our server finds a matchingUser
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.
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 ofsendRequestAndGetResponse
method that all API methods use in our web application. Openbook/5-begin/app/lib/api/sendRequestAndGetResponse.ts
file and find line with:Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }),
Setting
credentials
value toinclude
makes request to contain cookies even for cross-origin calls (and calls betweenapp
andapi
projects is cross-origin):
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentialsFor server-side rendered page, there is request from the browser to
app
server and then there is request between two servers,app
server andapi
server. Original request for page comes from the browser to theapp
server first. Soapp
server hascookie
because ofcredentials: 'include'
. But how to ensure that request fromapp
server toapi
server includescookie
as well? Actually, it already does! Because of the code we wrote earlier but promised to explained later. Openbook/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 containcookie
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 theapp
server), which isrequest.headers.cookie
. Then we save thiscookie
value toheaders
by assigning value toheaders.cookie
. Then we use theseheaders
withcookie
for the second request (from theapp
server to theapi
server), thus ensuring thatapi
server hascookie
value. We still did not add any code that translatescookie
value into matchingsession
anduser
but we will do so below.
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 theAPI
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 asession
object, as long as we configured session Express middleware to ourapi
Express server. For session Express middleware to work, we need to configure session and mount session Express middleware onapi
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.