Now that we have added our tests, it's time to implement the feature. Since we know that we want to check the token for most requests, we should not define the token validation logic solely within the Delete User engine. Instead, we should abstract all the generic validation steps (for example, the token is a valid JWT, the signature is well-formed, and so on) into middleware.
To start, create a file at src/middlewares/authenticate/index.js with the following boilerplate:
function authenticate (req, res, next) {}
export default authenticate;
First, we want to allow anyone to get a single user and search for users; therefore, when the request is a GET request, we don't need to validate the token. At the top of the authenticate function, add the following check:
if (req.method === 'GET') { return next(); }
Usually, before a browser sends a CORS request, it will send a preflight request that checks to see whether the CORS protocol is understood. This request uses the OPTIONS method, and thus we also don't need to validate the token for OPTIONS requests either:
if (req.method === 'GET' || req.method === 'OPTIONS') { return next(); }
Next, we also want unauthenticated users to be able to call the Create User and Login endpoints. Just below the previous line, add the following early return checks:
if (req.method === 'POST' && req.path === '/users') { return next(); }
if (req.method === 'POST' && req.path === '/login') { return next(); }
For any other endpoints, an Authorization header is required. Therefore, we'll next check for the presence of the Authorization header. If the header is not set, then we will return with a 401 Unauthorizated error:
const authorization = req.get('Authorization');
if (authorization === undefined) {
res.status(401);
res.set('Content-Type', 'application/json');
return res.json({ message: 'The Authorization header must be set' });
}
Next, we check that the value of the Authorization is valid. First, we can use the following code to check that a scheme is specified and is set to the value "Bearer":
const [scheme, token] = authorization.split(' ');
if (scheme !== 'Bearer') {
res.status(400);
res.set('Content-Type', 'application/json');
return res.json({ message: 'The Authorization header should use the Bearer scheme' });
}
Then, we will check that the token is a valid JWT. We do this by specifying a regular expression and checking that the token specified in the header conforms to this regular expression. This uses the jsonwebtoken library, so be sure to import it at the top:
const jwtRegEx = /^[\w-]+\.[\w-]+\.[\w-.+/=]*$/;
// If no token was provided, or the token is not a valid JWT token, return with a 400
if (!token || !jwtRegEx.test(token)) {
res.status(400);
res.set('Content-Type', 'application/json');
return res.json({ message: 'The credentials used in the Authorization header should be a valid bcrypt digest' });
}
We have done all the relatively resource-light tasks, and exits early if these base conditions are not met. In the last step for this middleware, we will actually use the verify method to check that the payload is a valid JSON object and that the signature is valid. If it is, then we will add a user property to the req object with the ID of the user:
import { JsonWebTokenError, verify } from 'jsonwebtoken';
verify(token, process.env.PUBLIC_KEY, { algorithms: ['RS512'] }, (err, decodedToken) => {
if (err) {
if (err instanceof JsonWebTokenError && err.message === 'invalid signature') {
res.status(400);
res.set('Content-Type', 'application/json');
return res.json({ message: 'Invalid signature in token' });
}
res.status(500);
res.set('Content-Type', 'application/json');
return res.json({ message: 'Internal Server Error' });
}
req.user = Object.assign({}, req.user, { id: decodedToken.sub });
return next();
});
To apply the middleware, add it inside src/index.js after all the other middlewares, but before the route definitions:
import authenticate from './middlewares/authenticate';
...
app.use(bodyParser.json({ limit: 1e6 }));
app.use(authenticate);
app.get('/salt', ...);
...
However, we're not quite done yet. The middleware only validates the token, but it still doesn't prevent a user from deleting another user. To implement this, add the following lines to the top of the Delete User engine:
if (req.params.userId !== req.user.id) {
return Promise.reject(new Error('Forbidden'));
}
And in the Delete User handler, define an if block to catch the Forbidden error and return a 403 Forbidden status code:
function del(req, res) {
return engine(req)
.then(() => { ... })
.catch((err) => {
if (err.message === 'Not Found') { ... }
if (err.message === 'Forbidden') {
res.status(403);
res.set('Content-Type', 'application/json');
res.json({ message: 'Permission Denied. Can only delete yourself, not other users.' });
return err;
}
...
})
}
If we run the E2E tests for the Delete User endpoint, they should all pass! Now, follow the same steps to add authentication and authorization logic to the Replace Profile and Update Profile endpoints. Start by updating the E2E tests, and then update the engine and handlers to handle scenarios where a user is trying to perform an operation they are not allowed to.
Also, update the unit and integration tests, add more tests if you feel it's necessary, and then commit your code to the remote repository.
If you get stuck, check out our implementation from the code bundle we've provided. Do this before moving on to the next chapter.