In the context of the web, what is authentication? Is it the same as authorization? →
How do websites verify that a user is who they claim to be? How do websites implement authentication? →
(a code that's texted to your phone - think gmail's 2-factor auth, or a dedicated device, like yubikey)
If our site collects any sensitive information from a user, the communication between our server and the client should be encrypted. To do this, you'll need to use TLS/SSL (that's when you see the padlock icon and https in the schema part of the url):
TLS/SSL are cryptographic protocols
You'll have to: →
Let's Encrypt is a free certificate authority backed by a non-profit. Check out:
A fully detailed lecture on security and encryption is beyond the scope of this class (we'll talk a little more about tls/ssl). However, you should know there's support for TLS/SSL in Node.js and Express. →
// require http, https, express, etc.
const options = {
key: fs.readFileSync(__dirname + '/ssl/server.pem'),
cert: fs.readFileSync(__dirname + '/ssl/server.crt'),
};
https.createServer(options, app).listen(app.get('port'), function(){
console.log('Express started ...');
});
If you're curious about how it works under the hood: →
Also, we can actually check out certs in our browser. →
(try going to home.nyu.edu in chrome… and check on the padlock)
Ok… now that that's out of the way… If we'd like to add username and password for authentication, where do we store that information? →
Our database makes sense, of course, but what would our Schema look like, and what would the contents be of each field? →
Simple enough… just two fields, username to store username and password to store password. Easy!
const userSchema = mongoose.Schema({
{
username: String,
password: String,
})
That password field is just the password in plain text. Why is storing a password in plain text a bad idea? →
The data in our database may be compromised (how? →):
Both are ways that we can use to transform a string into another string… but what's the difference between the two? →
Which do you think is appropriate for storing passwords? Why? →
Ok… so, how do I find or create an adequate hashing algorithm? What are some properties that we would look for? →
It turns out that these are the ones that are recommended: →
bcrypt
PBKDF2
But only for now … as the landscape continues to change: →
What are some ways of figuring out a password from a hash? (You'll see why the hashing algorithm should be computationally expensive) →
Is a one way hash of a password adequate? Are we done yet? What's another consideration? →
What can be inferred from two passwords if their hash is the same? →
They're the same passwords! If you figure out one, you've figured out the other.
How can we make the hash of two of the same passwords different from eachother? →
Add salt.
To prevent the hash of two of the same passwords from being the same, we can salt the password.
And here are some particularly good resources
Assuming that we have all of the previous stuff on password storage right. What's next? We'll need to manage:
We'll use the following node modules for authentication and session management:
So… what does it actually do? →
req.user
object
To setup passport… you'll need to →
Passport uses strategies to authenticate a request. There are multiple ways to authenticate a user (we mentioned them before). What are some possible authentication strategies? →
We'll be using local authentication… authentication with a username and password stored in a local database (MongoDB).
When we create a strategy, we define a callback function that: →
We also have to activate two pieces of middleware:
app.use(passport.initialize());
app.use(passport.session());
Username and password (credentials) are usually only transmitted once during the initial login request. →
To support login sessions, Passport will serialize and deserialize instances of the user object to and from the session store (for us the session store is in memory).
By the way, what do we mean by serialization? →
Translate a data structure / object to a storable format). We'll have to define functions that do this and tell passport all about it or rely on (again) a module. →
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
Passport is middleware that authenticates requests. It'll give us:
If someone registers for our site, what are the steps that we should take for storing their login/password info? →
And what about logging in… how can we tell if a person's password is correct. What steps should we take? →
Again, Passport allows the flexibility of writing our own strategy for:
However… that's a lot of work, and it's easy to get that stuff wrong (for something so important, it's maybe too easy to get wrong).
Passport-Local Mongoose is a plugin for Mongoose that bundles up all of that functionality by bringing together passport and Mongoose.
It provides a bunch of static methods for us - that we otherwise have to write on our own - for:
We have an idea what these might look like, right? Let's check the actual implementation
Outside of registration and login, what else might we need to support if we have username/password authentication?
We'll support the following features:
As usual, we'll create a db.js
that contains our schemas, registers our models and connects to the database.
The user schema can be totally blank. Passport local mongoose will add properties to the schema, as well as some static methods! →
const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const UserSchema = new mongoose.Schema({ });
UserSchema.plugin(passportLocalMongoose);
mongoose.model('User', UserSchema);
mongoose.connect('mongodb://localhost/class16db');
In a file called auth.js
in the root of your project, let passport know what strategy you want to use as well as how to serialize and deserialize a user:
const mongoose = require('mongoose'),
passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
User = mongoose.model('User');
// hey... one of those static functions that passport-local
// mongoose gives our model...
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
At the top of app.js, bring in the two files that we created, db.js
and auth.js
:
require('./db');
require('./auth');
const passport = require('passport');
We've done this before (put this after you've created your app object):
const session = require('express-session');
const sessionOptions = {
secret: 'secret cookie thang (store this elsewhere!)',
resave: true,
saveUninitialized: true
};
app.use(session(sessionOptions));
Start up passport and allow login sessions using the following middleware (do this before defining/using your routes!):
app.use(passport.initialize());
app.use(passport.session());
Add some middleware that drops req.user into the context of every template. This is done by adding properties to res.locals.
app.use(function(req, res, next){
res.locals.user = req.user;
next();
});
In a file in the routes directory, let's setup our usual set of requires for creating routers. Additionally, add dependencies for passport and mongoose so that we can actually login and register. (Don't forget to export your router too)
const express = require('express'),
router = express.Router(),
passport = require('passport'),
mongoose = require('mongoose'),
User = mongoose.model('User');
// route handlers go above
module.exports = router;
These route handlers will handle requests to home, the login form and the registration form:
router.get('/', function(req, res) {
res.render('index');
});
router.get('/login', function(req, res) {
res.render('login');
});
router.get('/register', function(req, res) {
res.render('register');
});
The templates for both of these will pretty much be the same. The auth strategy we use expect username and password, so we'll name our input fields that.
We'll also reserve a spot for error messages.
...
Lastly, it might be nice to drop in username in our layouts.hbs
(Remember, we put user into the context using some not-so-fancy middleware).
{{#if user}}
Logged in as {{user.username}}
{{/if}}
Our registration post handler will create a new user or render an error if something goes wrong. If a new user is created, go ahead and log them in!
router.post('/register', function(req, res) {
User.register(new User({username:req.body.username}),
req.body.password, function(err, user){
if (err) {
res.render('register',{message:'Your registration information is not valid'});
} else {
passport.authenticate('local')(req, res, function() {
res.redirect('/');
});
}
});
});
Ugh… so login is a bit weird. Here, we authenticate, and on successful authentication, use req.logIn to start the logged in session.
Otherwise, render an error message…
router.post('/login', function(req,res,next) {
passport.authenticate('local', function(err,user) {
if(user) {
req.logIn(user, function(err) {
res.redirect('/');
});
} else {
res.render('login', {message:'Your login or password is incorrect.'});
}
})(req, res, next);
});
There are a couple of demos that I've created in the examples repository (you need to be logged in to github to see these):
The 2nd version allows a user to store image urls.
/users/some-username