Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save micahwierenga/540e943857005a1c579b382913659ac2 to your computer and use it in GitHub Desktop.
Save micahwierenga/540e943857005a1c579b382913659ac2 to your computer and use it in GitHub Desktop.

OAuth: Understanding the Logical Order

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.

When the Server Starts

  1. Load express-session and passport libraries.
In server.js:
const session = require('express-session');
const passport = require('passport');
  1. Mount all passport configurations to the server.
In server.js:

require('./config/passport');

  1. In config/passport.js:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

Load passport and passport-google-oauth libraries.

  1. In config/passport.js:
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);
    });
});

Pass an instance of GoogleStrategy, along with initial values to its constructor function to passport.use. These values come from the .env file, and configure the connection between Google OAuth and your app. The GoogleStrategy 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 below.

We'll also configure the passport.serializeUser method, but the guts of it will also go into action when logging in, which will be explained below.

Finally, we'll configure the passport.deserializeUser method, which will go into action each time the client makes a request to the server, so that will be explained below.

  1. In server.js:

const indexRoutes = require('./routes/index');

The log in routes are located in this file.

  1. In routes/index.js:

const passport = require('passport');

Load the passport library.

  1. In routes/index.js:
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');
});

The /auth/google route will coordinate with Google's OAuth server.

The /oauth2callback route will be called by Google after the user confirms.

Because the logout() method was added to the req object by passport, we can use that in our /logout route.

These routes are now in place to handle the relevant requests.

  1. In server.js:
app.use(session({
  secret: 'SEIRocks!',
  resave: false,
  saveUninitialized: true
}));

Configure and mount the session middleware. The secret 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.

  1. In server.js:
app.use(passport.initialize());
app.use(passport.session());

Now that it's been configured, mount passport.

  1. In server.js:

app.use('/', indexRoutes);

Mount routes with appropriate base paths.

How the Authentication Route is Handled

  1. In server.js:
app.use(session({
  secret: 'SEIRocks!',
  resave: false,
  saveUninitialized: true
}));

When a user first arrives at your app, the session middleware 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 as connect.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.

  1. In views/students/index.ejs:
<a href="/auth/google"><i class="material-icons left">vpn_key</i>Login with Google</a>

Click on this link with the auth/google route to initialize the login request.

  1. In routes/index.js:
router.get('/auth/google',
    passport.authenticate('google', { 
    scope: ['profile', 'email'] 
  })
);

The already-mounted /auth/google route will coordinate with Google's OAuth server.

  1. In routes/index.js:
router.get('/oauth2callback',
  passport.authenticate('google', {
    successRedirect: '/students',
    failureRedirect: '/students'
  })
);

Once the user confirms, the /oauth2callback route will be called by Google, which will redirect the user based on a successful or failed authentication.

  1. In config/passport.js:
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);
            });
        }
    });
  }
));

When the server loaded, the constructor properties of the new GoogleStrategy instance were set. Now the verify callback goes into action to check if a student with the googleId sent back from Google can be found in our db.

  1. In config/passport.js:
passport.serializeUser(function(student, done) {
    done(null, student.id);
});

If the student is verified, add that user's id to their session. (Since this is MongoDB, you can also use student._id.)

  1. In config/passport.js:
passport.deserializeUser(function(id, done) {
    Student.findById(id, function(err, student) {
        done(err, student);
    });
});

Each request goes through deserializeUser. It retrieves the id from the session and uses it to find the Student. Then it adds that student to the request body as req.user, which can be used from any controller (i.e., route handler)!

  1. In controllers/students.js:
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 
    });
  });
}

The callback route (/oauth2callback) redirects the user to the /students route and therefore to this index controller in order to complete the request. Notice how we can set the user key inside the render with req.user.

  1. In views/students/index.ejs:
<% 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>
<% } %>

Now that we have a user, we can hide the "Login with Google" link and show them the "Log Out" link instead.

How Authorization Protects an Example Route

  1. In views/students/index.ejs:
<form action="/facts" method="POST">
  <input type="text" name="text" class="white-text">
  <button type="submit" class="btn white-text">Add Fact</button>
</form>

After entering content into the form, submit it with the "Add Fact" button to initialize a post request to /facts.

  1. In config/passport.js:
passport.deserializeUser(function(id, done) {
    Student.findById(id, function(err, student) {
        done(err, student);
    });
});

As soon as the request comes in, the id is retrieved from the session and used to find the Student. Then it adds that student to the request body as req.user, which can be used from any controller (i.e., route handler)!

  1. In server.js:

app.use('/', studentsRoutes);

Plus this in routes/students.js:

router.post('/facts', isLoggedIn, studentsCtrl.addFact);

The already-mounted routes will direct the request to the addFact controller, but before it can get there, it must go through the authorization middleware.

  1. In routes/students/js:
function isLoggedIn(req, res, next) {
    if ( req.isAuthenticated() ) return next();
    res.redirect('/auth/google');
}

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!

  1. In controllers/students.js:
function addFact(req, res, next) {
  req.user.facts.push(req.body);
  req.user.save(function(err) {
    res.redirect('/students');
  });
}

Since the deserializeUser method already did the Mongoose query of finding the Student and then adding that student to the request object as req.user, we can push the fact to that student's facts array, and then save it to the db. The controller then redirects the logic to the /students index route

  1. In config/passport.js:
passport.deserializeUser(function(id, done) {
    Student.findById(id, function(err, student) {
        done(err, student);
    });
});

Since the addFact controller triggered a new request to the /students route, we're back at the deserializeUser method again because all requests go through it! After it finds the Student again and adds it to req.user again, we're off to the route.

  1. In server.js:

app.use('/', indexRoutes);

Plus this in routes/index.js:

router.get('/students', studentsCtrl.index);

No authorization middleware here, so the route moves us on to the index controller.

  1. In controllers/index.js:
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 
    });
  });
}

The index controller now gets all the students (along with the newly-added fact) and renders them with the views/students/index.ejs view. We're done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment