Description Handling Authentication is probably the most critical part of most applications. Every error or bug where authentication is involved could lead to a hack or the website or a leak of private / sensible information. Managing all required steps to properly secure an application is not straightforward and involves many battle tested techniques which are important to learn.
In this lesson we will build an Authentication system using Json Web Tokens (JWT).
The first step is to build a module (i.e. a file) which will handle the JWT creation and validation. For this purpose we will use the jsonwebtoken library.
Our Authentication system will then be composed of two parts:
- In the login route, after validating the user credentials in a login validation middleware, a new token must be created and added to the response as
Authentication
header
// services/sessions/middlewares/login.validation.js
module.exports = async (req, res, next) => {
// Get email and password from the request body
[...]
// Fetch the User by email by using the UserRepo
[...]
// Check the password is correct via the 'bcrypt' package
[...]
// If the User exists, save user into the req object (see suggestions)
[...]
next();
};
// services/sessions/sessions.router.js
router.post('/', loginValidation, (req, res) => {
// Get user id from the req object and create token using 'jsonwebtoken' (see suggestions)
res.set('Authorization', `Bearer ${token}`);
// Send back the User data as json response
});
this will be read and stored by the client and sent back to each subsequent request to the server. The response will send back the data of the logged in User
- A middleware which acts on each protected route (i.e. each route which needs an authenticated user to be accessed) will check whether the request has a valid token.
// services/sessions/middlewares/auth.check.js
module.exports = async (req, res, next) => {
// Check whether the Authorization header is present
[...]
// Verify the token via
let payload;
try {
payload = jwtManager.verify(token);
} catch(error) {
// TODO Return error via res.json()
}
// Save the userId in the req object (see suggestions)
[...]
next();
};
Goals
- Build a
(POST) /sessions
API Endpoint to perform User login. A Json Web Token (JWT) must be returned upon successful authentication - All
/users
routes must be now private, i.e. only authenticated users can access them
Allowed Npm Packages
axios
: http client used to perform http requestsbcryptjs
: password hasherbody-parser
: Express middleware to parse the body requestsexpress
: web serverjsonwebtoken
: create and verify Json Web Tokensmoment
: date managermongoose
: MongoDB clientnconf
: configuration files managervalidator
: string validation librarywinston
: logger
Requirements
-
The results must be saved in
userdata/data.json
-
The logs must be saved under
storage/logs/nodeJobs.log
-
The Data Logger must reside into
libraries/dataLogger.js
-
The File Logger must reside into
libraries/fileLogger.js
-
The MongoDB configuration variables must reside into
config/secrets.json
, which MUST be gitignored -
A
config/secrets.json.example
file must be provided, with the list of supported keys and example values of theconfig/secrets.json
file -
Configuration values must be loaded by using
nconf
directly at the beginning of theindex.js
-
The Mongoose configuration must reside into a
mongoose.js
file, loaded directly from theindex.js
-
The Mongoose client must be made available in Express under the
mongooseClient
key -
The Users Model must be saved into
models/users.js
and have the following Schema :- username: String, required, unique
- email: String, unique
- password: string, required
-
The Users Model must be made available in Express under the
usersModel
key -
The
/users
routes must be defined in theservices/users/users.router.js
file by using the Express router -
Middlewares used in the
/users
endpoints must reside in theservices/users/middlewares/
folder -
Optionally use only
async
/await
instead of pure Promises in all/services/
files -
User input validation errors must return a
422
Json response with{ hasError: 1/0
,error: <string>
} as response data (payload) -
User passwords must be
bcrypt
hashed before being saved into the database -
JWT management (creation and verification) must be handled in
libraries/jwtManager.js
, which must export a Javascript Class. It must be available in Express under thejwtManager
key -
The Secret Key used to create the tokens must be stored in the
secrets.json
file -
/sessions
routes must be defined inservices/sessions/sessions.router.js
-
/users
API Endpoints must check for authenticated users through the use of aservices/sessions/middlewares/auth.check.js
middleware -
HTTP Status Codes must be coherent: 401 if no authentication is provided (i.e. token not found in the
Authorization
header, 403 if the token is expired or invalid
Suggestions
- Exporting a class is quite easy
const package = require('package');
class MyClass
{
/**
* @param {string} myvariable
*/
constructor(myvariable) {
this.myvariable = myvariable
// Code
}
/**
* @param {string} myparameter
*/
myMethod(myparameter) {
// variable is available under this.myvariable
// package is available inside methods
// Code
}
}
module.exports = function (myvariable) {
return new MyClass(myvariable);
};
- When creating a new token, remember to save the userId into the
sub
payload field. It can be set with thesubject
key in theoptions
parameter of thesign()
method of thejsonwebtoken
package
// jwt.sign(payload, secretOrPrivateKey, [options, callback])
const newToken = jwt.sign({
data: {}
}, 'my secret key stored in /config/secrets.json', {
expiresIn: '4h',
subject: <User id> // The User Id, as defined by MongoDB, should be stored here
});
- When checking for login validation, in case of success it is useful to save the authenticated user id and user data into the request, so that it is available later
// services/sessions/middlewares/login.validation.js
module.exports = async (req, res, next) => {
if (!req.context) {
req.context = {};
}
[...] // Perform Login Validation and retrieve the user data
req.context.id = user._id;
req.context.user = {
id: user._id,
email: user.email,
username: user.username,
admin: user.admin || false
};
[...]
};
// services/sessions/sessions.router.js
[...]
router.post('/', (req, res) => {
// req.context.user is now available here!
[...]
});
[...]
- Let's save the user id also in the auth check function
// services/sessions/middlewares/auth.check.js
module.exports = async (app, data, context) => {
[...] // Check whether the user is authenticated
context.id = userId;
};