Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save micahwierenga/868bc6dfce3888e00a7dd3e9788ec847 to your computer and use it in GitHub Desktop.
Save micahwierenga/868bc6dfce3888e00a7dd3e9788ec847 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:

    Code
    const session = require('express-session');
    const passport = require('passport');
  2. Mount all passport configurations to the server in server.js.

    Code

    require('./config/passport');

  3. Load passport and passport-google-oauth libraries in config/passport.js.

    Code
    const passport = require('passport');
    const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
  4. In config/passport.js, pass an instance of GoogleStrategy, along with initial values to its constructor function to passport.use (these values come from the .env file), which will 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 later.

    We'll also configure the passport.serializeUser and passport.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);
        });
    });
  5. Load the appropriate routes (the authentication-related routes) in server.js.

    Code

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

  6. Load the passport library in routes/index.js:

    Code

    const passport = require('passport');

  7. 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 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.

    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');
    });
  8. In server.js, 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.

    Code
    app.use(session({
    secret: 'SEIRocks!',
    resave: false,
    saveUninitialized: true
    }));
  9. Now that it's been configured, mount passport in server.js.

    Code
    app.use(passport.initialize());
    app.use(passport.session());
  10. Mount routes with appropriate base paths in server.js.

    Code

    app.use('/', indexRoutes);

When the Server Starts

How the Authentication Route is Handled

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

    Code
    app.use(session({
    secret: 'SEIRocks!',
    resave: false,
    saveUninitialized: true
    }));
  2. In views/students/index.ejs, the user will click on this link with the auth/google route to initialize the login request.

    Code
    <a href="/auth/google"><i class="material-icons left">vpn_key</i>Login with Google</a>
  3. The already-mounted /auth/google route in routes/index.js will coordinate with Google's OAuth server.

    Code
    router.get('/auth/google',
        passport.authenticate('google', { 
        scope: ['profile', 'email'] 
    })
    );
  4. Once the user confirms, the /oauth2callback route in routes/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'
    })
    );
  5. When the server loaded, the constructor properties of the new GoogleStrategy instance were set in config/passport.js. 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.

    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);
                });
            }
        });
    }
    ));
  6. In config/passport.js, if the student is verified, use the done callback add that user's id to their session. (Since this is MongoDB, you can also use student._id.)

    Code
    passport.serializeUser(function(student, done) {
        done(null, student.id);
    });
  7. In config/passport.js, 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)!

    Code
    passport.deserializeUser(function(id, done) {
        Student.findById(id, function(err, student) {
            done(err, student);
        });
    });
  8. The callback route (/oauth2callback) redirects the user to the /students route and therefore to this index controller in controllers/students.js in order to complete the request. Notice how we can set the user key inside the render with req.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 
        });
    });
    }
  9. 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>
    <% } %>

How the Authentication Route is Handled

How Authorization Protects an Example Route

  1. 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>
  2. In config/passport.js, 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)!

    Code
    passport.deserializeUser(function(id, done) {
        Student.findById(id, function(err, student) {
            done(err, student);
        });
    });
  3. 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 the addFact controller. But, before it can get there, it must go through the isLoggedIn authorization middleware.

    Code

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

  4. 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');
    }
  5. In controllers/students.js, 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.

    Code
    function addFact(req, res, next) {
    req.user.facts.push(req.body);
    req.user.save(function(err) {
        res.redirect('/students');
    });
    }
  6. Because the addFact controller triggered a new request to the /students route, we're back at the deserializeUser method in config/passport.js 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.

    Code
    passport.deserializeUser(function(id, done) {
        Student.findById(id, function(err, student) {
            done(err, student);
        });
    });
  7. The redirected request hits the server.js and gets forwarded to indexRoutes.

    Code

    app.use('/', indexRoutes);

    Then, inside this router, because there is no isLoggedIn authorization middleware here, the route moves the request on to the index controller.

    Code

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

  8. The index controller in controllers/index.js now gets all the students (along with the newly-added fact) and renders them with the views/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 
        });
    });
    }

How Authorization Protects an Example Route

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