There are a few different processes that we need to understand when it comes to OAuth: 1) What happens when the server starts, 2) What happens when an authentication request is handled, and 3) What happens when a normal route with authorization is handled. We won't be writing code in this order, but these lists will help you understand their part in the processes.
-
Load
express-session
andpassport
libraries inserver.js
:Code
const session = require('express-session'); const passport = require('passport');
-
Mount all passport configurations to the server in
server.js
.Code
require('./config/passport');
-
Load
passport
andpassport-google-oauth
libraries inconfig/passport.js
.Code
const passport = require('passport'); const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
-
In
config/passport.js
, pass an instance ofGoogleStrategy
, along with initial values to itsconstructor
function topassport.use
(these values come from the.env
file), which will configure the connection between Google OAuth and your app. TheGoogleStrategy
instance also has a verify callback, but this callback won't actually go into an action until the user logs in, so we'll look at that later.We'll also configure the
passport.serializeUser
andpassport.deserializeUser
methods, but the guts of these will also go into action when logging in, so those will be explained later.Code
passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_SECRET, callbackURL: process.env.GOOGLE_CALLBACK }, function(accessToken, refreshToken, profile, cb) { ... } )); passport.serializeUser(function(student, done) { done(null, student.id); }); passport.deserializeUser(function(id, done) { Student.findById(id, function(err, student) { done(err, student); }); });
-
Load the appropriate routes (the authentication-related routes) in
server.js
.Code
const indexRoutes = require('./routes/index');
-
Load the
passport
library inroutes/index.js
:Code
const passport = require('passport');
-
In
routes/index.js
, the/auth/google
route will coordinate with Google's OAuth server. Secondly, the/oauth2callback
route will be called by Google after the user confirms. Finally, because thelogout()
method was added to thereq
object bypassport
, we can use that in our/logout
route.These routes are now in place to handle the relevant requests.
Code
router.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }) ); router.get('/oauth2callback', passport.authenticate('google', { successRedirect: '/students', failureRedirect: '/students' }) ); router.get('/logout', function(req, res){ req.logout(); res.redirect('/students'); });
-
In
server.js
, configure and mount thesession
middleware. Thesecret
is used to digitally sign the session cookie, making it very secure. You can change it to anything you want. Don't worry about the other two settings, they are only being set to suppress deprecation warnings.Code
app.use(session({ secret: 'SEIRocks!', resave: false, saveUninitialized: true }));
-
Now that it's been configured, mount
passport
inserver.js
.Code
app.use(passport.initialize()); app.use(passport.session());
-
Mount routes with appropriate base paths in
server.js
.Code
app.use('/', indexRoutes);
-
When a user first arrives at your app, the
session
middleware inserver.js
creates a new session in the server's memory for each user and assigns each session an id. Then, the session id is attached to the response from the server and then stored in the client's cookies (it's stored asconnect.sid
).Then, each request from the client to the server takes that session id with it in order to add, modify, or remove any data related to that user's session. For example, when the user has logged in, we'll add their id to their session.
Code
app.use(session({ secret: 'SEIRocks!', resave: false, saveUninitialized: true }));
-
In
views/students/index.ejs
, the user will click on this link with theauth/google
route to initialize the login request.Code
<a href="/auth/google"><i class="material-icons left">vpn_key</i>Login with Google</a>
-
The already-mounted
/auth/google
route inroutes/index.js
will coordinate with Google's OAuth server.Code
router.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }) );
-
Once the user confirms, the
/oauth2callback
route inroutes/index.js
will be called by Google, which will redirect the user based on a successful or failed authentication.Code
router.get('/oauth2callback', passport.authenticate('google', { successRedirect: '/students', failureRedirect: '/students' }) );
-
When the server loaded, the
constructor
properties of the newGoogleStrategy
instance were set inconfig/passport.js
. Now the verify callback goes into action to check if a student with thegoogleId
sent back from Google can be found in our db.Code
passport.use(new GoogleStrategy({ ... }, function(accessToken, refreshToken, profile, cb) { // Use the googleId to find a student in the db. Student.findOne({ googleId: profile.id }, function(err, student) { // If there was an error trying to find the student, use the callback to send the error. if (err) return cb(err); // Otherwise, if a student was found, use the callback to log them in. if (student) { if(!student.avatar) { student.avatar = profile.photos[0].value; student.save(function(err) { return cb(null, student); }) } else { return cb(null, student); } // Otherwise, if a student wasn't found and there was no error, create a new student and use the callback to log them in. } else { const newStudent = new Student({ name: profile.displayName, email: profile.emails[0].value, googleId: profile.id }); newStudent.save(function(err) { if (err) return cb(err); return cb(null, newStudent); }); } }); } ));
-
In
config/passport.js
, if the student is verified, use thedone
callback add that user's id to their session. (Since this is MongoDB, you can also usestudent._id
.)Code
passport.serializeUser(function(student, done) { done(null, student.id); });
-
In
config/passport.js
, each request goes throughdeserializeUser
. It retrieves the id from the session and uses it to find theStudent
. Then it adds that student to the request body asreq.user
, which can be used from any controller (i.e., route handler)!Code
passport.deserializeUser(function(id, done) { Student.findById(id, function(err, student) { done(err, student); }); });
-
The callback route (
/oauth2callback
) redirects the user to the/students
route and therefore to thisindex
controller incontrollers/students.js
in order to complete the request. Notice how we can set theuser
key inside therender
withreq.user
.Code
function index(req, res, next) { ... Student.find(modelQuery) .sort(sortKey).exec(function(err, students) { if (err) return next(err); res.render('students/index', { students, user: req.user, name: req.query.name, sortKey }); }); }
-
In
views/students/index.ejs
, now that we have a user, we can hide the "Login with Google" link and show them the "Log Out" link instead.Code
<% if (user) { %> <a href="/logout"><i class="material-icons left">trending_flat</i>Log Out</a> <% } else { %> <a href="/auth/google"><i class="material-icons left">vpn_key</i>Login with Google</a> <% } %>
-
In
views/students/index.ejs
, we have an "Add Fact" form. So, after entering content into the form, submit it with the "Add Fact" button to initialize a post request to/facts
.Code
<form action="/facts" method="POST"> <input type="text" name="text" class="white-text"> <button type="submit" class="btn white-text">Add Fact</button> </form>
-
In
config/passport.js
, as soon as the request comes in, the id is retrieved from the session and used to find theStudent
. Then it adds that student to the request body asreq.user
, which can be used from any controller (i.e., route handler)!Code
passport.deserializeUser(function(id, done) { Student.findById(id, function(err, student) { done(err, student); }); });
-
First, the request is directed by the already-mounted controller in
server.js
:Code
app.use('/', studentsRoutes);
Then the already-mounted routes in
routes/students.js
will direct the request to theaddFact
controller. But, before it can get there, it must go through theisLoggedIn
authorization middleware.Code
router.post('/facts', isLoggedIn, studentsCtrl.addFact);
-
In
routes/students.js
, the second argument in the route above calls this function, which will check if the request is authenticated. If it is, allow the request through to the controller. Otherwise, force the user to log in by triggering the/auth/google
route. This is how we protect our routes!Code
function isLoggedIn(req, res, next) { if ( req.isAuthenticated() ) return next(); res.redirect('/auth/google'); }
-
In
controllers/students.js
, since thedeserializeUser
method already did the Mongoose query of finding theStudent
and then adding that student to the request object asreq.user
, we can push the fact to that student'sfacts
array, and then save it to the db. The controller then redirects the logic to the/students
index route.Code
function addFact(req, res, next) { req.user.facts.push(req.body); req.user.save(function(err) { res.redirect('/students'); }); }
-
Because the
addFact
controller triggered a new request to the/students
route, we're back at thedeserializeUser
method inconfig/passport.js
again because all requests go through it! After it finds theStudent
again and adds it toreq.user
again, we're off to the route.Code
passport.deserializeUser(function(id, done) { Student.findById(id, function(err, student) { done(err, student); }); });
-
The redirected request hits the
server.js
and gets forwarded toindexRoutes
.Code
app.use('/', indexRoutes);
Then, inside this router, because there is no
isLoggedIn
authorization middleware here, the route moves the request on to theindex
controller.Code
router.get('/students', studentsCtrl.index);
-
The
index
controller incontrollers/index.js
now gets all thestudents
(along with the newly-added fact) and renders them with theviews/students/index.ejs
view. We're done!Code
function index(req, res, next) { let modelQuery = req.query.name ? {name: new RegExp(req.query.name, 'i')} : {}; let sortKey = req.query.sort || 'name'; Student.find(modelQuery) .sort(sortKey).exec(function(err, students) { if (err) return next(err); res.render('students/index', { students, user: req.user, name: req.query.name, sortKey }); }); }