Skip to content

Instantly share code, notes, and snippets.

@luishrd
Last active December 27, 2023 22:31
Show Gist options
  • Save luishrd/f79df59f089e88503bb9986f70294609 to your computer and use it in GitHub Desktop.
Save luishrd/f79df59f089e88503bb9986f70294609 to your computer and use it in GitHub Desktop.
Auth Sprint

Adding Authentication to APIs

Topics

  • express middleware review (global vs local).

  • mongoose middleware (lifecycle hooks).

  • hashing user passwords for database storage.

  • extending mongoose models with custom methods.

  • persisting state across requests using sessions (in-memory and persisted in mongo).

  • authenticating users using cookies/sessions.

  • protecting resources from unauthenticated users.

  • authenticating users using JSON Web Tokens (JWTs, pronounced JOT).

  • persisting tokens on the front-end to keep users authenticated across sessions.

  • security good practices.

Day 1

Topics

  • express middleware review (global vs local).
  • implementing register endpoint.
  • hashing user passwords for database storage.
  • mongoose middleware (lifecycle hooks).

client > request > [ (middleware) api ({middleware queue} mongoose) ] > db

Proper AuthN (authentication)

  • password storage
  • brute-force attack mitigation
  • password strength - The haystack

Hashing vs Encryption

Encryption

  • it's a two way process
  • plain text password + private key => encrypted password
  • encrypted password + private key => original plain text password

Hashing

  • it's a one way process
  • parameters + password => hash
  • to slow down production of hashes we add cost/time.
  • hash + time = key derivation function === bcrypt

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 express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const User = require('./users/User');
mongoose
.connect('mongodb://localhost/authdb')
.then(conn => {
console.log('\n=== connected to mongo ===\n');
})
.catch(err => console.log('error connecting to mongo', err));
const server = express();
function authenticate(req, res, next) {
if (req.session && req.session.username) {
next();
} else {
res.status(401).send('You shall not pass!!!');
}
}
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(express.json());
server.use(session(sessionConfig));
server.get('/', (req, res) => {
if (req.session && req.session.username) {
res.send(`welcome back ${req.session.username}`);
} else {
res.send('who are you? who, who?');
}
});
server.post('/register', function(req, res) {
const user = new User(req.body);
user
.save()
.then(user => res.status(201).send(user))
.catch(err => res.status(500).send(err));
});
server.post('/login', (req, res) => {
const { username, password } = req.body;
User.findOne({ username })
.then(user => {
if (user) {
// compare the passwords
user.isPasswordValid(password).then(isValid => {
if (isValid) {
req.session.username = user.username;
res.send('have a cookie');
} else {
res.status(401).send('invalid password');
}
});
} else {
res.status(401).send('invalid username');
}
})
.catch(err => res.send(err));
});
server.get('/users', authenticate, (req, res) => {
User.find().then(users => res.send(users));
});
server.get('/logout', (req, res) => {
if (req.session) {
req.session.destroy(function(err) {
if (err) {
res.send('error');
} else {
res.send('good bye');
}
});
}
});
server.listen(8000, () => console.log('\n=== api running on 8k ===\n'));
const mongoose = require('mongoose');
const bcrypt = require('bcrypt'); //< ===========
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
lowercase: true, // Kyle => kyle
},
password: {
type: String,
required: true,
},
});
userSchema.pre('save', function(next) {
bcrypt.hash(this.password, 11, (err, hash) => {
if (err) {
return next(err);
}
this.password = hash;
return next(); // goes on to save to the db
});
});
userSchema.methods.isPasswordValid = function(passwordGuess) {
return bcrypt.compare(passwordGuess, this.password);
};
// userSchema.post('save', function(next) {
// console.log('post save hook');
// next();
// });
module.exports = mongoose.model('User', userSchema);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment