📜 ⬆️ ⬇️

Modern JWT authorization for modern Kode framework Node.js

image
The authorization task arises in almost every Node.js project, however, in order to properly configure it, you need to connect a large number of modules and collect a bunch of information from different sources.

In this article, I will describe a complete authorization solution based on JSON Web Token (JWT) for Node.js and Koa with the storage of password hashes in MongoDB. The reader is expected to have a basic knowledge of Node.js and how to work with MongoDB through Mongoose.

A few words about what exactly will be discussed and why.

Why Koa. Despite the much greater popularity of the Express framework, Koa provides the ability to write applications using the modern async / await syntax. Using async / await instead of callbacks is a big enough incentive to get a closer look at this framework.
')
Why JWT. The approach to authorization using sessions can already be called obsolete, since it does not allow its use in mobile applications and where there is no support for cookies. Also problems with sessions can arise in cluster systems. JWT authorization does not have these disadvantages, and has several additional advantages. Read more about JWT here.

The article will consider a complete authorization solution using:

  1. passport.js. The de facto standard for working with authorization in Node.js projects
  2. password hashing and hash storage in MongoDB database
  3. authentication for REST API
  4. authentication for socket.io, which is usually a more complex topic than p.3

To preserve the educational value of the article in the code there will be no extended checks for errors and exceptions, which often make the code less understandable. Therefore, before using code examples in production, you need to work on error handling and control of client input.

So, let's begin


1. Connect the Koa. Unlike Express, Koa is a lighter framework and therefore is usually used with a number of additional modules.

const Koa = require('koa'); //  const Router = require('koa-router'); //  const bodyParser = require('koa-bodyparser'); //   POST  const serve = require('koa-static'); // ,      index.html    const logger = require('koa-logger'); //      .   . const app = new Koa(); const router = new Router(); app.use(serve('public')); app.use(logger()); app.use(bodyParser()); 

2. Connect Passport.js . Passport.js allows you to flexibly configure authorization using different mechanisms called Strategies (local, social networks etc.). Currently, the library has more than 300 strategies.

 const passport = require('koa-passport'); // passport  Koa const LocalStrategy = require('passport-local'); //   const JwtStrategy = require('passport-jwt').Strategy; //   JWT const ExtractJwt = require('passport-jwt').ExtractJwt; //   JWT app.use(passport.initialize()); //  passport app.use(router.routes()); //   const server = app.listen(3000);//     3000 

3. Connect work with JWT. In a nutshell, JWT is just JSON in which, for example, a user's email can be stored. This JSON is signed by a secret key that does not allow this email to be changed, although it allows it to be read.

Thus, when you receive from a JWT client, you are sure that the user you came for is the one who claims to be himself (provided that his JWT was not stolen by someone, but this is another story).

 const jwtsecret = "mysecretkey"; //    JWT const jwt = require('jsonwebtoken'); //   JWT  hhtp const socketioJwt = require('socketio-jwt'); //   JWT  socket.io 

4. Connect socket.io. In a nutshell, socket.io is a module for applications that react to changes occurring on the server, for example, it can be used for chat. If the server and browser support the WebSockets protocol, then socket.io will use it, otherwise it will look for other mechanisms for implementing two-way communication between the browser and the server.

 const socketIO = require('socket.io'); 

5. We connect MongoDB for storage of objects of users.

 const mongoose = require('mongoose'); //      MongoDB const crypto = require('crypto'); //  node.js     ,  ..   . 

Now let's run it all together.


The user object ( user ) will consist of its name, e-mail and password hash.

To convert a password obtained from a POST request into a hash that will be stored in the database, the concept of virtual fields is used. A virtual field is a field that is in the Mongoose model, but which is not in the MongoDB database.

 mongoose.Promise = Promise; //  Mongoose    mongoose.set('debug', true); //  Mongoose       .     mongoose.connect('mongodb://localhost/test'); //    test   .   ,    . 

We create the scheme and model for the User:

 const userSchema = new mongoose.Schema({ displayName: String, email: { type: String, required: ' e-mail', unique: ' e-mail  ' }, passwordHash: String, salt: String, }, { timestamps: true }); userSchema.virtual('password') .set(function (password) { this._plainPassword = password; if (password) { this.salt = crypto.randomBytes(128).toString('base64'); this.passwordHash = crypto.pbkdf2Sync(password, this.salt, 1, 128, 'sha1'); } else { this.salt = undefined; this.passwordHash = undefined; } }) .get(function () { return this._plainPassword; }); userSchema.methods.checkPassword = function (password) { if (!password) return false; if (!this.passwordHash) return false; return crypto.pbkdf2Sync(password, this.salt, 1, 128, 'sha1') == this.passwordHash; }; const User = mongoose.model('User', userSchema); 

For a deeper understanding of the mechanism of working with password hashes, you can read about the pbkdf2Sync command in the Node.js dock

We configure work with Passport.js


The user authorization process is as follows:

Step 1. A new user is registered and a record is created about him in the MongoDB database.
Step 2. The user logs in with the password on the site and upon successful input of the login and password receives JWT.
Step3. The user enters an arbitrary resource, sends his JWT, by which he is authorized without entering a password.

The configuration mechanism of Passport.js consists of two stages:

Stage 1. Setting up Strategies. Upon successful authorization, the strategy returns the user object described earlier in the userSchema schema.
Step 2. Using the user object obtained in step 1 for further actions, for example, creating a JWT for it.

Stage 1


Customize Passport Local Strategy. In more detail, how the strategy works can be read on here .

 passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password', session: false }, function (email, password, done) { User.findOne({email}, (err, user) => { if (err) { return done(err); } if (!user || !user.checkPassword(password)) { return done(null, false, {message: '     .'}); } return done(null, user); }); } ) ); 

Customize Passport JWT Strategy. In more detail, how the strategy works can be read on here .

 //  JWT  Header const jwtOptions = { jwtFromRequest: ExtractJwt.fromAuthHeader(), secretOrKey: jwtsecret }; passport.use(new JwtStrategy(jwtOptions, function (payload, done) { User.findById(payload.id, (err, user) => { if (err) { return done(err) } if (user) { done(null, user) } else { done(null, false) } }) }) ); 

Stage 2


We will create a REST API that will work with the user object.

The API will consist of three endpoints that correspond to the three steps of the authorization process described above.

Post request for / user - creates a new user. Usually this API is called when a new user is registered. In the body of the request, we expect JSON with the name, mail and password of the user.

 router.post('/user', async(ctx, next) => { try { ctx.body = await User.create(ctx.request.body); } catch (err) { ctx.status = 400; ctx.body = err; } }); 

Post request for / login creates a JWT to use. In the body of the request, we expect to receive JSON in which the mail and password of the user will be. In production, it is logical to issue JWT also when registering a user.

 router.post('/login', async(ctx, next) => { await passport.authenticate('local', function (err, user) { if (user == false) { ctx.body = "Login failed"; } else { //--payload -            const payload = { id: user.id, displayName: user.displayName, email: user.email }; const token = jwt.sign(payload, jwtsecret); //  JWT ctx.body = {user: user.displayName, token: 'JWT ' + token}; } })(ctx, next); }); 

A GET request for / custom checks for a valid JWT.

 router.get('/custom', async(ctx, next) => { await passport.authenticate('jwt', function (err, user) { if (user) { ctx.body = "hello " + user.displayName; } else { ctx.body = "No such user"; console.log("err", err) } } )(ctx, next) }); 

Now let's make the final chord for setting up authorization for socket.io. The problem here is that the WebSockets protocol runs on top of tcp, not http and the REST API mechanisms do not apply to it. Fortunately, for it there is a module socketio-jwt, which allows you to quite concisely describe the authorization through JWT.

 let io = socketIO(server); io.on('connection', socketioJwt.authorize({ secret: jwtsecret, timeout: 15000 })).on('authenticated', function (socket) { console.log('    : ' + socket.decoded_token.displayName); socket.on("clientEvent", (data) => { console.log(data); }) }); 

More details about authorization via JWT for socket.io can be read here .

Conclusion


Using the code above, you can build a working Node.js application using a modern authorization approach. Of course, in production, you will need to add a number of checks, which are usually standard for this kind of application.

The full version of the code with a description of how to test it can be viewed in GitHub.

Source: https://habr.com/ru/post/324066/


All Articles