Sunday, April 28, 2019

Easy Node Authentication

Application Structure


    - app
    ------ models
    ---------- user.js  <!-- our user model -->
    ------ routes.js    <!-- all the routes for our application -->
    - config
    ------ auth.js      <!-- will hold all our client secret keys (facebook, twitter, google) -->
    ------ database.js  <!-- will hold our database connection settings -->
    ------ passport.js  <!-- configuring the strategies for passport -->
    - views
    ------ index.ejs    <!-- show our home page with login links -->
    ------ login.ejs    <!-- show our login form -->
    ------ signup.ejs   <!-- show our signup form -->
    ------ profile.ejs  <!-- after a user logs in, they will see their profile -->
    - package.json      <!-- handle our npm packages -->
    - server.js         <!-- setup our application -->

Go ahead and create all those files and folders and we'll fill them in as we go along

Packages package.json
We are going to install all the packages needed for the entire tutorial series. This means we'll install all the packages needed for passport local, facebook, twitter, google, and the other things we need. It's all commented out so you know what each does.
// package.json

{
  "name": "node-authentication",
  "main": "server.js",
  "dependencies" : {
    "express" : "~4.14.0",           
    "ejs" : "~2.5.2",               
    "mongoose" : "~4.13.1",          
    "passport" : "~0.3.2",         
    "passport-local" : "~1.0.0",    
    "passport-facebook" : "~2.1.1", 
    "passport-twitter" : "~1.0.4",  
    "passport-google-oauth" : "~1.0.0",
    "connect-flash" : "~0.1.1",     
    "bcrypt-nodejs" : "latest",

    "morgan": "~1.7.0",
    "body-parser": "~1.15.2",
    "cookie-parser": "~1.4.3",
    "method-override": "~2.3.6",
    "express-session": "~1.14.1"   
  }
}

npm install With all of our packages ready to go, let's set up our application in server.js.

Application Setup server.js
Let's make all our packages work together nicely. Our goal is to set up this file and try to have it bootstrap our entire application. We'd like to not go back into this file if it can be helped. This file will be the glue for our entire application.
// server.js

// set up ======================================================================
// get all the tools we need
var express  = require('express');
var app      = express();
var port     = process.env.PORT || 8080;
var mongoose = require('mongoose');
var passport = require('passport');
var flash    = require('connect-flash');

var morgan       = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser   = require('body-parser');
var session      = require('express-session');

var configDB = require('./config/database.js');

// configuration ===============================================================
mongoose.connect(configDB.url); // connect to our database

// require('./config/passport')(passport); // pass passport for configuration

// set up our express application
app.use(morgan('dev')); // log every request to the console
app.use(cookieParser()); // read cookies (needed for auth)
app.use(bodyParser()); // get information from html forms

app.set('view engine', 'ejs'); // set up ejs for templating

// required for passport
app.use(session({ secret: 'ilovescotchscotchyscotchscotch' })); // session secret
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session

// routes ======================================================================
require('./app/routes.js')(app, passport); // load our routes and pass in our app and fully configured passport

// launch ======================================================================
app.listen(port);
console.log('The magic happens on port ' + port);

We are going to comment out our passport configuration for now. We'll uncomment it after we create that config/passport.js file.
The path of our passport object is important to note here. We will create it at the very beginning of the file with var passport = require('passport');. Then we pass it into our config/passport.js file for it to be configured. Then we pass it to the app/routes.js file for it to be used in our routes.
Now with this file, we have our application listening on port 8080. All we have to do to start up our server is:
node server.js Then when we visit http://localhost:8080 we will see our application. (Not really right this moment since we have some more set up to do)
Auto Refreshing: By default, node doesn't automatically refresh our server every time we change files. To do that we'll use nodemon. Just install with: npm install -g nodemon and use with: nodemon server.js.
Now this won't do much for our application since we don't have our database configurationroutesuser model, or passport configuration set up. Let's do the database and routes now.

Database Config config/database.js
We already are calling this file in server.js. Now we just have to set it up.
// config/database.js
module.exports = {

    'url' : 'your-settings-here' // looks like mongodb://<user>:<pass>@mongo.onmodulus.net:27017/Mikha4ot

};
Routes app/routes.js
We will keep our routes simple for now. We will have the following routes:
  • Home Page (/)
  • Login Page (/login)
  • Signup Page (/signup)
  • Handle the POST for both login
  • Handle the POST for both signup
  • Profile Page (after logged in)
// app/routes.js
module.exports = function(app, passport) {

    // =====================================
    // HOME PAGE (with login links) ========
    // =====================================
    app.get('/', function(req, res) {
        res.render('index.ejs'); // load the index.ejs file
    });

    // =====================================
    // LOGIN ===============================
    // =====================================
    // show the login form
    app.get('/login', function(req, res) {

        // render the page and pass in any flash data if it exists
        res.render('login.ejs', { message: req.flash('loginMessage') }); 
    });

    // process the login form
    // app.post('/login', do all our passport stuff here);

    // =====================================
    // SIGNUP ==============================
    // =====================================
    // show the signup form
    app.get('/signup', function(req, res) {

        // render the page and pass in any flash data if it exists
        res.render('signup.ejs', { message: req.flash('signupMessage') });
    });

    // process the signup form
    // app.post('/signup', do all our passport stuff here);

    // =====================================
    // PROFILE SECTION =====================
    // =====================================
    // we will want this protected so you have to be logged in to visit
    // we will use route middleware to verify this (the isLoggedIn function)
    app.get('/profile', isLoggedIn, function(req, res) {
        res.render('profile.ejs', {
            user : req.user // get the user out of session and pass to template
        });
    });

    // =====================================
    // LOGOUT ==============================
    // =====================================
    app.get('/logout', function(req, res) {
        req.logout();
        res.redirect('/');
    });
};

// route middleware to make sure a user is logged in
function isLoggedIn(req, res, next) {

    // if user is authenticated in the session, carry on 
    if (req.isAuthenticated())
        return next();

    // if they aren't redirect them to the home page
    res.redirect('/');
}

app.post: For now, we will comment out the routes for handling the form POST. We do this since a passport isn't set up yet.
req.flash: This is the connect-flash way of getting flash data in the session. We will create the loginMessage inside our passport configuration.
isLoggedIn: Using route middleware, we can protect the profile section route. A user has to be logged in to access that route. Using the isLoggedIn function, we will kick a user back to the home page if they try to access http://localhost:8080/profile and they are not logged in.
Logout: We will handle logout by using req.logout() provided by passport. After logging out, redirect the user to the home page.
With our server running, we can visit our application in our browser at http://localhost:8080. Once again, we won't see much since we haven't made our views. Let's go do that now. (We're almost to the authentication stuff, I promise).

Views views/index.ejs, views/login.ejs, views/signup.ejs
Here we'll define our views for our home pagelogin page, and signup/registration page.

Home Page views/index.ejs

Our home page will just show links to all our forms of authentication.
<!-- views/index.ejs -->
<!doctype html>
<html>
<head>
    <title>Node Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
        body        { padding-top:80px; }
    </style>
</head>
<body>
<div class="container">

    <div class="jumbotron text-center">
        <h1><span class="fa fa-lock"></span> Node Authentication</h1>

        <p>Login or Register with:</p>

        <a href="/login" class="btn btn-default"><span class="fa fa-user"></span> Local Login</a>
        <a href="/signup" class="btn btn-default"><span class="fa fa-user"></span> Local Signup</a>
    </div>

</div>
</body>
</html>
Here are the views for our login and signup pages also.

Login Form views/login.ejs

<!-- views/login.ejs -->
<!doctype html>
<html>
<head>
    <title>Node Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
        body        { padding-top:80px; }
    </style>
</head>
<body>
<div class="container">

<div class="col-sm-6 col-sm-offset-3">

    <h1><span class="fa fa-sign-in"></span> Login</h1>

    <!-- show any messages that come back with authentication -->
    <% if (message.length > 0) { %>
        <div class="alert alert-danger"><%= message %></div>
    <% } %>

    <!-- LOGIN FORM -->
    <form action="/login" method="post">
        <div class="form-group">
            <label>Email</label>
            <input type="text" class="form-control" name="email">
        </div>
        <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" name="password">
        </div>

        <button type="submit" class="btn btn-warning btn-lg">Login</button>
    </form>

    <hr>

    <p>Need an account? <a href="/signup">Signup</a></p>
    <p>Or go <a href="/">home</a>.</p>

</div>

</div>
</body>
</html>

Signup Form views/signup.ejs

<!-- views/signup.ejs -->
<!doctype html>
<html>
<head>
    <title>Node Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
        body        { padding-top:80px; }
    </style>
</head>
<body>
<div class="container">

<div class="col-sm-6 col-sm-offset-3">

    <h1><span class="fa fa-sign-in"></span> Signup</h1>

    <!-- show any messages that come back with authentication -->
    <% if (message.length > 0) { %>
        <div class="alert alert-danger"><%= message %></div>
    <% } %>

    <!-- LOGIN FORM -->
    <form action="/signup" method="post">
        <div class="form-group">
            <label>Email</label>
            <input type="text" class="form-control" name="email">
        </div>
        <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" name="password">
        </div>

        <button type="submit" class="btn btn-warning btn-lg">Signup</button>
    </form>

    <hr>

    <p>Already have an account? <a href="/login">Login</a></p>
    <p>Or go <a href="/">home</a>.</p>

</div>

</div>
</body>
</html>

Authenticating With Passport Locally

Finally! We have finally set up our application and have gotten to the authentication part. Don't worry. The rest of the authentication articles in this tutorial series will use the same base so we won't have to set up our application again.
So far we have installed our packagesset up our applicationconnected to our databasecreated our routes, and created our views.
Now we will create our user modelconfigure passport for local authentication, and use our configured passport to process our login/signup forms.

User Model

We will create our user model for the entire tutorial series. Our user will have the ability to be linked to multiple social accounts and to a local account. For local accounts, we will be keeping email and password. For the social accounts, we will be keeping their idtoken, and some users information.
You can change these fields out to be whatever you want. You can authenticate locally using username and password (passport-local actually uses username by default but we'll change that to email).
// app/models/user.js
// load the things we need
var mongoose = require('mongoose');
var bcrypt   = require('bcrypt-nodejs');

// define the schema for our user model
var userSchema = mongoose.Schema({

    local            : {
        email        : String,
        password     : String,
    },
    facebook         : {
        id           : String,
        token        : String,
        name         : String,
        email        : String
    },
    twitter          : {
        id           : String,
        token        : String,
        displayName  : String,
        username     : String
    },
    google           : {
        id           : String,
        token        : String,
        email        : String,
        name         : String
    }

});

// methods ======================
// generating a hash
userSchema.methods.generateHash = function(password) {
    return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
};

// checking if password is valid
userSchema.methods.validPassword = function(password) {
    return bcrypt.compareSync(password, this.local.password);
};

// create the model for users and expose it to our app
module.exports = mongoose.model('User', userSchema);

Our model is done. We will be hashing our password within our user model before it saves to the database. This means we don't have to deal with generating the hash ourselves. It is all handled nicely and neatly inside our user model.
Let's move onto the important stuff of this article: authenticating locally!

Configuring Passport for Local Accounts

All the configuration for passport will be handled in config/passport.js. We want to keep this code in its own file away from our other main files like routes or the server file. I have seen some implementations where passport will be configured in random places. I believe having it in this config file will keep your overall application clean and concise.
So far, we created our passport object in server.js, and then we pass it to our config/passport.js file. This is where we configure our Strategy for local, facebook, twitter, and google. This is also the file where we will create the serializeUser and deserializeUserfunctions to store our user in session.

Handling Signup/Registration
We will be handling login and signup in config/passport.js. Let's look at signup first.
// config/passport.js

// load all the things we need
var LocalStrategy   = require('passport-local').Strategy;

// load up the user model
var User            = require('../app/models/user');

// expose this function to our app using module.exports
module.exports = function(passport) {

    // =========================================================================
    // passport session setup ==================================================
    // =========================================================================
    // required for persistent login sessions
    // passport needs ability to serialize and unserialize users out of session

    // used to serialize the user for the session
    passport.serializeUser(function(user, done) {
        done(null, user.id);
    });

    // used to deserialize the user
    passport.deserializeUser(function(id, done) {
        User.findById(id, function(err, user) {
            done(err, user);
        });
    });

    // =========================================================================
    // LOCAL SIGNUP ============================================================
    // =========================================================================
    // we are using named strategies since we have one for login and one for signup
    // by default, if there was no name, it would just be called 'local'

    passport.use('local-signup', new LocalStrategy({
        // by default, local strategy uses username and password, we will override with email
        usernameField : 'email',
        passwordField : 'password',
        passReqToCallback : true // allows us to pass back the entire request to the callback
    },
    function(req, email, password, done) {

        // asynchronous
        // User.findOne wont fire unless data is sent back
        process.nextTick(function() {

        // find a user whose email is the same as the forms email
        // we are checking to see if the user trying to login already exists
        User.findOne({ 'local.email' :  email }, function(err, user) {
            // if there are any errors, return the error
            if (err)
                return done(err);

            // check to see if theres already a user with that email
            if (user) {
                return done(null, false, req.flash('signupMessage', 'That email is already taken.'));
            } else {

                // if there is no user with that email
                // create the user
                var newUser            = new User();

                // set the user's local credentials
                newUser.local.email    = email;
                newUser.local.password = newUser.generateHash(password);

                // save the user
                newUser.save(function(err) {
                    if (err)
                        throw err;
                    return done(null, newUser);
                });
            }

        });    

        });

    }));

};

We have now provided a strategy to passport called local-signup. We will use this strategy to process our signup form. Let's open up our app/routes.js and handle the POST for our signup form.
// app/routes.js
...

    // process the signup form
    app.post('/signup', passport.authenticate('local-signup', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/signup', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));

...

That's all the code we need for the route. All of the heavy duty stuff lives inside of config/passport.js. All we have to set here is where our failures and successes get redirected. Super clean.
There is also much more you can do with this. Instead of specifying a successRedirect, you could use a callback and take more control over how your application works. Here is a great stackoverflow answer on error handling. It explains how to use done() and how to be more specific with your handling of a route.

Testing Signup

With our passport config finally laid out, we can uncomment that line in our server.js. This will load our config and then we can use our signup form.
// server.js
...

    // uncomment this line
    require('./config/passport')(passport); // pass passport for configuration

...

Now that we have passport, our routes, and our redirects in place, let's go ahead and test our signup form. In your browser, go to http://localhost:8080/signup and fill out your form.
If all goes according to plan, you should be logged in, your user saved in the session, and you are redirected to the /profile page (the profile page will show nothing right now since we haven't defined that view).
If we look in our database, we'll also see our user sitting there cozily with all the credentials we created for him.
Exploring Your Database: I use Robomongo to see what's in my database. Just download it and connect to your database to see your new users after they signup!
With users able to sign up, let's give them a way to login.

Login

This will be very similar to the signup strategy. We'll add the strategy to our config/passport.jsand the route in app/routes.js.
// config/passport.js

...

    // =========================================================================
    // LOCAL LOGIN =============================================================
    // =========================================================================
    // we are using named strategies since we have one for login and one for signup
    // by default, if there was no name, it would just be called 'local'

    passport.use('local-login', new LocalStrategy({
        // by default, local strategy uses username and password, we will override with email
        usernameField : 'email',
        passwordField : 'password',
        passReqToCallback : true // allows us to pass back the entire request to the callback
    },
    function(req, email, password, done) { // callback with email and password from our form

        // find a user whose email is the same as the forms email
        // we are checking to see if the user trying to login already exists
        User.findOne({ 'local.email' :  email }, function(err, user) {
            // if there are any errors, return the error before anything else
            if (err)
                return done(err);

            // if no user is found, return the message
            if (!user)
                return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash

            // if the user is found but the password is wrong
            if (!user.validPassword(password))
                return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata

            // all is well, return successful user
            return done(null, user);
        });

    }));

};

We have now provided a strategy to passport called local-login. We will use this strategy to process our login form. We can check if a user exists, if the password is wrong, and set flash data to show error messages. Let's open up our app/routes.js and handle the POST for our login form.
// app/routes.js
...

    // process the login form
    app.post('/login', passport.authenticate('local-login', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/login', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));

...

If you try to login with a user email that doesn't exist in our database, you will see the error. Same goes for if your password is wrong.

Displaying User and Secure Profile Page views/profile.ejs


Now we have functional signup and login forms. If a user is successful in authenticating they will be redirected to the profile page. If they are not successful, they will go home. The last thing we need to do is make our profile page so that those that are lucky enough to signup (all of us?) will have an exclusive place of our site all to themselves.
<!-- views/profile.ejs -->
<!doctype html>
<html>
<head>
    <title>Node Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css">
    <style>
        body        { padding-top:80px; word-wrap:break-word; }
    </style>
</head>
<body>
<div class="container">

    <div class="page-header text-center">
        <h1><span class="fa fa-anchor"></span> Profile Page</h1>
        <a href="/logout" class="btn btn-default btn-sm">Logout</a>
    </div>

    <div class="row">

        <!-- LOCAL INFORMATION -->
        <div class="col-sm-6">
            <div class="well">
                <h3><span class="fa fa-user"></span> Local</h3>

                    <p>
                        <strong>id</strong>: <%= user._id %><br>
                        <strong>email</strong>: <%= user.local.email %><br>
                        <strong>password</strong>: <%= user.local.password %>
                    </p>

            </div>
        </div>

    </div>

</div>
</body>
</html>

After a user logs in, they can see all their information. It is grabbed from the session and passed to our view in the app/routes.js file. We will also provide a link to logout.

No comments:

Post a Comment