MEAN Tutorial Part 3 - Registration and Authorization

in #utopian-io7 years ago (edited)

logo_part3.png

This tutorial series shows how to build web applications with the MEAN stack. The MEAN stack is MongoDB, Express.js, Angular, and Node.js. This stack allows to write applications, where Javascript can be used both for the client and the server part.
In this tutorial a simple webapplication to manage portfolios of cryptocurrencies such as Bitcoin or Ethereum will be built.

What will I learn?

In this part of the tutorial series

  • you will learn to setup routes to register and authenticate users,
  • you will learn how to use token based authentication to secure your routes,
  • and you will learn how to test your routes without a frontend.

Requirements

For this part of the tutorial you need the following background knowledge:

  • Previous parts of this series
  • Working with the command line
  • Experience in JavaScript

Difficulty

Intermediate.

Tutorial contents

This part of the tutorial shows how to extend the server framework created in the previous parts of this series with registration of new users and authorization of users that are already registered. In the last part of the tutorial, we already wrote all code necessary to add users to the database and to retrieve them by their email. In this lesson I will show how to create routes with Express.js and how to use token based authentication to secure routes. We will use Postman to test our routes.

Register a middleware to parse JSON bodies

All messages to communicate between frontend and backend will be encoded in JSON, which is human-readable, easy to write and compact. For the automatic conversion of JavaScript objects from and to JSON in the bodies of our HTTP requests, we use the body-parser middleware with Express.js. First install it with

npm install body-parser@1 --save

Then go to app.js and add it to the list of imports at the top of the file with

const bodyparser = require("body-parser");

Now register it as middleware with the app object after its creation:

app.use(bodyparser.json());

If you are unsure, whether you put everything at the correct location, you can always lookup the full source code, which you can find on Github (you find the link at the end of each tutorial). If you remember the first lesson, you may know, that the callback functions registered with Express.js routes receive three parameters. The first parameter, typically named req represents the incoming HTTP request. body-parser automatically parses the the body of the incoming HTTP requests and attaches the result to reqbody. E.g. if the body of the incoming message is

{
  "foo" : bar,
  "amount" : 1.0
}

you can directly access the values via req.body.foo and req.body.amount.

Add a route to register users.

As with the user model in the previous part of this series, we put all routes in a separate directory in our project. Create a folder routes and a new file users.js in it. We need access to the Express.js router and our user model, so import them:

const router = (module.exports = require("express").Router());
const User = require("../models/user.js");

The router for the user endpoints is exported, because we need it in app.js. So go to app.js and import the user routes with

const userroutes = require("./routes/users.js");

Now register it with the app object:

app.use("/users", userroutes);

Back to routes/users.js we start with the route for the /register endpoint:

router.post("/register", async (req, res, next) => {
  let newuser = new User({
    email: req.body.email,
    password: req.body.password
  });

The route expects a JSON document with the fields email and password. With these, a new instance of the user model is created. This instance can now be passed to User.addUser. This is an async function, which may return error, so it needs to be wrapped in a try/catch block. Since we made the email field of our user model unique, an error will occur if there is already a user with the same email address in the database. Either way, the callback function of the route should send a response at some point, otherwise the client will wait forever and eventually report a timeout. There are various ways to send a response, but since we decided to use JSON as serialization format for server/client communication, the json() method is used. This method sends the passed javascript object as JSON in the body of the response.

  try {
    let added_user = await User.addUser(newuser);
    if (added_user) {
      res.json({
        success: true,
        msg: "User added"
      });
    } else {
      res.json({
        success: false,
        msg: "User not added"
      });
    }
  } catch (err) {
    res.json({
      success: false,
      msg: "Error occured."
    });
  }
});
Test the /register route with Postman

Postman is a handy little GUI to create HTTP requests. It is available as standalone application or as extension for the Chrome browser. Get it here.
Open a tab for a new request and change the method from GET to POST. Enter http://localhost:3000/users/register as request URL. It is necessary to specify what format the data in the body will have. Therefore select Headers and enter Content-Type as key and application/json as value. Now switch to Body, select raw and enter the following JSON string

{
  "email": "[email protected]",
  "password": "ilikecats"
}

Now hit Send and if everything works as expected (be sure that your node app runs, I'd suggest to use nodemon as presented in the previous tutorial) our app will report, that the user has been added successfully:

postman_body_shadow.png

If you immediately try that again with the same request, it will fail because the user already exists in our database.

Create a central file for configuration

Before we implement the /authenticate route, let's create a single file, where we put all configuration. Create a new folder /config and a new file /config.js. We move the URL of the MongoDB instance to the database and create a new configuration variable secret, which will be used later to create JWT tokens. The configuration file should look like

"use strict";

// secret to sign the json web token
module.exports.secret = "Ilikecats";
// the URL to our local instance of MongoDB
module.exports.db_url = "mongodb://localhost:27017/mean_stack_tutorial";

Now go to app.js, import ./config/config.js with

const config = require("./config/config");

and replace the hard coded database URL with the configuration variable:

mongoose.connect(config.db_url);

If you have sensitive data in your configuration (e.g. AWS keys), you should consider adding such files to your .gitignore to exclude them from version control. This may protect you from accidentally leaking such critical data on Github.

Authenticate users

When we authenticate users, we will return a token to our client, that can be used to authenticate the user in protected API endpoints. To achieve this, we need three new packages. The Express.js middleware passport, that simplifies protection of routes, passport-jwt which provides a JWT (JSON web token) based strategy for passport, and jsonwebtoken to create a JWT for a user object. Install them with npm

npm install passport@0 --save
npm install passport-jwt@3 --save
npm install jsonwebtoken@8 --save

For the /authenticate route, the jsonwebtoken package is needed, so it must be imported in routes/users.js together with config.js which is required because it contains the secret to sign the token.

const jsonwebtoken = require("jsonwebtoken");
const config = require("../config/config");

With this we have everything required for the user authentication route:

router.post("/authenticate", async (req, res, next) => {
  try {
    let user = await User.getUserByEmail(req.body.email);
    if (!user) {
      res.json({
        success: false,
        msg: "Unknown user"
      });
      return;
    }
    let matched = await User.passwordMatches(req.body.password, user.password);
    if (matched) {
      const jwt = jsonwebtoken.sign({ id: user._id }, config.secret, {
        expiresIn: 604800
      });
      res.json({
        success: true,
        msg: "User authenticated",
        token: "bearer " + jwt
      });
    } else {
      res.json({
        success: false,
        msg: "Incorrect password"
      });
    }
  } catch (err) {
    res.json({
      success: false,
      msg: "Error occured: " + err
    });
  }
});

Given the email of the user in the request's body, the callback at first retrieves the user from the database. If it doesn't exist, an error is reported to the frontend. Otherwise passWordMatches is called to verify, whether the given password is the correct one. If not, again an error is reported to the frontend.
Finally if the password matches, the token is created. For this jsonwebtoken.sign() is used. This function expects three parameters. The first parameter is the data to be encoded in the token. In this case we only encode the database id of the user, since we can read all other data from the database. The second parameter is some secret which is also required to decode the token. Keep it secret! Keep it safe!.
In the third parameter a dictionary with optional parameters can be passed. For example with expiresIn, it is possible to specify how long the token is valid (the number supplied is in seconds and corresponds to one week). At last the created token is included in the response to the frontend. For passport to work correctly it is vital that the string "bearer " is prefixed to the token.

Test the new route in PostMan, if everything works as expected, the response should look like

postman_authenticate_shadow.png

Setup passport to protect another route

Passport supports a variety of authentication options (e.g. authentication with a Facebook or Google account). For this we need to provide a function that setups passport for authentication with a JWT token. Most of this is supplied by the passport-jwt package, but nevertheless we need to setup some code in a new file config/passport.js

const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const User = require("../models/user");
const config = require("../config/config");

module.exports = function(passport) {
  let opts = {};
  opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
  opts.secretOrKey = config.secret;
  passport.use(
    new JwtStrategy(opts, (jwt_payload, done) => {
      User.getUserById(jwt_payload.id)
        .then(user => {
          if (user) {
            return done(null, user);
          } else {
            return done(null, false);
          }
        })
        .catch(err => done(err, false));
    })
  );
};

If you lookup the README.md of passport-jwt you will notice that this is more or less some boilerplate code adapted to our use case. We need to pass some options to the JwtStrategy constructor. It is necessary to specify where the authorization token is expected. With ExtractJwt.fromAuthHeaderAsBearerToken() we tell passport, that the token will be passed in the Authentication field of the request's header. The bearer scheme will be used, which simply means that the string "bearer " must be prefixed to the token string.
Next to the options, a callback function is passed to the constructor. This callback function receives the object encoded in the JWT token and must call the done callback with the user object received from the database. passport will add the user to the request object of a route callback in req.user.

Before we add the protected route, we need to register the passport middleware with our Express.js app. Go to app.js and add the following lines to import passport and the initialization function created above.

const passport = require("passport");
const passport_config = require("./config/passport.js");

Now add the passport middleware to the app object and call the initialization function

app.use(passport.initialize());
app.use(passport.session());
passport_config(passport);

With all been setup we can now add a protected route. Go to /routes/users.js and add the following route for the /portfolio endpoint.

router.get(
  "/portfolio",
  passport.authenticate("jwt", {
    session: false
  }),
  (req, res, next) => {
    res.json({
      user_email: req.user.email,
      user_id: req.user._id
    });
  }
);

In contrast to the routes created before, this new route is created with an additional second parameter, which tells passport to protect this route with the JWT strategy. The callback function will only be called, if the the authorization with the token was successful. In this case, the user extracted by the function defined in config/passport.js can be accessed by req.user. In this example, we only return the email and the id of the user to the frontend.

Try to access this route with Postman. Create a Get request, enter http://localhost:3000/users/portfolio as URL and hit Send. The reply is Unauthorized. Now go back to the authentication request from before and copy the string returned in "token". In the request to /portfolio, go to Headers and enter Authorization as key and the copied token as value (be sure to include the "bearer " prefix). If you send the request again, it is successfully answered:

postman_portfolio_shadow.png

This concludes the third part of the tutorial series on how to build a web application with the MEAN web application stack.

Example Code on Github

The code for this part of the tutorial can be found on Github.

What's next?

In the next part I will start building the frontend with user registration and authentication.

Curriculum

Previous parts of this series:



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @nafestw I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x