Building APIs is important; however, building secured APIs is also very important. In this tutorial, you learn how to build a secured API in Node.js. The authentication will be handled using tokens. Let’s dive in.
Cookies vs. Tokens
Let’s talk a little about cookies and tokens, to understand how they work and the differences between them.
Cookie-based authentication keeps an authentication record or session both on the server and the client side. In other words, it is stateful. The server keeps track of an active session in the database, while a cookie is created on the front-end to hold a session identifier.
In token-based authentication, every server request is accompanied by a token which is used by the server to verify the authenticity of the request. The server does not keep a record of which users are logged in. Token-based authentication is stateless.
In this tutorial, you will implement token-based authentication.
Setting Up an Express App
First, create a folder where you will be working from, and initialize npm. You can do so by running:
npm init -y
Use the package.json file I have below. It contains all the dependencies you will be making use of to build this application.
#package.json { "name": "api-auth", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start-dev": "nodemon app.js", "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.17.2", "express": "^4.15.4", "express-promise-router": "^2.0.0", "joi": "^10.6.0", "jsonwebtoken": "^8.0.0", "mongoose": "^4.11.10", "morgan": "^1.8.2", "nodemon": "^1.12.0", "passport": "^0.4.0", "passport-jwt": "^3.0.0", "passport-local": "^1.0.0" } }
Now you can install the dependencies by running:
npm install
You can go grab a cup of coffee as npm does its magic; you’ll need it for this ride!
Create a new file called app.js. In this file, you will require some packages and do a bit of setup. Here is how it should look.
#app.js // 1 const express = require('express') const morgan = require('morgan') const bodyParser = require('body-parser') const mongoose = require('mongoose') const routes = require('./routes/users') // 2 mongoose.Promise = global.Promise mongoose.connect('mongodb://localhost:27017/api-auth') const app = express() // 3 app.use(morgan('dev')) app.use(bodyParser.json()) // 4 app.use('/users', routes) // 5 const port = process.env.PORT || 3000 app.listen(port) console.log(`Server listening at ${port}`)
- This imports all dependencies you installed using NPM. Here you are requiring express, morgan, body-parser, and mongoose. Morgan is an HTTP logger for Node.js. Here, morgan will be used to log HTTP requests to your console. So you see something like this when requests are made:
POST /users/
signin
401 183.635 ms - -
. You also require your routes (which will be created soon) and set it to routes. - Here you are setting mongoose to make use of ES6 promises.
- Set up morgan and body-parser middleware. Body-parser makes incoming requests available in your req.body property.
- Set up routes. With this, whenever a request is made to /users, the routes file which you required above and set to routes will be used.
- You set port to the port of your environment or 3000 and then pass it to the listen function which is called on the app to start up your server. When the server starts, a log is sent to the console to show that the server has started.
It’s time to get our model up and running.
Map Out the User Model
Your User model will do a few things:
- Create a user schema.
- Hash the user password.
- Validate the password.
Time to see what that should look like in real code. Create a folder called models, and in the folder create a new file called user.js. The first thing you want to do is require the dependencies you will be making use of.
The dependencies you will need here include mongoose and bcryptjs.
You will need bcryptjs for the hashing of a user password. We will come to that, but for now just require the dependencies like so.
#models/user.js const mongoose = require('mongoose') const bcrypt = require('bcryptjs')
With that out of the way, you want to create your user schema. This is where mongoose comes in handy. Your schema will map to a MongoDB collection, defining how each document within the collection should be shaped. Here is how the schema for your current model should be structured.
#models/user.js ... const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, lowercase: true }, password: { type: String, required: true } })
In the above, you have just two fields: email and password. You are making both fields a type of String, and requiring them. The email also has an option of unique
, as you will not want to different users signing up with the same email, and it is also important that the email is in lowercase.
The next part of your model will handle the hashing of the user password.
#models/user.js ... userSchema.pre('save', async function (next) { try { const salt = await bcrypt.genSalt(10) const passwordhash = await bcrypt.hash(this.password, salt) this.password = passwordhash next() } catch (error) { next(error) } })
The code above makes of bcryptjs to generate a salt. You also use bcryptjs to hash the user password by passing in the user’s password and the salt you just generated. The newly hashed password gets stored as the password for the user. The pre hook function ensures this happens before a new record of the user is saved to the database. When the password has been successfully hashed, control is handed to the next middleware using the next()
function, else an error is thrown.
Finally, you need to validate user password. This comes in handy when a user wants to sign in, unlike the above which happens for signing up.
In this part, you need to ensure a user who wants to sign in is rightly authenticated. So you create an instance method called isValidPassword
()
. In this method, you pass in the password the user entered and compare it with the password attributed to that user. The user is found using the email address s/he entered; you will see how this is handled soon. A new error is thrown if one is encountered.
#models/user.js ... userSchema.methods.isValidPassword = async function (newPassword) { try { return await bcrypt.compare(newPassword, this.password) } catch (error) { throw new Error(error) } } module.exports = mongoose.model('User', userSchema)
When that is all set and done, it is important you export the file as a module so it can be rightly required.
Up and Running With Passport
Passport provides a simple way to authenticate requests in Node.js. These requests go through what are called strategies. In this tutorial, you will implement two strategies.
Go ahead and create a file called passport.js in your working directory. Here is what mine looks like.
#passport.js // 1 const passport = require('passport') const JwtStrategy = require('passport-jwt').Strategy const { ExtractJwt } = require('passport-jwt') const LocalStrategy = require('passport-local').Strategy const { JWT_SECRET } = require('./config/index') const User = require('./models/user') // 2 passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromHeader('authorization'), secretOrKey: JWT_SECRET }, async (payload, done) => { try { const user = User.findById(payload.sub) if (!user) { return done(null, false) } done(null, user) } catch(err) { done(err, false) } })) // 3 passport.use(new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => { try { const user = await User.findOne({ email }) if (!user) { return done(null, false) } const isMatch = await user.isValidPassword(password) if (!isMatch) { return done(null, false) } done(null, user) } catch (error) { done(error, false) } }))
- Require dependencies installed using NPM.
- Creates a new instance of
JwtStrategy
. This object receives two important arguments:secretOrKey
andjwtFromRequest
. ThesecretOrKey
will be the JWT secret key which you will create shortly.jwtFromRequest
defines the token that will be sent in the request. In the above, the token will be extracted from the header of the request. Next, you do a validation to look for the right user, and an error gets thrown if the user is not found. - Create a new instance of
LocalStrategy
. By default, the LocalStrategy uses only the username and password parameters. Since you are making use of email, you have to set theusernameField
to email. You use the email to find the user, and when the user is found, theisValidPassword
()
function which you created in the user model gets called. This method compares the password entered with the one stored in the database. The comparison is done using the bcryptjs compared method. Errors are thrown if any is encountered, else the user is returned.
Before you jump to setting your controllers and routes, you need to create the JWT secret key mentioned above. Create a new folder called config, and a file inside called index.js. You can make the file look like this.
#config/index.js module.exports = { JWT_SECRET: 'apiauthentication' }
User Controllers
The user controller gets called whenever a request is made to your routes. Here is how it should look.
#controllers/users.js // 1 const JWT = require('jsonwebtoken') const User = require('../models/user') const { JWT_SECRET } = require('../config/index') // 2 signToken = ((user) => { return JWT.sign({ iss: 'ApiAuth', sub: user.id, iat: new Date().getTime(), exp: new Date().setDate(new Date().getDate() + 1) }, JWT_SECRET) }) module.exports = { // 3 signup: async (req, res, next) => { console.log('UsersController.signup() called') const { email, password } = req.value.body const foundUser = await User.findOne({ email }) if (foundUser) { return res.status(403).json({ error: 'Email is already in use' }) } const newUser = new User({ email, password }) await newUser.save() const token = signToken(newUser) res.status(200).json({ token }) }, // 4 signin: async (req, res, next) => { const token = signToken(req.user) res.status(200).json({ token }) }, // 5 secret: async (req, res, next) => { res.json({ secret: "resource" }); } }
- Require dependencies.
- Here you are creating a new token for the user. To create the token, you need to create the payload, which contains the claims. Here you set the issuer, subject, expiration, and issued at. The creation of this token is saved in a function called
signToken
()
. - This is the sign-up action that gets called when creating a new user. The email and password are obtained from the body of the request. You need to check if a user with the same email exists, to ensure two users are not signed up with same email address. When that has been verified, the
signToken
()
function gets called with the new user as an argument to create a new token. - This action handles the signing in of users. Here you simply pass in the user who is trying to sign in to the
signToken
()
function that creates a new token. - You are using this function for a secret route that can only be accessed by users who have been authenticated.
Setting Up Routes
Before you create the main routes for your application, you will first create a route helper. Go ahead and create a new folder called helpers, and a file called routeHelpers.js. Here is how it should look.
#helpers/routeHelpers.js const Joi = require('joi') module.exports = { validateBody: (schema) => { return (req, res, next) => { const result = Joi.validate(req.body, schema) if (result.error) { return res.status(400).json(result.error) } if (!req.value) { req.value = {} } req.value['body'] = result.value next() } }, schemas: { authSchema: Joi.object().keys({ email: Joi.string().email().required(), password: Joi.string().required() }) } }
Here you are validating the body of requests sent by users. This will ensure that users enter the right details when signing up or in. The details to be validated are obtained from req.body
.
The routes of your application will take this format.
#routes/users.js const express = require('express') const router = require('express-promise-router')() const passport = require('passport') const passportConf = require('../passport') const { validateBody, schemas } = require('../helpers/routeHelpers') const UsersController = require('../controllers/users') router.route('/signup') .post(validateBody(schemas.authSchema), UsersController.signup) router.route('/signin') .post(validateBody(schemas.authSchema), passport.authenticate('local', { session: false }), UsersController.signin) router.route('/secret') .get(passport.authenticate('jwt', { session: false }), UsersController.secret) module.exports = router
Save it in routes/users.js. In the above, you are authenticating each request, and then calling the respective controller action.
Now you can start up your server by running:
npm run start-dev
Open up postman and play around with your newly created API.
Conclusion
You can go further to integrate new features to the API. A simple Book API with CRUD actions with routes that need to be authenticated will do. In the tutorial, you learned how to build a fully fledged authenticated API.
If you’re looking for additional JavaScript resources to study or to use in your work, check out what we have available in the Envato marketplace.
Powered by WPeMatico