Objective: Add auth to a simple blogging platform using passport.js
This application has the following REST endpoints:
GET /api/posts
to READ all blog postsPOST /api/posts
to CREATE a new blog postGET /api/posts/:id
to READ one blog postPUT /api/posts/:id
to UPDATE one blog postDELETE /api/posts/:id
to DELETE one blog post- Full CRUD for blog posts
- A persistent database that store blog posts with a
title
anddescription
. - Client-side templating using Handlebars (and AJAX)
- Bootstrap
- Fork this repo, and clone it into your
develop
folder on your local machine. - Next, run
npm install
to download the dependencies listed inpackage.json
- You are encouraged to use Postman to test and debug your API routes. Make sure you are always
console.log
-ing data that comes back from your API when you make an AJAX call before you write any other code.
Adapted from User Authentication With Passport and Express 4.
- Install the new libraries
cookie-parser
,express-session
,passport
,passport-local
, andpassport-local-mongoose
.
➜ npm install --save cookie-parser express-session passport passport-local passport-local-mongoose
- cookie-parser: parses cookies from the browser
- express-session: stores logged-in user info in the session
- passport: authentication middleware for Node/Express
- passport-local: passport strategy for authenticating with username and password
- passport-local-mongoose: mongoose plugin that simplifies building username and password auth with passport
- Require the newly installed dependencies in
server.js
.
/*
* server.js
*/
// require express and other modules
var express = require('express'),
app = express(),
bodyParser = require('body-parser'),
mongoose = require('mongoose'),
// NEW ADDITIONS
cookieParser = require('cookie-parser'),
session = require('express-session'),
passport = require('passport'),
LocalStrategy = require('passport-local').Strategy;
- Also in
server.js
, tell Express to use the auth middleware you just installed.
/*
* server.js
*/
...
// middleware for auth
app.use(cookieParser());
app.use(session({
secret: 'supersecretkey', // change this!
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
- Right under your auth middleware in
server.js
, configure passport. This allows users to sign up, log in, and log out of your application.
/*
* server.js
*/
...
// passport config
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
At this point, if you check your terminal, you'll see that your server crashed. This is because in your passport configuration, you're using a User
model (User.authenticate()
, etc.), which you haven't defined yet. Let's go ahead and do that now!
- Create a new file in
models
calleduser.js
.
➜ touch models/user.js
- Open up
user.js
, and requiremongoose
andpassport-local-mongoose
at the top.
/*
* models/user.js
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
passportLocalMongoose = require('passport-local-mongoose');
- Also in
user.js
, define theUserSchema
. Users should have the attributesemail
andpassword
.
/*
* models/user.js
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
passportLocalMongoose = require('passport-local-mongoose');
var UserSchema = new Schema({
username: String,
password: String
});
- Next, add
passportLocalMongoose
to theUserSchema
.passportLocalMongoose
takes care of hashing and salting the user's plain-text password when they sign up. It also takes of comparing the password the user enters at login to their hashed and salted password stored in the database.
/*
* models/user.js
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
passportLocalMongoose = require('passport-local-mongoose');
var UserSchema = new Schema({
username: String,
password: String
});
UserSchema.plugin(passportLocalMongoose);
- The last step is to create the
User
model and export it.
/*
* models/user.js
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
passportLocalMongoose = require('passport-local-mongoose');
var UserSchema = new Schema({
username: String,
password: String
});
UserSchema.plugin(passportLocalMongoose);
var User = mongoose.model('User', UserSchema);
module.exports = User;
Make sure to also update /models/index.js
to import/export your User
model:
/*
* models/user.js
*/
...
module.exports.User = require("./user");
- Back in
server.js
, require theUser
model (you can do this right under where you required thePost
model). Ensure that any references toUser
inserver.js
come after variable declaration forUser
!
/*
* server.js
*/
...
// require Post and User models
var db = require("./models"),
Post = db.Post,
User = db.User;
- Create a new view with a form for users to sign up.
➜ touch views/signup.html
Your signup form should look something like this:
<!-- signup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- set viewport to device width to make site responsive -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap css -->
<link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- custom styles -->
<link rel="stylesheet" type="text/css" href="/styles/style.css">
<!-- jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<!-- bootstrap js -->
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<title>Express Microblog App</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>Sign Up</h2>
<br>
<form method="POST" action="/signup">
<div class="form-group">
<input type="text" name="username" class="form-control" placeholder="Username" autofocus>
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="submit" class="btn btn-block btn-primary" value="Sign Up">
</div>
</form>
</div>
</div>
</div>
</body>
</html>
Take note of the method
and action
in the form. This combination of the request type POST
and the URL /signup
will correspond to a server route for signing up new users.
- In
server.js
, you need two new routes for signing up new users: a route to render thesignup
view, and a route to handle thesignup
request when the user submits the form. Let's create the route to render the view first:
/*
* server.js
*/
...
// AUTH ROUTES
// show signup view
app.get('/signup', function (req, res) {
res.sendFile('/views/signup.html', { root: __dirname });
});
- Now, let's create the route that handles signing up new users. Again, the code in this route will run when the user submits the signup form (since you already set
method
andaction
in the form to match this route).
/*
* server.js
*/
...
// AUTH ROUTES
...
// sign up new user, then log them in
// hashes and salts password, saves new user to db
app.post('/signup', function (req, res) {
User.register(new User({ username: req.body.username }), req.body.password,
function (err, newUser) {
passport.authenticate('local')(req, res, function() {
res.send('signed up!!!');
});
}
);
});
-
Go to
http://localhost:3000/signup
in your browser. You should see the signup form. Fill out the form and hit submit. If it's working, you should see the "signed up!!!" message in your browser. If not, use the error messages you're getting to guide your debugging. -
Best practice is to redirect your user when they have successfully signed up. Change
res.send
tores.redirect('/')
(or wherever you want to send them).
- Create a new view with a form for users to login.
➜ touch views/login.html
Your login form should look something like this:
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- set viewport to device width to make site responsive -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap css -->
<link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- custom styles -->
<link rel="stylesheet" type="text/css" href="/styles/style.css">
<!-- jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<!-- bootstrap js -->
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<title>Express Microblog App</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>Log In</h2>
<br>
<form method="POST" action="/login">
<div class="form-group">
<input type="text" name="username" class="form-control" placeholder="Username" autofocus>
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="submit" class="btn btn-block btn-primary" value="Log In">
</div>
</form>
</div>
</div>
</div>
</body>
</html>
Take note of the method
and action
in the form. This combination of the request type POST
and the URL /login
will correspond to a server route for logging in existing users.
We are not using AJAX to sumbit our forms! We are just using a vanilla HTML form.
- In
server.js
, you need two new routes for logging in existing users: a route to render thelogin
view, and a route to handle thelogin
request when the user submits the form. Let's create the route to render the view first:
/*
* server.js
*/
...
// AUTH ROUTES
...
// show login view
app.get('/login', function (req, res) {
res.sendFile('/views/login.html', { root: __dirname });
});
- Now, let's create the route that handles logging in existing users. Again, the code in this route will run when the user submits the login form (since you already set
method
andaction
in the form to match this route).
/*
* server.js
*/
...
// AUTH ROUTES
...
// log in user
app.post('/login', passport.authenticate('local'), function (req, res) {
console.log(req.user);
res.send('logged in!!!'); // sanity check
// res.redirect('/'); // preferred!
});
-
Go to
http://localhost:3000/login
in your browser. You should see the login form. Fill out the form with the username and password of the user you just created, then hit submit. If it's working, you should see the "logged in!!!" message in your browser. If not, use the error messages you're getting to guide your debugging. -
Best practice is to redirect your user when they have successfully logged in. Change
res.send
tores.redirect('/')
(or wherever you want to send them).
- You should also have a route for a user to log out. Set this up in
server.js
as well. You'll want to redirect the user to the homepage (/
) after successfully logging out.
/*
* server.js
*/
...
// AUTH ROUTES
...
// log out user
app.get('/logout', function (req, res) {
console.log("BEFORE logout", JSON.stringify(req.user));
req.logout();
console.log("AFTER logout", JSON.stringify(req.user));
res.redirect('/');
});
This is an odd step, but a useful strategy for attaching your user to the page without requiring an additional request/response for user data.
- Open
views/index.hbs
and add the following script:
<script type="text/javascript">
window.user = {{{ user }}};
</script>
- We also need to update
server.js
to pass in theuser
object:
/*
* server.js
*/
...
app.get('/', function (req, res) {
res.render('index', {user: JSON.stringify(req.user) + " || null"});
});
This works just like you would expect a front-end handlebars template to work, except that now we're doing this work on the backend!
-
Visit your homepage, and see if the user object is globally accessible:
window.user // {_id: "13245t", username: "example"} // or just user // {_id: "13245t", username: "example"}
Congrats! If a user is currently logged in, you now have an easy way of retrieving their data from the page!
When a logged-in user visits our site, they send a cookie to server which stores their session id. Passport.js looks up the session and attaches the user to the request (req.user
). We can access this user object at any time, and use it to "authorize" or protect our routes.
Of the following /posts
CRUD routes, which do we need to protect?
GET /posts
GET /posts/:id
POST /posts
PUT /posts/:id
DELETE /posts/:id
Can you use the req.user
object to protect sensitive endpoints?
For example:
if (req.user) {
res.send(...)
} else {
res.status(401).send({error: "Not Authorized! Please login first."})
}
Or using the "return early and often" pattern:
if (!req.user) {
return res.sendStatus(401);
}
These approaches may eventually start to get repetitive and cumbersome, at which point you should consider refactoring to use express middleware. For example, you could create a blanket check that no un-logged-in user should ever be able to DELETE
. To get this to work you will need to research app.use
and create your own custom middleware.
See the solution
branch.