Many web applications and APIs use a form of authentication to protect resources and restrict their access only to verified users.
JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
This guide will walk you through how to implement authentication for an API using JWTs and Passport, an authentication middleware for Node.
Here a brief overview of the application you will be building:
To complete this tutorial, you will need:
This tutorial was verified with Node v14.2.0, npm
v6.14.5, and mongodb-community
v4.2.6.
Let’s start by setting up the project. In your terminal window, create a directory for the project:
- mkdir jwt-and-passport-auth
And navigate to that new directory:
- cd jwt-and-passport-auth
Next, initialize a new package.json
:
- npm init -y
Install the project dependencies:
- npm install --save bcrypt@4.0.1 body-parser@1.19.0 express@4.17.1 jsonwebtoken@8.5.1 mongoose@5.9.15 passport@0.4.1 passport-jwt@4.0.0 passport-local@1.0.0
You will need bcrypt
for hashing user passwords, jsonwebtoken
for signing tokens, passport-local
for implementing local strategy, and passport-jwt
for retrieving and verifying JWTs.
Warning: When running install, you may encounter issues with bcrypt
depending on the version of Node you are running.
Refer to the README to determine compatibility with your environment.
At this point, your project has been initialized and all the dependencies have been installed. Next, you will be adding a database to store user information.
A database schema establishes the types of data and structure of the database. Your database will require a schema for users.
Create a model
directory:
- mkdir model
Create a model.js
file in this new directory:
- nano model/model.js
The mongoose
library is used to define a schema that is mapped to a MongoDB collection. In the schema, an email and password will be required for a user. The mongoose
library takes the schema and converts it into a model:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
const UserModel = mongoose.model('user', UserSchema);
module.exports = UserModel;
You should avoid storing passwords in plain text because if an attacker manages to get access to the database, the passwords can be read.
To avoid this, you will use a package called bcrypt
to hash user passwords and store them safely. Add the library and the following lines of code:
// ...
const bcrypt = require('bcrypt');
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
'save',
async function(next) {
const user = this;
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
next();
}
);
// ...
module.exports = UserModel;
The code in the UserScheme.pre()
function is called a pre-hook. Before the user information is saved in the database, this function will be called, you will get the plain text password, hash it, and store it.
this
refers to the current document about to be saved.
await bcrypt.hash(this.password, 10)
passes the password and the value of salt round (or cost) to 10
. A higher cost will run the hashing for more iterations and be more secure. It has a trade-off of being more computationally intensive to the point that it may impact your application’s performance.
Next, you replace the plain text password with the hash and then store it.
Finally, you indicate you are done and should move on to the next middleware with next()
.
You will also need to make sure that the user trying to log in has the correct credentials. Add the following new method:
// ...
const UserSchema = new Schema({
// ...
});
UserSchema.pre(
// ...
});
UserSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
// ...
module.exports = UserModel;
bcrypt
hashes the password sent by the user for login and checks if the hashed password stored in the database matches the one sent. It will return true
if there is a match. Otherwise, it will return false
if there is not a match.
At this point, you have a schema and model defined for your MongoDB collection.
Passport is an authentication middleware used to authenticate requests.
It allows developers to use different strategies for authenticating users, such as using a local database or connecting to social networks through APIs.
In this step, you’ll be using the local (email and password) strategy.
You will use the passport-local
strategy to create middleware that will handle user registration and login. This will then be plugged into certain routes and be used for authentication.
Create an auth
directory:
- mkdir auth
Create an auth.js
file in this new directory:
- nano auth/auth.js
Start by requiring passport
, passport-local
, and the UserModel
that was created in the previous step:
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const UserModel = require('../model/model');
First, add a Passport middleware to handle user registration:
// ...
passport.use(
'signup',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.create({ email, password });
return done(null, user);
} catch (error) {
done(error);
}
}
)
);
This code saves the information provided by the user to the database, and then sends the user information to the next middleware if successful.
Otherwise, it reports an error.
Next, add a Passport middleware to handle user login:
// ...
passport.use(
'login',
new localStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}
)
);
This code finds one user associated with the email provided.
"User not found"
error."Wrong Password"
error."Logged in Successfully"
message, and the user information is sent to the next middleware.Otherwise, it reports an error.
At this point, you have a middleware for handling signing up and logging in.
Express is a web framework that provides routing. In this step, you will create a route for a signup
endpoint.
Create a routes
directory:
- mkdir routes
Create a routes.js
file in this new directory:
- nano routes/routes.js
Start by requiring express
and passport
:
const express = require('express');
const passport = require('passport');
const router = express.Router();
module.exports = router;
Next, add handling of a POST request for signup
:
// ...
const router = express.Router();
router.post(
'/signup',
passport.authenticate('signup', { session: false }),
async (req, res, next) => {
res.json({
message: 'Signup successful',
user: req.user
});
}
);
module.exports = router;
When the user sends a POST request to this route, Passport authenticates the user based on the middleware created previously.
You now have a signup
endpoint. Next, you will need a login
endpoint.
When the user logs in, the user information is passed to your custom callback, which in turn creates a secure token with the information.
In this step, you will create a route for a login
endpoint.
First, require jsonwebtoken
:
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
// ...
Next, add handling of a POST request for login
:
// ...
const router = express.Router();
// ...
router.post(
'/login',
async (req, res, next) => {
passport.authenticate(
'login',
async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An error occurred.');
return next(error);
}
req.login(
user,
{ session: false },
async (error) => {
if (error) return next(error);
const body = { _id: user._id, email: user.email };
const token = jwt.sign({ user: body }, 'TOP_SECRET');
return res.json({ token });
}
);
} catch (error) {
return next(error);
}
}
)(req, res, next);
}
);
module.exports = router;
You should not store sensitive information such as the user’s password in the token.
You store the id
and email
in the payload of the JWT. You then sign the token with a secret or key (TOP_SECRET
). Finally, you send back the token to the user.
Note: You set { session: false }
because you do not want to store the user details in a session. You expect the user to send the token on each request to the secure routes.
This is especially useful for APIs, but it is not a recommended approach for web applications for performance reasons.
You now have a login
endpoint. A successfully logged in user will generate a token. However, your application does not do anything with the token yet.
So now you’ve handled user signup and login, the next step is allowing users with tokens access to certain secure routes.
In this step, you will verify that the tokens haven’t been manipulated and are valid.
Revisit the auth.js
file:
- nano auth/auth.js
Add the following lines of code:
// ...
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(
new JWTstrategy(
{
secretOrKey: 'TOP_SECRET',
jwtFromRequest: ExtractJWT.fromUrlQueryParameter('secret_token')
},
async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}
)
);
This code uses passport-jwt
to extract the JWT from the query parameter. It then verifies that this token has been signed with the secret or key set during logging in (TOP_SECRET
). If the token is valid, the user details are passed to the next middleware.
Note: If you will need extra or sensitive details about the user that are not available in the token, you could use the _id
available on the token to retrieve them from the database.
Your application is now capable of both signing tokens and verifying them.
Now, let’s create some secure routes that only users with verified tokens can access.
Create a new secure-routes.js
file:
- nano routes/secure-routes.js
Next, add the following lines of code:
const express = require('express');
const router = express.Router();
router.get(
'/profile',
(req, res, next) => {
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.secret_token
})
}
);
module.exports = router;
This code handles a GET request for profile
. It returns a "You made it to the secure route"
message. It also returns information about the user
and token
.
The goal will be so that only users with a verified token will be presented with this response.
So now that you’re all done with creating the routes and authentication middleware, you can put everything together.
Create a new app.js
file:
- nano app.js
Next, add the following code:
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');
const UserModel = require('./model/model');
mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;
require('./auth/auth');
const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/', routes);
// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);
// Handle errors.
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.json({ error: err });
});
app.listen(3000, () => {
console.log('Server started.')
});
Note: Depending on your version of mongoose
, you may encounter the following message: WARNING: The 'useMongoClient' option is no longer necessary in mongoose 5.x, please remove it.
.
You may also encounter deprecation notices for useNewUrlParser
, useUnifiedTopology
, and ensureIndex
(createIndexes
).
During troubleshooting, we were able to resolve these by modifying the mongoose.connect
method call and adding a mongoose.set
method call:
mongoose.connect("mongodb://127.0.0.1:27017/passport-jwt", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoose.set("useCreateIndex", true);
Run your application with the following command:
- node app.js
You will see a "Server started."
message. Leave the application running to test it.
Now that you’ve put everything together, you can use Postman to test your API authentication.
Note: If you need assistance navigating the Postman interface for requests, consult the official documentation.
First, you will have to register a new user in your application with an email and password.
In Postman, set up the request to the signup
endpoint you created in routes.js
:
POST localhost:3000/signup
Body
x-www-form-urlencoded
And send over these details through the Body
of your request:
Key | Value |
---|---|
example@example.com |
|
password | password |
When that’s done, click the Send button to initiate the POST
request:
Output{
"message": "Signup successful",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "example@example.com",
"password": "[a long string of characters representing an encrypted password]",
"__v": 0
}
}
Your password displays as an encrypted string because this is how it is stored in the database. This is a result of the pre-hook you wrote in model.js
to use bcrypt
to hash the password.
Now, log in with the credentials and get your token.
In Postman, set up the request to the login
endpoint you created in routes.js
:
POST localhost:3000/login
Body
x-www-form-urlencoded
And send over these details through the Body
of your request:
Key | Value |
---|---|
example@example.com |
|
password | password |
When that’s done, click the Send button to initiate the POST
request:
Output{
"token": "[a long string of characters representing a token]"
}
Now that you have your token, you will send over this token whenever you want to access a secure route. Copy and paste it for later use.
You can test how your application handles verifying tokens by accessing /user/profile
.
In Postman, set up the request to the profile
endpoint you created in secure-routes.js
:
GET localhost:3000/user/profile
Params
And pass your token in a query parameter called secret_token
:
Key | Value |
---|---|
secret_token | [a long string of characters representing a token] |
When that’s done, click the Send button to initiate the GET
request:
Output{
"message": "You made it to the secure route",
"user": {
"_id": "[a long string of characters representing a unique id]",
"email": "example@example.com"
},
"token": "[a long string of characters representing a token]"
}
The token will be collected and verified. If the token is valid, you’ll be given access to the secure route. This is a result of the response you created in secure-routes.js
.
You can also try accessing this route, but with an invalid token, the request will return an Unauthorized
error.
In this tutorial, you set up API authentication with JWT and tested it with Postman.
JSON web tokens provide a secure way of creating authentication for APIs. An extra layer of security can be added by encrypting all the information within the token, thereby making it even more secure.
If you would like more in-depth knowledge of JWTs, you can use these extra resources:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
it would be great having the same example but applied on Digital Ocean Functions (functions acting as microservices). Thanks
Syntax highlighting words BUBBLES is really hard to read.
Example of such code in this articles is ‘auth/auth.js’
My suggestion is to remove or alter the BUBBLE styling.
<3
After this, do I need to store this token in a cookie to build my web app?
Registration works fine. But when I try to post a login request to localhost:3000/login - I get this error in postman.
500Internal Server Error
For some reason
ExtractJWT.fromUrlQueryParameter('secret_token')
kept returning a 401 error. I had to change it toExtractJWT.fromAuthHeaderAsBearerToken()
Thank you so much I learned a lot from this guide.
The app.js file needs to contain
app.use(passport.initialize());
Without it the passport module won’t function correctly…
What does the call to req.login() in the login route do? The code seems to work fine without it.
Why we should not store sensitive information such as the user’s password in the token while it’s fine to send sensitive information back in the response of signup.
I got through the entire implementation and did everything the way it was laid out. I get a 404 error.