Skip to content

Instantly share code, notes, and snippets.

@luishrd
Last active July 9, 2018 06:20
Show Gist options
  • Save luishrd/f524250abfb89161a62ee5357abe6988 to your computer and use it in GitHub Desktop.
Save luishrd/f524250abfb89161a62ee5357abe6988 to your computer and use it in GitHub Desktop.
CS10 - Auth with Sessions

Adding Authentication using Sessions and Cookies

Day 1

Proper AuthN

Hashing vs Encryption

Hashing

  • one way process.
  • password + parameters => hash.
  • MD5, SHA-1, optimized for speed.
  • [hash] + [time] = [Key Derivation Function] === bcrypt.

Encryption

  • a two way process.

Day 2

Review

  • what is the difference between global and local express middleware?
  • what is another name for mongoose middleware?
  • is it better to encrypt or hash passwords? why?
  • which middleware hook is useful for encrypting/hashing passwords when creating users?
  • which npm package did we use to do the password encryption/hashing? which method did we call on it?
  • why do we add time/cost to our encryption/hashing algorithm? what is a good number of rounds?
  • is it better to require uppercase + numbers + special characters or a minimum length to make passwords more secure? why?

Topics

  • implementing login.
  • extending mongoose models with custom methods.
  • using sessions.
  • protect a resource to only allow access for logged in users
  • implementing logout.

Sessions

express-session

  • sessions are a way to persist data across requests.
  • each user/device has a unique session.

Adding session support:

const session = require('express-session');
server.use(
  session({
    secret: 'nobody tosses a dwarf!',
    cookie: { maxAge: 1 * 24 * 60 * 60 * 1000 }, // 1 day in milliseconds
    httpOnly: true,
    secure: true,
  })
);

Now we can store session data in one route handler and read it in another.

app.get('/', (req, res) => {
  req.session.name = 'Luis';
  res.send('got it');
});
app.get('/greet', (req, res) => {
  const name = req.session.name;
  res.send(`hello ${req.session.name}`);
});

Common ways to store session data:

  • memory
  • cookie
  • memory cache (like Redis and Memcached)
  • database

Storing session data in memory

  • data stored in memory is wiped when the server restarts.
  • causes memory leaks as more and more memory is used as the application continues to store data in session for different clients.
  • good for development due to its simplicity.

Storing session data in cookies.

  • a cookie is a small key/value pair data structure that is passed back and forth between client and server and stored in the browser.
  • the server use it to store information about a particular client/user.
  • workflow for using cookies as session storage:
    • the server issues a cookie with an expiration time and sends it with the response.
    • browsers automatically store the cookie and send it on every request to the same domain.
    • the server can read the information contained in the cookie (like the username).
    • the server can make changes to the cookie before sending it back on the response.
    • rinse and repeat.

express-session uses cookies for session management.

Drawbacks when using cookies

  • small size, around 4KB.
  • sent in every request, increasing the size of the request if too much information is stored in them.
  • if an attacker gets a hold of the private key used to encrypt the cookie they could read the cookie data.

Storing session data in Memory Cache (preferred way of storing sessions in production applications)

  • stored as key-value pair data in a separate server.
  • the server still uses a cookie, but it only contains the session id.
  • the memory cache server uses that session id to find the session data.

Advantages

  • quick lookups.
  • decoupled from the api server.
  • a single memory cache server can serve may applications.
  • automatically remove old session data.

Downsides

  • another server to set up and manage.
  • extra complexity for small applications.
  • hard to reset the cache without losing all session data.

Storing session data in a database

connect-mongo

  • similar to storing data in a memory store.
  • the session cookie still holds the session id.
  • the server uses the session id to find the session data in the database.
  • retrieving data from a database is slower than reading from a memory cache.
  • causes chatter between the server an the database.
  • need to manage/remove old sessions manually or the database will be filled with unused session data. connect-mongo manages that for you.
const session = require('express-session');
const connectMongo = require('connect-mongo');
const MongoStore = connectMongo(session);
// const MongoStore = require('connect-mongo')(session); // shorter

const sessionConfig = {
  secret: 'nobody tosses a dwarf!',
  cookie: {
    maxAge: 1 * 24 * 60 * 60 * 1000,
  }, // 1 day in milliseconds
  httpOnly: true,
  secure: false,
  resave: true,
  saveUninitialized: false,
  name: 'noname',
  store: new MongoStore({
    url: 'mongodb://localhost/sessions',
    ttl: 60 * 10,
  }),
};

server.use(session(sessionConfig));
{
"name": "cs10",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "nodemon"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"nodemon": "^1.17.5"
},
"dependencies": {
"bcrypt": "^2.0.1",
"express": "^4.16.3",
"express-session": "^1.15.6",
"mongoose": "^5.1.4"
}
}
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const User = require('./auth/UserModel');
mongoose.connect('mongodb://localhost/cs10').then(() => {
console.log('\n*** Connected to database ***\n');
});
const server = express();
// middleware
const sessionOptions = {
secret: 'nobody tosses a dwarf!',
cookie: {
maxAge: 1000 * 60 * 60, // an hour
},
httpOnly: true,
secure: false,
resave: true,
saveUninitialized: false,
name: 'noname',
};
function protected(req, res, next) {
if (req.session && req.session.username) {
next();
} else {
res.status(401).json({ message: 'you shall not pass!!' });
}
}
server.use(express.json());
server.use(session(sessionOptions));
server.get('/api/users', protected, (req, res) => {
User.find()
.then(users => res.json(users))
.catch(err => res.json(err));
});
server.get('/', (req, res) => {
if (req.session && req.session.username) {
res.status(200).json({ message: `welcome back ${req.session.username}` });
} else {
res.status(401).json({ message: 'speak friend and enter' });
}
});
server.post('/api/register', (req, res) => {
// save the user to the database
// const user = new User(req.body);
// user.save().then().catch;
// or an alternative syntax would be:
User.create(req.body)
.then(user => {
res.status(201).json(user);
})
.catch(err => {
res.status(500).json(err);
});
});
server.post('/api/login', (req, res) => {
// grab credentials
const { username, password } = req.body;
// find the user to get access to the store password
User.findOne({ username })
.then(user => {
if (user) {
// compare password guess to the stored password
user
.validatePassword(password)
.then(passwordsMatch => {
// the passwords match, the can continue
if (passwordsMatch) {
req.session.username = user.username;
res.send('have a cookie');
} else {
res.status(401).send('invalid credentials');
}
})
.catch(err => {
res.send('error comparing passwords');
});
} else {
// if not found
res.status(401).send('invalid credentials');
}
})
.catch(err => {
res.send(err);
});
});
server.get('/api/logout', (req, res) => {
if (req.session) {
req.session.destroy(err => {
if (err) {
res.send('error logging out');
} else {
res.send('good bye');
}
});
}
});
server.listen(8000, () => {
console.log('\n*** API running on port 8K ***\n');
});
// ./auth/UserModel.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 4,
},
});
userSchema.pre('save', function(next) {
// console.log('pre save hook');
bcrypt.hash(this.password, 12, (err, hash) => {
// it's actually 2 ^ 12 rounds
if (err) {
return next(err);
}
this.password = hash;
next();
});
});
userSchema.methods.validatePassword = function(passwordGuess) {
return bcrypt.compare(passwordGuess, this.password);
};
module.exports = mongoose.model('User', userSchema);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment