OAuth Authentication with Express & Passport

Learning Objectivesβ
Students will be able to: |
---|
Explain the difference between Authentication & Authorization |
Identify the advantages OAuth provides for users and businesses |
Explain what happens when a user clicks "Login with [OAuth Provider]" |
Add OAuth authentication to an Express app using PassportJS |
Use Middleware & PassportJS to implement authorization |
Road Mapβ
- Setup
- Intro to Authentication
- Why OAuth?
- What is OAuth?
- Preview the Completed
mongoose-movies
App - Today's OAuth Game Plan
- Step 1: Register our App with Google's OAuth Server
- Step 2: Define the
User
model - Step 3: Discuss PassportJS
- Step 4: Install & Configure Session middleware
- Step 5: Install PassportJS
- Step 6: Create a Passport config module
- Step 7: Install a Passport Strategy for OAuth
- Step 8: Configure Passport
- Step 9: Define routes for authentication
- Step 10: Add Login/Logout UI
- Code the User Stories
- Implement Authorization
- Challenge Exercise (optional)
Videosβ
1. Setupβ
This lesson will finalize mongoose-movies!
The starter code for this lesson begins where the Mongoose Queries Lab left off (which also seeded the database with some Movies and performers).
You only have to sync if your code is complete or is not working.
-
Move into the
mongoose-movies
project we started in the Intro to Mongoose lesson:cd ~/code/mongoose-movies
-
Again, you only have to sync if your code is complete or is not working:
git reset --hard origin/sync-15-finish-seeding-query-lab
-
Open the project's folder in VS Code:
code .
-
Open an integrated terminal session:
control + backtick
-
Start the Express server:
nodemon
-
Browse to
localhost:3000
Remove Any Existing Reviewsβ
The upcoming user-centric change that's going to be made to the reviewSchema
will prevent being able to add new reviews to any movie that currently has existing reviews (because they won't conform to the new schema).
Therefore, we need to clear out the reviews
array exactly how/why we needed to clear out the cast
array during the referencing lesson.
- Open a new Node REPL:
node
- Load crud-helper.js:
.load crud-helper.js
- Run the following command to clear out all reviews:
Movie.updateMany({}, { reviews: [] }).then(console.log);
- Exit the REPL:
.exit
Now back to our regularly scheduled programming...
2. Intro to Authenticationβ
Why We Need Authenticationβ
An application's functionality typically revolves around a particular user.
For example, when we use online banking, or more importantly, save songs to our Spotify playlists, the application has to know who we are - and this is where authentication comes in.
What is Authentication?β
Authentication is what enables an application to know the identity of the person using it.
In SEI, we're going to learn 3 types of authentication:
- Unit 2: Logging in via a third-party provider (OAuth)
- Unit 3: Session-based username/password login
- Unit 4: Token-based username/password login
Authentication vs. Authorizationβ
Authentication and authorization are not the same thing...
Authentication verifies a user's identity.
Authorization determines what functionality a given user can access. For example:
- Features a logged in (authenticated) user has vs. an anonymous visitor
- Features an admin user has vs. some other user role
- Only the user that added a given comment can delete that comment
3. Why OAuth?β
Consider applications where we have to sign up and log in using a username and a password...
β What pitfalls of username/password authentication can you think of from a user's perspective?
-
Creating multiple logins requires you to remember and manage all of those login credentials.
-
You will often use the same credentials across multiple sites, so if there's a security breach at one of the sites where you are a member, the hackers know that users often use the same credentials across all of their sites - oh snap!
-
You are tempted to use simple/weak passwords so that you can remember all of them.
β What might be the pitfalls of username/password authentication from a business that owns the web app point of view?
-
Managing users' credentials requires carefully crafted security code written by highly-paid developers.
-
Users (customers) are annoyed by having to create dedicated accounts, especially for entertainment or personal interest type websites.
-
Managing credentials makes your business a target for hackers (internal and external) and that brings with it liability.
The bottom-line is that users prefer to use OAuth instead of creating another set of credentials to use your site.
When users are your customers, you want to make them as happy as possible!
OAuth is hot, so let's use it!
4. What is OAuth?β
A Bit of OAuth Vocabularyβ
-
OAuth 2.0: The current OAuth standard. Version 1.0 is obsolete and should not be used.
-
OAuth provider: A service company such as Google that makes its OAuth authentication service available to third-party applications.
-
client application: Our web application! Remember, this is from an OAuth provider's perspective.
-
owner: A user of a service such as Facebook, Google, Dropbox, etc.
-
resources: An owner's information on a service that may be exposed to client applications. For example, a user of Dropbox may allow access to their files.
-
access token: An temporary key that provides access to an owner's resources.
-
scope: Determines what resources and rights (read-only, update, etc) a particular token has.
What is it?β
OAuth is an open standard that provides client applications access to resources of a service such as Google with the permission of the resources' owner.
There are numerous OAuth Providers including:
- GitHub
- Many more...
OAuth 2's Flow & Scopeβ

The above image, taken from this excellent article, diagrams the flow of logging in using OAuth 2.0.
The ultimate goal is for the client application (our web app) to obtain an access token from an OAuth provider that allows the app to access the user's resources from that provider's API's.
OAuth is token based. A token is a generated string of characters.
Each token has a scope that determines what resources an app can access for that user.
For mongoose-movies, as is the case with many applications, we are only interested in using OAuth for identifying who the user is by accessing their basic profile info (name, email & avatar).
If an application needs to access more than a user's basic profile, the scope would need to be expanded as dictated by the specific provider's documentation on how to access additional resources.
Yes, OAuth is complex. But not to worry, we don't have to know all of the nitty gritty details in order to take advantage of it because we will be using PassportJS middleware that will handle most of the "OAuth dance" for us.
β OAuth Review Questionsβ
(1) True or False: If your site allows users to authenticate via OAuth, you should ensure they create a "strong" password.
False - Trick question because users won't be providing a password to our site if we use OAuth π
(2) To an OAuth provider, such as Google, what is the client application?
Our web application/server
5. Preview the Completed mongoose-movies
Appβ
The deployed mongoose-movies includes OAuth authentication & authorization.
Here are some of the differences in the final code vs. where the app is now:
- The ability to log in and out with Google OAuth has been implemented.
- The navbar dynamically renders its links based upon whether a user is logged in or not.
- A
User
model has been added and when a user logs in for the first time the user is added to the database. - The
Movie --< Review
relationship has been updated to be "user-centric", i.e.,Movie --< Review >-- User
6. Today's OAuth Game Planβ
Ready for some OAuth? Here's today's game plan:
- Step 1: Register our App with Google's OAuth Server
- Step 2: Define the
User
model - Step 3: Discuss PassportJS
- Step 4: Install & Configure Session middleware
- Step 5: Install PassportJS
- Step 6: Create a Passport config module
- Step 7: Install a Passport Strategy for OAuth
- Step 8: Configure Passport
- Step 9: Define routes for authentication
- Step 10: Add Login/Logout UI
Yep, lots of fun ahead...
Step 1 - Register our Appβ
Every OAuth provider requires that our web app be registered with it.
When we do so, we obtain a Client ID and a Client Secret that identifies our application to the OAuth provider.
For this lesson, we are going to use Google's OAuth provider.
Time to register our app...
Step 1.1 - Google Developers Consoleβ
- You must be logged into the console for Google's Cloud Platform Console. Here's what you'll see once logged in to the console for the first time and consent to the Terms of Service:

Step 1.2 - Create a Projectβ
- Click Select a project:

- Click NEW PROJECT:

- Update the Project name to something like
mongoose-movies
, then click CREATE:

Note: The project name must be globally unique, so Google will append and additional id to the name you provide.
- It might take a bit to create the project. When done, click SELECT PROJECT:

Step 1.3 - Enable the People APIβ
-
So that we can access the user's basic profile, we'll need to enable the People API.
-
Click Go to APIs overview:

- Click + ENABLE APIS AND SERVICES:

- Search for people and click on Google People API when it is visible:

- Click ENABLE:

Step 1.4 - Obtain Credentials for Appβ
- Now we need to create credentials for the app. Click CREATE CREDENTIALS:

- Select User data then click NEXT:

- Under OAuth Consent Screen provide a friendly App name, select the User support email, leave App Logo blank, type in a developer Email address then click SAVE AND CONTINUE:

- Ignore the Scopes section by clicking SAVE AND CONTINUE (you may have to scroll down a bit):

- In the dropdown, select Web application, then click the + ADD URI in the Authorized redirect URIs:

- Copy/paste
http://localhost:3000/oauth2callback
into the URIs 1 input then click CREATE:

π You will have to add an additional entry in the Authorized redirect URIs once you have deployed your application to Heroku - something like
https://<your app name>.herokuapp.com/oauth2callback
.
- We now need to change the publishing status so that anyone can log in. Click the OAuth consent screen page link:

- Click the PUBLISH APP button:

- Click the CONFIRM:

- Awesome. Now let's go view our credentials by clicking Credentials in the side menu:

- Click the Web client 1 link:

Step 1.5 - Add the Client ID and Client Secret to .env
β
Strangely, we either have to make our browser window very narrow or very large in order to see the Client ID and the Client secret:

Now we can add the credentials, along with that callback URI we provided, to the .env
file so that it looks something like this:
DATABASE_URL=mongodb+srv://<username>:<password>@cluster0-oxnsb.azure.mongodb.net/mongoose-movies?retryWrites=true&w=majority
GOOGLE_CLIENT_ID=245025414218-2r7f4bvh3t88s3shh6hhagrki0f6op8t.apps.googleusercontent.com
GOOGLE_SECRET=Yn9T_2BKzxr5zgprzKDGI5j3
GOOGLE_CALLBACK=http://localhost:3000/oauth2callback
π Registering a project with other OAuth providers will be similar, but unique to that provider.
Congrats on Registering your App with Google's OAuth Service!
Step 2 - Define the User
Modelβ
Let's start by defining a minimal User
model:
-
Create the Node module:
touch models/user.js
-
Define the minimal schema, compile the schema into a
User
model and export it:models/User.jsconst mongoose = require("mongoose");
const Schema = mongoose.Schema;
const userSchema = new Schema(
{
name: String,
},
{
timestamps: true,
}
);
module.exports = mongoose.model("User", userSchema);
We will add additional properties once we "discover" what user info Google provides us with.
Step 3 - Passport Discussionβ
Implementing OAuth is complex. There are redirects going on everywhere, access tokens that only last for a short time, refresh tokens used to obtain a fresh access token, etc.
As usual, we will stand on the shoulders of giants that have done much of the heavy lifting for us - enter PassportJS.
Passport is by far the most popular authentication framework out there for Express apps.
Passport's website states that it provides Simple, unobtrusive authentication for Node.js.
Basically this means that it handles much of the mundane tasks related to authentication for us, but leaves the details up to us, for example, not forcing us to configure our user model a certain way.
There are numerous types of authentication, if Passport itself was designed to do them all, it would be ginormous!
Instead, Passport uses Strategies designed to handle a given type of authentication. Think of them as plug-ins for Passport.
Each Express app with Passport can use one or more of these strategies.
Passport's site currently shows over 500 strategies available.
OAuth2, although a standard, can be implemented slightly differently by OAuth providers such as Facebook and Google.
As such, there are strategies available for each flavor of OAuth provider.
For this lesson, we will be using the passport-google-oauth strategy.
Passport is just middleware designed to authenticate requests.
IMPORTANT INFO BELOW
When a request is sent from an authenticated user, Passport's middleware will automatically add a user
property to the req
object.
π req.user
will be the logged in user's Mongoose documentβοΈ
If a user is not logged in, req.user
will be undefined
.
You will then be able to access the req.user
document in all of the controller actions - so, DO NOT write code to retrieve the user document from the DB because req.user
is already the document!
Step 4 - Session Middlewareβ
Before we install Passport and a strategy, we need to install the express-session
middleware.
Sessions, are a server-side way of remembering a user's browser session.
Sessions remembers the browser session by setting a cookie that contains a session id. No other data is stored in the cookie, just the id of the session.
On the server-side, your application can optionally store data pertaining to the user's session (req.session
).
Passport will use the session, which is an in-memory data-store by default, to store a nugget of information that will allow us to lookup the user in the database.
FYI, since sessions are maintained in memory by default, if the server restarts, session data will be lost. You will see this happen when nodemon restarts the server and you are no longer logged in.
Step 4.1 - Installing Session Middlewareβ
Let's install the module:
npm install express-session
Next, require it below the logger
:
var logger = require("morgan");
// new code below
var session = require("express-session");
Step 4.2 - Add a SECRET to .env
β
When session middleware is configured, it will require a "secret" string that it uses to digitally sign the session cookie. Let's add the secret to the .env
:
...
GOOGLE_CALLBACK=http://localhost:3000/oauth2callback
SECRET=SEIRocks
Step 4.3 - Configure and Mount Session Middlewareβ
Now, we can configure and mount the session middleware below the express.static
middleware:
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(
session({
secret: process.env.SECRET,
resave: false,
saveUninitialized: true,
})
);
The resave
and saveUninitialized
settings, they are being set to suppress deprecation warnings.
Step 4.4 - Verifying Session Middlewareβ
Make sure your server is running with nodemon.
Browse to the app at localhost:3000
.
Open the Application tab in DevTools, then expand Cookies in the menu on the left.
A cookie named connect.sid
confirms that the session middleware is doing its job.
Congrats, the session middleware is now in place!
Time for a few questions...
β Review Questions (1 min)β
(1) Before a web app can use an OAuth provider, it must first r__________with it to obtain a client____ and a client secret.
...it must first register with it to obtain a client ID and a client secret.
(2) Passport uses s__________ designed to handle specific types of authentication.
strategies
(3) If there is an authenticated user, the request (req
) object will have what property attached to it by Passport?
req
) object will have what property attached to it by Passport?req.user
π Do you need to sync your code?
git reset --hard origin/sync-16-sessions
Step 5 - Install Passportβ
The Passport middleware is easy to install, but challenging to configure correctly.
First the easy part:
npm i passport
Require it as usual below express-session
:
var session = require("express-session");
// new code below
const passport = require("passport");
Step 5.1 - Mount Passportβ
With Passport required, we need to mount it. Be sure to mount it after the session middleware and always before any of the routes that would need access to the current user:
// app.use(session({... code above
app.use(passport.initialize());
app.use(passport.session());
The way passport
middleware is being mounted is straight from the docs.
Step 6 - Create a Passport Config Moduleβ
Because it takes a significant amount of code to configure Passport, we will create a separate module so that we don't pollute server.js.
Let's create the file:
touch config/passport.js
In case you're wondering, although the module is named the same as the passport
module we've already required, it won't cause a problem because a module's full path uniquely identifies it to Node.
Step 6.1 - Passport Module's Exports Codeβ
Our config/passport
module is not middleware.
Its code will basically configure Passport and be done with it. We're not going to export anything either.
Requiring below our database is as good of a place as any in server.js:
require("./config/database");
// new code below
require("./config/passport");
Step 6.2 - Require Passportβ
In the config/passport.js module we will certainly need access to the passport
module:
const passport = require("passport");
Now on to the strategy...
Step 7 - Install the OAuth Strategyβ
Time to install the strategy that will implement Google's flavor of OAuth:
npm i passport-google-oauth
This module implements Google's OAuth 2.0 and 1.0 API.
Note that OAuth 1.0 does still exist here and there, but it's obsolete.
Step 7.1 - Require the OAuth Strategyβ
Now let's require the passport-google-oauth
module below that of passport
in config/passport.js:
const passport = require("passport");
// new code below
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy;
Note that the variable is named using upper-camel-case.
β What does this naming convention typically hint at?
The it's a class
Let's make sure there's no errors before moving on to the fun stuff!
Step 8 - Configuring Passportβ
To configure Passport we will:
-
Call the
passport.use()
method to plug-in an instance of the OAuth strategy and provide a verify callback function that will be called whenever a user has logged in using OAuth. -
Call the
passport.serializeUser()
method to provide a callback that Passport will call after the verify callback. -
Call the
passport.deserializeUser()
method to provide a callback that Passport will call for every request when a user is logged in.
Step 8.1 passport.use()
β
Time to call the passport.use()
method to plug-in an instance of the OAuth strategy and provide a verify callback function that will be called whenever a user logs in with OAuth.
In config/passport.js:
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy;
// new code below
passport.use(
new GoogleStrategy(
// Configuration object
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK,
},
// The verify callback function
// Let's use async/await!
async function (accessToken, refreshToken, profile, cb) {
// A user has logged in with OAuth...
}
)
);
Note the settings from the .env
file being passed to the GoogleStrategy
constructor function.
Next we have to code the verify callback function...
Step 8.2 - The Verify Callbackβ
The verify callback will be called by Passport when a user has logged in with OAuth.
It's called a verify callback because with most other strategies we would have to verify the credentials, but with OAuth, well, there are no credentials!
In this callback we must:
-
Fetch the user from the database and provide them back to Passport by calling the
cb()
callback function, or... -
If the user does not exist, we have a new user! We will add them to the database and pass along this new user in the
cb()
callback function.
But wait, how can we tell what user to lookup?
Looking at the callback's signature:
function(accessToken, refreshToken, profile, cb) {
We can see that we are being provided the user's profile
by Google.
If we were to inspect this profile
object, we'd find the following useful properties:
id
: The user's Google Id that uniquely identifies each Google account.displayName
: The user's full name.emails
: An array of email objects associated with the account.photos
: An array of avatar image objects associated with the account.
Let's add these field to our User
model's schema to hold it...
Step 8.3 - Modify the User
Modelβ
Let's add a property for googleId
to our userSchema
inside models/user.js
file:
const userSchema = new Schema(
{
name: String,
googleId: {
type: String,
required: true,
},
email: String,
avatar: String,
},
{
timestamps: true,
}
);
Cool, now when we get a new user via OAuth, we can use the Google profile
object's info to create our new user!
Step 8.4 - Verify Callback Codeβ
Now we need to code the callback!
We're going to need access to our User
model:
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy;
// new code below
const User = require("../models/user");
Cool, here comes the code for the entire passport.use
method. We'll review it as we type it in...
passport.use(
new GoogleStrategy(
// Configuration object
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK,
},
// The verify callback function...
// Marking a function as an async function allows us
// to consume promises using the await keyword
async function (accessToken, refreshToken, profile, cb) {
// When using async/await we use a
// try/catch block to handle an error
try {
// A user has logged in with OAuth...
let user = await User.findOne({ googleId: profile.id });
// Existing user found, so provide it to passport
if (user) return cb(null, user);
// We have a new user via OAuth!
user = await User.create({
name: profile.displayName,
googleId: profile.id,
email: profile.emails[0].value,
avatar: profile.photos[0].value,
});
return cb(null, user);
} catch (err) {
return cb(err);
}
}
)
);
Step 8.5 serializeUser()
& deserializeUser()
Methodsβ
With the passport.use()
method done, we now need to code two more methods inside of config/passport
module.
Step 8.6 serializeUser()
Methodβ
After the verify callback function returns the user document, passport calls the passport.serializeUser()
method's callback passing that same user document as an argument.
It is the job of that callback function to return the nugget of data that passport is going to add to the session used to track the user:
passport.serializeUser(function (user, cb) {
cb(null, user._id);
});
By using the MongoDB _id
of the user document we can easily retrieve the user doc within the deserializeUser()
method's callback...
Step 8.7 deserializeUser()
Methodβ
The passport.deserializeUser()
method's callback function is called every time a request comes in from an existing logged in user.
The callback needs to return what we want passport to assign to the req.user
object.
Code it below the passport.serializeUser()
method:
// Add to bottom of config/passport.js
passport.deserializeUser(async function (userId, cb) {
// It's nice to be able to use await in-line!
cb(null, await User.findById(userId));
});
Let's do another error check.
π Do you need to sync your code?
git reset --hard origin/sync-17-passport
Step 9 - Define Routes for Authenticationβ
We're going to need three auth related routes:
-
A route to handle the request sent when the user clicks Login with Google
-
The
/oauth2callback
route that we told Google to call after the user confirms or denies the OAuth login. -
Lastly, we will need a route for the user to logout.
Step 9.1 Coding the Routesβ
We're going to code these three new auth related routes in the existing routes/index
module.
These new routes will need to access the passport
module, so let's require it in routes/index.js:
var router = require("express").Router();
// new code below
const passport = require("passport");
Step 9.2 Login Routeβ
In routes/index.js, let's add the login route below the root route:
// Google OAuth login route
router.get(
"/auth/google",
passport.authenticate(
// Which passport strategy is being used?
"google",
{
// Requesting the user's profile and email
scope: ["profile", "email"],
// Optionally force pick account every time
// prompt: "select_account"
}
)
);
The passport.authenticate()
method will return a middleware function that does the coordinating with Google's OAuth server.
The user will be presented the consent screen if they haven't previously consented.
Step 9.3 Google Callback Routeβ
Below the login route we just added, let's add the "callback" route that Google will call after the user confirms:
// Google OAuth callback route
router.get(
"/oauth2callback",
passport.authenticate("google", {
successRedirect: "/movies",
failureRedirect: "/movies",
})
);
Note that we can specify the redirects for a successful and unsuccessful login. For this app, we will redirect to our main /movies
route in both cases.
Step 9.4 Logout Routeβ
The last route to add is the route that will logout the user:
// OAuth logout route
router.get("/logout", function (req, res) {
req.logout(function () {
res.redirect("/movies");
});
});
The logout()
method was automatically added to the req
object by Passport!
Good time to do another error check.
Step 10 - Add Login/Logout UIβ
We want the nav bar in views/partials/header.ejs to update dynamically depending upon whether there's an authenticated user or not:

versus

Step 10.1 Pass req.user
to All Views via res.locals
& Middlewareβ
Instead of having to pass req.user
every time we render a template, let's take advantage of Express' res.locals object and a tidy custom middleware function:
In server.js:
app.use(passport.session());
// Add this middleware BELOW passport middleware
app.use(function (req, res, next) {
res.locals.user = req.user;
next();
});
Now the logged in user is in a user
variable that's available inside all EJS templates!
If nobody is logged in, user
will be undefined
.
Step 10.2 Add the Login / Logout UI Logicβ
We're going to use an if
/else
block in the EJS to dynamically render the nav links depending upon whether there is a logged in user or not.
Here's the updated <nav>
element in partials/header.ejs:
<nav>
<img src="/images/camera.svg">
<a href="/movies" <%- title === 'All Movies' ? 'class="active"' : '' %>>ALL MOVIES</a>
<% if (user) { %>
<a href="/performers/new" <%- title === 'Add Performer' ? 'class="active"' : '' %>>
ADD PERFORMER</a>
<a href="/movies/new" <%- title === 'Add Movie' ? 'class="active"' : '' %>>ADD MOVIE</a>
<a href="/logout">LOG OUT</a>
<% } else { %>
<a href="/auth/google" class="login">LOG IN <img src="https://i.imgur.com/FHjYyi0.png"></a>
<% } %>
</nav>
There's a class="login"
in use - add the following to the bottom of public/stylesheets/style.css:
.login {
display: flex;
justify-content: center;
align-items: center;
height: 34px;
}
.login img {
height: 30px;
}
Step 10.3 Try Logging In/Outβ
We've finally got to the point where you can test out our app's authentication!
May the force be with us!
π Do you need to sync your code?
git reset --hard origin/sync-18-login-out
7. Code the User Storiesβ
Our first user story reads:
AAU, I want to add reviews for a movie when I'm logged in.
We already have a <form>
for creating reviews on the views/movies/show.ejs template.
However, we only want this UI to show when there's a logged in user.
π You Do - Hide/Show Review <form>
(2 minutes)β
Using an <% if %>
EJS block in views/movies/show.ejs, render the <form>
for adding a review ONLY if there's a logged in user.
Solution
<!-- Add the line below -->
<% if (user) { %>
<form id="add-review-form" method="POST" action="/movies/<%= movie._id %>/reviews">
<label>Review:</label>
<textarea name="content"></textarea>
<label>Rating:</label>
<select name="rating">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<input type="submit" value="Add Review">
</form>
<!-- Add the line below -->
<% } %>
Update the reviewSchema
β
We're going to make three changes to the reviewSchema
to make it user-centric:
- Add a
user
property that references the user'sObjectId
that created the review. - To avoid having to populate review sub-docs just to display the user's name with them, we're also going to add a
userName
property. - Similar to
userName
, let's also add auserAvatar
property.
Update reviewSchema
in models/movie.js:
const reviewSchema = new Schema(
{
content: {
type: String,
required: true,
},
rating: {
type: Number,
min: 1,
max: 5,
default: 5,
},
// Don't forget to add the comma above then
// add the 3 new properties below
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
},
userName: String,
userAvatar: String,
},
{
timestamps: true,
}
);
Note that
required: true
has been added to prevent "orphan" reviews. You'll want to do the same on other user-centric schemas as well.
Update the create
action in the Reviews controllerβ
Much of an application's CRUD data operations pertain to the logged in user.
The Guide to User-Centric CRUD using Express & Mongoose will prove very helpful when coding your projects.
We've already added the necessary properties to the reviewSchmea
, now let's update the create
action in the controllers/reviews.js controller:
async function create(req, res) {
const movie = await Movie.findById(req.params.id);
// Add the user-centric info to req.body (the new review)
req.body.user = req.user._id;
req.body.userName = req.user.name;
req.body.userAvatar = req.user.avatar;
// We can push (or unshift) subdocs into Mongoose arrays
movie.reviews.push(req.body);
try {
// Save any changes made to the movie doc
await movie.save();
} catch (err) {
console.log(err);
}
res.redirect(`/movies/${movie._id}`);
}
Remember:
req.user
is the logged in user's Mongoose document!
Update How Reviews are Being Displayedβ
Here's the updated views/movies/show.ejs that displays the user's name and avatar with each review:
<table>
<thead>
<tr>
<th>User</th>
<th>Date</th>
<th>Review</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
<% let total = 0 %>
<% movie.reviews.forEach(function(r) { %>
<% total += r.rating %>
<tr>
<td class="review-user"><img alt="avatar" src="<%= r.userAvatar %>" referrerpolicy="no-referrer" ><%= r.userName %></td>
<td><%= r.createdAt.toLocaleDateString() %></td>
<td><%= r.content %></td>
<td><%= r.rating %></td>
</tr>
<% }); %>
<tr>
<td colspan="3"></td>
<td><strong><%= (total / movie.reviews.length).toFixed(1) %></strong></td>
</tr>
</tbody>
</table>
Note: The
referrerpolicy="no-referrer"
attribute added to the<img>
cures Google's finickiness handling the browser's request for the avatar image.
Sprinkle in the the following CSS at the end of public/stylesheets/style.css:
.review-user {
display: flex;
justify-content: center;
align-items: center;
}
.review-user img {
border-radius: 50%;
height: 40px;
}
Test the First User Story
That should take care of our first user story - try it out!

Yes, the UX is not that great because of the full-page refresh, but we'll address that when we develop single-page apps with React.
π Do you need to sync your code?
git reset --hard origin/sync-19-first-user-story
Code the Next User Storyβ
Our next user story reads:
AAU, I want to be able to delete a review that I previously created.
We want to ensure that only the user that created a review can delete it.
As usual, we'll follow the 5 steps to add delete functionality...
Step 1 - Determine the Proper Routeβ
β What is the proper route to delete a review?
DELETE /reviews/:id
Although Reviews are a nested resource, the server doesn't need to know the id of the movie that the review is embedded within; thus the "shallow route".
Step 2 - Display the UI to Send the HTTP Requestβ
Nothing fancy...
Again, in views/movies/show.ejs:
...
<th>Rating</th>
<!-- Add this placeholder header cell -->
<th></th>
...
<td><%= r.rating %></td>
<!-- Add this td -->
<td>
<% if (user?._id.equals(r.user)) { %>
<form action="/reviews/<%= r._id %>?_method=DELETE" method="POST">
<button type="submit">X</button>
</form>
<% } %>
</td>
The user?._id...
is using JS's fresh optional chaining operator (?
) to avoid causing an error if user
is not a an object (user not logged in).
The _id.equals()
method is used to compare _id
s - this is necessary because they are objects.
Lastly, looks like we need to install and mount method-override
middleware:
npm i method-override
Require it:
var passport = require("passport");
// Add the line below
var methodOverride = require("method-override");
Now let's mount it:
app.use(cookieParser());
// Add the line below
app.use(methodOverride("_method"));
Step 3 - Define the Routeβ
π You Do - Define the delete
Route (1 min)
Add the new route for deleting a review to routes/reviews.js router.
Solution
// DELETE /reviews
router.delete("/reviews/:id", reviewsCtrl.delete);
Step 4 - Code the delete
Actionβ
It's not enough to just hide the delete button when the review does not belong to the logged in user.
We should validate ownership in the controller action as well. Again, using the Guide to User-Centric CRUD using Express & Mongoose...
module.exports = {
create,
// Add this export
delete: deleteReview,
};
async function deleteReview(req, res) {
// Note the cool "dot" syntax to query on the property of a subdoc
const movie = await Movie.findOne({
"reviews._id": req.params.id,
"reviews.user": req.user._id,
});
// Rogue user!
if (!movie) return res.redirect("/movies");
// Remove the review using the remove method available on Mongoose arrays
movie.reviews.remove(req.params.id);
// Save the updated movie doc
await movie.save();
// Redirect back to the movie's show view
res.redirect(`/movies/${movie._id}`);
}
π If you provide a non-
null
argument when callingnext()
, an error propagates through the Express framework which results in the built-in views/error.ejs error page displaying.
Step 5 - Render or Redirectβ
Already done in the above code!
Test it out!
Cool, just one step left!
8. Implement Authorizationβ
β What is authorization?
Authorization determines what functionality a given user can access. For example, we don't want unauthenticated users to be able to delete reviews.
We've already coded some client-side authorization by:
- Conditionally displaying the
<form>
used to add reviews. - Showing the delete button for only the reviews created by the logged in user.
We also performed a bit of server-side authorization by ensuring that the logged in user was the owner of the review being deleted.
In this step, we will see how we can easily protect the routes that require a user to be logged in, e.g., adding a movie or review.
Passport adds a nice method to the req
object, req.isAuthenticated()
that returns true
or false
depending upon whether there's a logged in user or not.
We're going to write our own little middleware function to take advantage of req.isAuthenticated()
to perform some authorization.
Code the Authorization Middlewareβ
Since we want to protect routes defined in multiple modules, we'll want to stay DRY and code the middleware function in its own module.
touch config/ensureLoggedIn.js
We only want to export a single thing, a middleware function. Thus, the best approach is to assign the function to module.exports
as follows:
// Middleware for routes that require a logged in user
module.exports = function (req, res, next) {
// Pass the req/res to the next middleware/route handler
if (req.isAuthenticated()) return next();
// Redirect to login if the user is not already logged in
res.redirect("/auth/google");
};
Our custom ensureLoggedIn
middleware function, like all middleware, will either call next()
, or respond to the request.
Using Authorization Middlewareβ
Express's middleware and routing is extremely flexible and powerful.
We can add multiple middleware functions before a route's final middleware function!
Let's modify routes/movies.js to protect routes that require a logged in user using our new ensureLoggedIn middleware:
// routes/movies.js
const moviesCtrl = require("../controllers/movies");
// Require the auth middleware
const ensureLoggedIn = require("../config/ensureLoggedIn");
router.get("/", moviesCtrl.index);
// Use ensureLoggedIn middleware to protect routes
router.get("/new", ensureLoggedIn, moviesCtrl.new);
router.get("/:id", moviesCtrl.show);
router.post("/", ensureLoggedIn, moviesCtrl.create);
Note the inserted ensureLoggedIn
middleware function!
If the ensureLoggedIn
middleware calls next()
, then the next middleware, e.g., moviesCtrl.new
will be called.
Test it out by logging out, and manually attempting to browse to http://localhost:3000/movies/new
. Very cool!
π You Do - Protect More Routes (3 mins)β
- Protect the appropriate routes in:
- routes/reviews.js
- routes/performers.js
Solution
routes/reviews.js
router.post("/movies/:id/reviews", ensureLoggedIn, reviewsCtrl.create);
router.delete("/reviews/:id", ensureLoggedIn, reviewsCtrl.delete);
routes/performers.js
router.get("/performers/new", ensureLoggedIn, performersCtrl.new);
router.post("/performers", ensureLoggedIn, performersCtrl.create);
router.post(
"/movies/:movieId/performers",
ensureLoggedIn,
performersCtrl.addToCast
);
π Congrats on implementing OAuth authentication and authorizationβ
Now you're ready to start your project and implement OAuth authentication before any other CRUD functionality.
π Do you need to sync your code?
git reset --hard origin/sync-20-finish
9. πͺ Challenge Exercise (optional)β
As an optional challenge exercise, use the Guide to User-Centric CRUD using Express & Mongoose to help you implement the following user story:
AAU, I want to be able to update a review that I previously created.