Module 1: Introduction to Authentication
What is Authentication
Authentication is the process by which our Web API verifies a client's identity that is trying to access a resource. This is different from authorization, which comes after authentication and determines what type of access a user should have.
Adding authentication to a Web API requires that an API can:
- Register user accounts.
- Login to prove identity.
- Logout of the system to invalidate the user's access until they log in again.
- Add a way for users to reset their passwords.
Proper authentication is difficult. There is a constant race between security experts with innovative ways to protect our information and attackers coming up with ways to circumvent those security measures.
Some of the things we need to take into account when implementing authentication are:
- Password storage.
- Password strength.
- Brute-force safeguards.
How to Build It
Let's tackle the first one, password storage. The rule of thumb is: NEVER, EVER, under no circumstances, store user passwords in plain text. Then what are the two main options:
- encryption.
- hashing.
Password Hashing vs. Encryption for password storage
- Encryption goes two ways. First, it utilizes plain text and private keys to generate encrypted passwords and then reverses the process to match an original password.
- Cryptographic hashes only go one way: parameters + input = hash. It is pure; given the same parameters and input, it generates the same hash.
Suppose the database of users and keys is compromised. In that case, it is possible to decrypt the passwords to their original values, which is bad because users often share passwords across different sites. This is one reason why cryptographic hashing is the preferred method for storing user passwords.
Password Strength
Password length alone is not enough to slow password guessing, but long passwords are generally better than short, complicated ones. It is a trade-off between convenience and security.
Visit this site to see how a combination of password length and complexity affects an attacker's ability to pre-generate password hashes.
Brute-Force Attack Mitigation
A common way that attackers circumvent hashing algorithms is by pre-calculating hashes for all possible character combinations up to a particular length using common hashing techniques. The results of said calculations are stored in a database table known as a rainbow table. Whenever there is a breach, the attacker checks every breached password against their table.
Which Cryptographic Hashing Algorithm should we use? MD5, SHA-1, SHA-2, SHA-3? None of these, because they are flawed, these algorithms are optimized for speed, not security.
We aim to slow down hackers' ability to get at a user's password. To do so, we will add time to our security algorithm to produce what is known as a key derivation function.
[Hash] + [Time] = [Key Derivation Function].
In the next section, we'll learn how to use a popular Key Derivation library to store user passwords safely.
Hashing Passwords
Instead of writing our key derivation function (fancy name for hashing function), we'll use a well-known and popular module called bcryptjs.
Bcryptjs features include:
- password hashing function
- implements salting both manually and automatically
- accumulative hashing rounds
Having an algorithm that hashes the information multiple times (rounds) means an attacker needs to have the hash, know the algorithm used, and how many rounds were used to generate the hash in the first place.
How to Build It
Follow these steps to use bcrypt in your project.
- Install bcryptjs using npm.
- Import it into your server.
const bcrypt = require('bcryptjs');
To hash a password:
const credentials = req.body;
const hash = bcrypt.hashSync(credentials.password, 14);
credentials.password = hash;
// move on to save the user.
To verify a password:
const credentials = req.body;
// find the user in the database by it's username then
if (!user || !bcrypt.compareSync(credentials.password, user.password)) {
return res.status(401).json({ error: 'Incorrect credentials' });
}
// the user is valid, continue on
Verifying passwords with Bcrypt
Use bcrypt.compareSync(), passing the password guess in plain text and the password hash from the database to validate credentials.
If the password guess is valid, the method returns true. Otherwise, it returns false. This is because the library hashes the password guess first and then compares the hashes.
How to Build It
Let's see an example.
server.post('/api/login', (req, res) => {
let { username, password } = req.body;
Users.findBy({ username })
.first()
.then(user => {
// check that passwords match
if (user && bcrypt.compareSync(password, user.password)) {
res.status(200).json({ message: `Welcome ${user.username}!` });
} else {
// we will return 401 if the password or username are invalid
// we don't want to let attackers know when they have a good username
res.status(401).json({ message: 'Invalid Credentials' });
}
})
.catch(error => {
res.status(500).json(error);
});
});
In-memory sessions
Sessions provide a way to persist data across requests. For example, we'll use them to save authentication information, so there is no need to re-enter credentials on every new request the client makes to the server.
When using sessions, each client will have a unique session stored on the server.
Now that we have a solution for keeping authentication information, we need to transmit that information between the client and server. For that, we'll use cookies.
Authentication Workflow for sessions
The basic workflow when using a combination of cookies and sessions for authentication is:
- Client sends credentials
- Server verifies credentials
- Server creates a session for the client
- Server produces and sends back a cookie
- Client stores the cookie
- Client sends the cookie on every request
- Server verifies that the cookie is valid
- Server provides access to resource
To understand how cookies are transmitted and stored in the browser, we need to look at the basic structure of an HTTP message. Every HTTP message, be it a request or a response, has two main parts: the headers and the body.
The headers are a set of key/value stores that include information about the request. There are several standard headers, but we can add our own if needed.
To send cookies, the server adds the Set-Cookie header to the response like so: "Set-Cookie": "session=12345". Notice how the value of a header is just a string. The browser will read the header and save a cookie called session with the value 12345 in this example. We will use a library that takes care of creating and sending the cookie.
The body contains the data portion of the message.
The browser will add the "Cookie": "session=12345" header on every subsequent request and the server.
Cookies are not accessible from JavaScript or anywhere because they are cryptographically signed and very secure.
There are sever libraries for handling sessions in Node.js, below are two examples:
We will use the latter.
Common Ways to Store Session Data on the Server:
- Memory
- Memory cache (like Redis and Memcached)
- Database
Cookies
- Automatically included on every request
- Unique to each domain + device pair
- Cannot be sent to a different domain
- Sent in the cookie header
- Has a body that can have extra identifying information
- Max size around 4KB
Storing Session Data in Memory
- Data stored in memory is wiped when the server restarts
- Causes memory leaks as more memory is used as the application continues to store data in session for different clients
- Good for development due to its simplicity
Using cookies to transfer session data
Advantages when using cookies:
- A cookie is a small key/value pair data structure passed back and forth between client and server and stored in the browser
- The server uses 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
- Send in every request, increasing the request size 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 many applications
- Automatically remove old session data
Drawbacks:
- 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
- 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
- It causes chatter between the server and the database
- Need to manage/remove old sessions manually, or the database will be filled with unused session data. Most libraries now manage this for you
Here is a list of express-session compatible stores
How to Build It
Let's add session support to our Web API:
const session = require('express-session');
// configure express-session middleware
server.use(
session({
name: 'notsession', // default is connect.sid
secret: 'nobody tosses a dwarf!',
cookie: {
maxAge: 1 * 24 * 60 * 60 * 1000,
secure: true, // only set cookies over https. Server will not send back a cookie over http.
}, // 1 day in milliseconds
httpOnly: true, // don't let JS code access cookies. Browser extensions run JS code on your browser!
resave: false,
saveUninitialized: false,
})
);
The resave option forces the session to be saved back to the session store, even if the session wasn't modified during the request.
The saveUninitialized flag, forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie.
Now we can store session data in one route handler and read it in another.
app.get('/', (req, res) => {
req.session.name = 'Frodo';
res.send('got it');
});
app.get('/greet', (req, res) => {
const name = req.session.name;
res.send(`hello ${req.session.name}`);
});
The server sends back a session id with every response, and the client then sends back that session id on every request.
express-session uses in-memory storage by default.
Note how we generalize the session cookie's name, to make it harder for attackers to know which library we're using to manage our sessions. This is akin to using a helmet to hide the X-Powered-By=Express header.
Implement Logout
Sessions remain active until they reach the expiration time configured when they were created, but we need to invalidate the session immediately when a user logs out.
We can do this by removing the session from our session store. Each library does it differently.
How to Build It
Here's how to implement a logout endpoint using express-session:
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');
}
});
}
});
The destroy() method removes the session from the store and eliminates the req.session property. This effectively logs out the user by invalidating their session.
Protecting Routes
Restricting access to endpoints is a two-step process:
- We write middleware to check that there is a session for the client.
- We place that middleware in front of the endpoints we want to restrict.
How to Build It
We'll start by writing a piece of middleware we can use locally to restrict access to protected routes.
function protected(req, res, next) {
if (req.session && req.session.userId) {
next();
} else {
res.status(401).json({ message: 'you shall not pass!!' });
}
}
This middleware verifies that we have a session and that the userId is set. We could use username or any other value to verify access to a resource.
Then, we add that middleware to the endpoints we'd like to protect.
server.get('/api/users', protected, (req, res) => {
db('users')
.then(users => res.json(users))
.catch(err => res.json(err));
});
The /api/users endpoint is only accessible when the client is logged in.
Guided Project
Intro to Authentication Starter Code
Intro to Authentication Solution
NOTE
The versions of project dependencies used in the recording are slightly different from the ones used in the starter and solution repositories, but this should not have any impact on the relevant code of the Guided Project.
The versions used in the repositories are more recent, and thus more similar to the versions you will install if you create a project from scratch.
Module 1 Project: Authentication
The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.
Instructions
The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:
Starter Repo: Node Authentication
- Fork the repository,
- clone it to your machine, and
- open the README.md file in VSCode, where you will find instructions on completing this Project.
- submit your completed project to the BloomTech Portal