I have a super redundant server.js file, since nearly every method in it belonging to the REST API starts like that, as I have to check at the API requests whether the client is authorized to ask for the particular thing.
var jwt = require('jsonwebtoken');
// ...
app.get('/getsomething', function(req, res) {
"use strict";
var token = req.headers[tokenName];
if (token) {
jwt.verify(token, app.get('some_secret'), {
ignoreExpiration: false
}, function(err, decoded) {
if (err || typeof decoded === "undefined") {
res.json({
status: 401,
message: "unauthorized"
});
}
else { // actual code starts here...
What would be a better way?
You should have a middleware that checks every request coming in, something like this:
// AUTHENTICATION
app.use(async (req) => {
try {
const token = req.headers.authorization
const { person } = await jwt.verify(token, SECRET)
req.person = person
return req.next()
} catch (e) {
return req.next()
}
})
In this example, both success or fail allows the user to pass through to the route, but req.person
is only populated if the user is logged in. Keep in mind, this is an asynchronous function due to async
keyword. It returns a promise and we are using try/catch architecture. This allows us to use await
which halts execution until jwt.verify()
has populated person
. This then makes req.person
available in every route. In this example, person
is a property that you specified when you did jwt.sign()
to create the JWT. Here is an example to jog your memory:
jwt.sign({
person: 'some unique identifier'
}, 'secret', { expiresIn: '1y' })
In this way, req.person
can only ever be populated by decoding a valid JWT. Do not be confused by const { person } = await jwt.verify(token, SECRET)
. This is the same as writing:
const decodedJWT = await jwt.verify(token, SECRET)
const person = decodedJWT.person
In every protected route, you can simply make the first line of code something like:
// PROTECTED
app.get('/radical', async (req, res, next) => {
try {
// If req.person is falsy, the user is not logged in
if (!req.person) return res.status(403).render('error/403')
// Otherwise, the user is logged in, allow him/her to continue
// Replace res.render() with res.json() in your case.
return res.render('radical/template', {
person: req.person
})
} catch (e) {
return next(e)
}
})
This example demonstrates EJS View Templating Engine:
Then, after all your routes, put a splat route, and an error handling middleware:
// SPLAT ROUTE
app.get('*', (req, res, next) => {
return res.status(404).render('error/404')
})
// ERRORS
app.use((err, req, res, next) => {
res.status(500).render('error/500')
throw err
})
The error handling middleware needs to come last, and it has an additional 4th parameter which is err
and it contains the value of the parameter of next()
only if you pass a parameter to it, ie: next('Error happened')
.
This above code works without any changes for GraphQL as well. To handle authentication in GraphQL, simply examine this:
// GRAPHQL
app.use('/graphql', bodyParser.json(), graphqlExpress((req) => {
const context = {
person: req.person
}
return {
schema,
context,
rootValue: null,
formatError: (error) => ({
message: error.message,
locations: error.locations,
stack: error.stack,
path: error.path
}),
debug: true
}
}))
The GraphQL endpoint must be mounted after the authentication middleware. The logged in user will be available in every resolver as context.person
, or if the request is illegal, context.person will be falsy. I mention this for any others searching in the future.