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-sessionandpassportlibraries 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
passportandpassport-google-oauthlibraries 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 itsconstructorfunction topassport.use(these values come from the.envfile), which will configure the connection between Google OAuth and your app. TheGoogleStrategyinstance 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.serializeUserandpassport.deserializeUsermethods, 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
passportlibrary inroutes/index.js:Code
const passport = require('passport'); -
In
routes/index.js, the/auth/googleroute will coordinate with Google's OAuth server. Secondly, the/oauth2callbackroute will be called by Google after the user confirms. Finally, because thelogout()method was added to thereqobject bypassport, we can use that in our/logoutroute.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 thesessionmiddleware. Thesecretis 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
passportinserver.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
sessionmiddleware inserver.jscreates 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/googleroute 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/googleroute inroutes/index.jswill coordinate with Google's OAuth server.Code
router.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }) );
-
Once the user confirms, the
/oauth2callbackroute inroutes/index.jswill 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
constructorproperties of the newGoogleStrategyinstance were set inconfig/passport.js. Now the verify callback goes into action to check if a student with thegoogleIdsent 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 thedonecallback 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/studentsroute and therefore to thisindexcontroller incontrollers/students.jsin order to complete the request. Notice how we can set theuserkey inside therenderwithreq.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.jswill direct the request to theaddFactcontroller. But, before it can get there, it must go through theisLoggedInauthorization 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/googleroute. 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 thedeserializeUsermethod already did the Mongoose query of finding theStudentand then adding that student to the request object asreq.user, we can push the fact to that student'sfactsarray, and then save it to the db. The controller then redirects the logic to the/studentsindex route.Code
function addFact(req, res, next) { req.user.facts.push(req.body); req.user.save(function(err) { res.redirect('/students'); }); }
-
Because the
addFactcontroller triggered a new request to the/studentsroute, we're back at thedeserializeUsermethod inconfig/passport.jsagain because all requests go through it! After it finds theStudentagain and adds it toreq.useragain, 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.jsand gets forwarded toindexRoutes.Code
app.use('/', indexRoutes);Then, inside this router, because there is no
isLoggedInauthorization middleware here, the route moves the request on to theindexcontroller.Code
router.get('/students', studentsCtrl.index); -
The
indexcontroller incontrollers/index.jsnow gets all thestudents(along with the newly-added fact) and renders them with theviews/students/index.ejsview. 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 }); }); }


