Token Authentication with JWT and Passport
Overview
Securing your API is an important step. When we were using Express to serve view templates we used PassportJS along with a username and password to authenticate users, but that is not the only way to secure an Express app, and in the context of an API it often makes sense to use a different strategy. The username and password session pattern that we learned previously will still work of course, though it is made a little more complicated by the fact that we’ve separated our front-end code from the back-end.
Another strategy is to generate and pass a secure token between our back-end and front-end code. Doing so will make sure that our user’s username and password are not compromised and will also give us the ability to expire our user’s session for added security. The basic idea is that when a user signs in to our app, a secure token is created, and then for all subsequent requests that token is passed in the header of our request object. In the end, the process is pretty simple since you should already be pretty comfortable with using passport to authenticate users.
This strategy, while particularly useful with APIs can be used with a traditional view-template project as well. The main difference here is that instead of setting and checking a cookie we’re passing a special token in the header of our request. In our previous Authentication Tutorial, the Passport middleware checked the cookie that was sent and then either authenticated or denied our user. In this case, we’re going to do something very similar, but instead of using cookies, we’re going to pass the token.
Token authentication vs session based authentication
In session based authentication, the server creates a session for the user after the user logs in. The session id is then stored on a cookie on the user’s browser. Whenever the user makes a request, the cookie is sent along with the request. The server then validates the session id and provides the response.
In token authentication, the server generates a token (usually a JSON Web Token) for the user after the user logs in. The token is then stored on the user’s browser, either in local storage or session storage. Whenever the user makes a request, the token is sent in the authorization header of the request. The server then verifies the token and provides the response.
The main advantages of token authentication over session based authentication are:
- Statelessness : The server does not need to store any information about the user’s session. This makes the server more scalable and less prone to errors.
- Cross-domain / CORS : Cookies do not work well across different domains or with CORS (Cross-Origin Resource Sharing). Tokens can be easily sent across different domains or platforms, such as web, mobile, etc.
- Storage : Tokens are usually smaller than cookies and can store more information in themselves, such as user roles, permissions, etc.
JSON Web Tokens
A JSON Web Token (JWT) is a standard way of representing claims or information between two parties. A JWT consists of three parts: a header, a payload, and a signature. The header and the payload are JSON objects that are base64 encoded and separated by a dot. The signature is created by applying a cryptographic algorithm (such as HMAC or RSA) to the header and the payload using a secret key.
The header typically contains the following information:
- alg : The algorithm used to generate the signature.
- typ : The type of the token, usually JWT.
The payload typically contains the following information:
- iss : The issuer of the token, usually the server or the application.
- sub : The subject of the token, usually the user id or the user name.
- exp : The expiration time of the token, usually a Unix timestamp.
- iat : The issued at time of the token, usually a Unix timestamp.
- aud : The audience of the token, usually the server or the application.
- nbf : The not before time of the token, usually a Unix timestamp.
- jti : The unique identifier of the token, usually a random string.
The signature is used to verify the integrity and authenticity of the token. The signature is generated by concatenating the header and the payload with a dot and then applying the algorithm specified in the header using the secret key. The signature is then appended to the header and the payload with another dot.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNvcGlsb3QiLCJpYXQiOjE1MTYyMzkwMjJ9.0aQ8wXl4fZL7Z0n0g2xqyYs8w0kQ1Z0lQ6l9LZ6v0u0
You can decode and verify a JWT using this tool.
Authorization header
The authorization header is a standard HTTP header that is used to send credentials or tokens to the server. The authorization header has the following format:
Authorization: <type> <credentials>
The type is usually Bearer
for JWTs, and the credentials are the JWT itself. For example:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNvcGlsb3QiLCJpYXQiOjE1MTYyMzkwMjJ9.0aQ8wXl4fZL7Z0n0g2xqyYs8w0kQ1Z0lQ6l9LZ6v0u0
The server can then extract the token from the header and verify it using the secret key.
Signing and verifying tokens
To sign and verify tokens, we need to use a library that supports JWTs. One such library is jsonwebtoken, which is a Node.js module that provides methods for creating and validating JWTs.
To install jsonwebtoken, run the following command in your terminal:
npm install jsonwebtoken
To sign a token, we need to provide a payload object, a secret key, and optionally some options such as the algorithm, the expiration time, etc. For example:
const jwt = require('jsonwebtoken');
// create a payload object with some claims
const payload = {
sub: '1234567890',
name: 'Copilot',
iat: Date.now()
};
// create a secret key
const secret = 'mysecretkey';
// sign the token using the payload, the secret, and some options
const token = jwt.sign(payload, secret, {
algorithm: 'HS256', // use HMAC with SHA-256
expiresIn: '1h' // set the expiration time to 1 hour
});
// print the token
console.log(token);
To verify a token, we need to provide the token, the secret key, and optionally some options such as the algorithm, the audience, etc. For example:
const jwt = require('jsonwebtoken');
// get the token from the authorization header
const token = req.headers.authorization.split(' ')[1]; // this turns the string "Bearer <access_token>" into an array (seperates them based on ' ') and takes the second element which is the access token.
// get the secret key
const secret = 'mysecretkey';
// verify the token using the token, the secret, and some options
jwt.verify(token, secret, {
algorithms: ['HS256'], // use HMAC with SHA-256
audience: 'myapp' // check the audience of the token
}, (err, decoded) => {
if (err) {
// handle the error
console.error(err);
res.status(401).send('Invalid token');
} else {
// handle the success
console.log(decoded);
// proceed with the request
next();
}
});
Custom middleware to verify tokens
To verify tokens on a given route, we can write a custom middleware function that uses the jsonwebtoken module to validate the token and then either allow or deny the request. For example:
const jwt = require('jsonwebtoken');
// create a custom middleware function
const verifyToke n = (req, res, next) => {
// get the token from the authorization header
const token = req.headers.authorization.split(' ')[1];
// get the secret key
const secret = 'mysecretkey';
// verify the token using the token, the secret, and some options
jwt.verify(token, secret, {
algorithms: ['HS256'], // use HMAC with SHA-256
audience: 'myapp' // check the audience of the token
}, (err, decoded) => {
if (err) {
// handle the error
console.error(err);
res.status(401).send('Invalid token');
} else {
console.log(decoded);
// attach the decoded payload to the request object
req.user = decoded;
// proceed with the request
next();
}
}); };
// use the custom middleware on a protected route app.get(‘/api/secret’, verifyToken, (req, res) => { // send some secret data to the authenticated user res.json({ message: 'This is a secret message for ’ + req.user.name }); });
Token expiration with JWT
One of the benefits of using JWTs is that we can set an expiration time for the tokens, which adds an extra layer of security. The expiration time can be specified in the payload of the token, using the exp
claim. The value of the exp
claim is a Unix timestamp that represents the time when the token will expire. For example:
const payload = {
sub: '1234567890',
name: 'Copilot',
iat: Date.now(),
exp: Date.now() + 3600 * 1000 // expire in 1 hour
};
Alternatively, we can also specify the expiration time in the options of the jwt.sign
method, using the expiresIn
property. The value of the expiresIn
property can be a number of seconds or a string that represents a time span. For example:
const token = jwt.sign(payload, secret, {
algorithm: 'HS256',
expiresIn: '1h' // expire in 1 hour
});
When we verify a token, the jsonwebtoken module will automatically check the expiration time and throw an error if the token is expired. The error will have the name TokenExpiredError
and the message jwt expired
. We can handle this error in our custom middleware and send an appropriate response to the user. For example:
jwt.verify(token, secret, {
algorithms: ['HS256'],
audience: 'myapp'
}, (err, decoded) => {
if (err) {
// handle the error
console.error(err);
if (err.name === 'TokenExpiredError') {
// handle the token expiration
res.status(401).send('Token expired');
} else {
// handle other errors
res.status(401).send('Invalid token');
}
} else {
// handle the success
console.log(decoded);
// attach the decoded payload to the request object
req.user = decoded;
// proceed with the request
next();
}
});
PassportJS with JWT
PassportJS is a popular authentication middleware for Node.js that supports various strategies for authenticating users. We have already learned how to use PassportJS with the local strategy, which uses a username and password to authenticate users. However, PassportJS also supports other strategies, such as the JWT strategy, which uses a JSON Web Token to authenticate users.
To use PassportJS with the JWT strategy, we need to install two modules: passport and passport-jwt. The passport module provides the core functionality of PassportJS, and the passport-jwt module provides the JWT strategy. To install them, run the following command in your terminal:
npm install passport passport-jwt
To use PassportJS with the JWT strategy, we need to do the following steps:
- Import the modules and create an instance of PassportJS.
- Configure the JWT strategy with the secret key, the options, and the callback function.
- Initialize PassportJS and use it as a middleware.
- Use the JWT strategy to protect the routes that require authentication.
For example:
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
// create an instance of PassportJS
const passportInstance = passport();
// configure the JWT strategy with the secret key, the options, and the callback function
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // extract the token from the authorization header
secretOrKey: 'mysecretkey', // use the same secret key as before
algorithms: ['HS256'], // use the same algorithm as before
audience: 'myapp' // use the same audience as before
};
const jwtCallback = (payload, done) => {
// the payload is the decoded token
// the done function is a callback that accepts an error and a user object
// find the user by the id in the payload
User.findById(payload.sub, (err, user) => {
if (err) {
// handle the error
return done(err, false);
}
if (user) {
// handle the success
return done(null, user);
} else {
// handle the failure
return done(null, false);
}
});
};
// create a new instance of the JWT strategy
const jwtStrategy = new JwtStrategy(jwtOptions, jwtCallback);
// use the JWT strategy with PassportJS
passportInstance.use(jwtStrategy);
// initialize PassportJS and use it as a middleware
app.use(passportInstance.initialize());
// use the JWT strategy to protect the routes that require authentication
app.get('/api/secret', passportInstance.authenticate('jwt', { session: false }), (req, res) => {
// send some secret data to the authenticated user
res.json({
message: 'This is a secret message for ' + req.user.name
});
});
Summary
- Token authentication is a stateless and cross-domain authentication strategy that uses a secure token to authenticate users.
- JSON Web Tokens are a standard way of representing claims or information between two parties. They consist of a header, a payload, and a signature.
- Authorization header is a standard HTTP header that is used to send credentials or tokens to the server. The authorization header has the format
Authorization: <type> <credentials>
. - Signing and verifying tokens are the processes of creating and validating JWTs using a secret key and a cryptographic algorithm.
- Custom middleware to verify tokens is a function that uses the jsonwebtoken module to validate the token and then either allow or deny the request.
- Token expiration with JWT is a feature that allows us to set a time limit for the validity of the tokens, which adds an extra layer of security.
-
PassportJS with JWT is a way of using PassportJS with the JWT strategy to authenticate users using JWTs.
- Creating & Verifying JWTs:https://www.youtube.com/watch?v=7nafaH9SddU:https://www.youtube.com/watch?v=7nafaH9SddU
- JWT Advantages:https://www.youtube.com/watch?v=7Q17ubqLfaM:https://www.youtube.com/watch?v=7Q17ubqLfaM Video summary:
This video explains what JWT (JSON Web Token) is, why it is used for authorization, and how it works. It compares JWT with the traditional session-based authentication and shows the advantages of JWT in different scenarios.
Highlights:
- [00:00:17][^3^][3] JWT is for authorization, not authentication
- Authentication is verifying the user’s identity
- Authorization is verifying the user’s access to resources
- [00:01:39][^4^][4] Session-based authentication uses cookies to store session IDs
- The server stores the user information in memory
- The client sends the session ID with every request
- The server looks up the user based on the session ID
- [00:03:08][^5^][5] JWT-based authentication uses tokens to store user information
- The server creates a token with the user information and signs it with a secret key
- The client stores the token and sends it with every request
- The server verifies the token and decodes the user information
- [00:05:33][^6^][6] JWT has three parts: header, payload, and signature
- The header specifies the algorithm and the token type
- The payload contains the user information and other data
- The signature is the hashed version of the header and payload
- [00:09:01][^7^][7] JWT can prevent tampering by using the secret key
- The server can check if the token has been changed by the client
- The server can reject invalid tokens
- The server should keep the secret key safe and inaccessible
- [00:11:02][^8^][8] JWT can be used across multiple servers and applications
- The server does not need to store anything in memory
- The client can authenticate with any server that shares the same secret key
- The server can expire tokens by using issued at and expired at fields
- Official JWT Website:https://jwt.io/:https://jwt.io/
- In-depth JWT Authentication:https://web.archive.org/web/20230207144457/https://laptrinhx.com/a-practical-guide-for-jwt-authentication-using-node-js-and-express-917791379/:https://web.archive.org/web/20230207144457/https://laptrinhx.com/a-practical-guide-for-jwt-authentication-using-node-js-and-express-917791379/
- Concise JWT Authentication:https://medium.com/@paul.allies/stateless-auth-with-express-passport-jwt-7a55ffae0a5c:https://medium.com/@paul.allies/stateless-auth-with-express-passport-jwt-7a55ffae0a5c
- Considerations before using JWTs:https://www.youtube.com/watch?v=JdGOb7AxUo0:https://www.youtube.com/watch?v=JdGOb7AxUo0