In Part -1 of this series, we saw how we can get started with KoaJS and in Part – 2 we built CRUD endpoints with MongoDB. In this part, we’re going to work with authentication. We will be using JSON Web Tokens aka JWT for the auth part. We have written detailed pieces on JWT before. You can read Understanding JWT to check out the basics and read our tutorial on JWT with Flask or JWT with Django to see how other frameworks like Flask uses JWT.
JWT with KoaJS
To implement JSON Web Tokens with KoaJS, we would be using two packages – koa-jwt
and jsonwebtoken
. The second package (jsonwebtoken
) provides useful helper functions to generate and verify JWTs. Where as koa-jwt
provides an easy to use middleware that we can use with KoaJS.
Let’s go ahead and install these packages:
|
npm i -S koa-jwt jsonwebtoken |
That should install the dependencies and save them in our package.json
.
Securing Routes with JWT
We have the required packages installed. So we can now start securing our routes with JWT. We can just require
the koa-jwt
package directly and use it. But we want to customize some aspects. For that we would create our own module named jwt.js
and put the custom stuff in there.
|
const jwt = require("koa-jwt"); const SECRET = "S3cRET~!"; const jwtInstance = jwt({secret: SECRET}); module.exports = jwtInstance; |
Now in our index.js
file, we would add the middleware to the app.
|
app.use(require("./jwt")); |
If we try to visit http://localhost:3000/, we will get a plain text error message saying “Authentication Error”. While the message is clear and concise, we want to output JSON, not a plain text error message. For that, we will write a custom middleware.
|
function JWTErrorHandler(ctx, next) { return next().catch((err) => { if (401 == err.status) { ctx.status = 401; ctx.body = { "error": "Not authorized" }; } else { throw err; } }); }; |
The code for this middleware is pretty simple. It invokes the next middleware and if it catches an error, it checks if it’s 401, if so, it sets a nice detailed JSON as the output. If you’re familiar with how middlewares work in express / koa, this should make sense. If it doesn’t make sense, don’t worry, you will get it over time.
Now we need to export this function from our jwt.js
module. Let’s change the exports a little bit.
|
module.exports.jwt = () => jwtInstance; module.exports.errorHandler = () => JWTErrorHandler; |
Now we’re exporting two functions, which, when called will return the specific middlewares. We also need to change our imports in index.js
–
|
const jwt = require("./jwt"); app.use(jwt.errorHandler()).use(jwt.jwt()); |
Please note the order of the middleware we used. The error handler must come before the JWT middleware itself, so it can call next()
and check for the 401 error.
If we try to browse the API now, we should get a nice JSON like this:
|
{"error":"Not authorized"} |
Secured Routes and Router
We used the middleware directly on the koa app. That means all our routes are now secure. All the routes would now check for the Authorization
header value and try to verify it’s value as a JSON Web Token. That’s good but there’s a slight problem. If we can’t access any of the routes without a token, which route do we access to get the token in the first place? And what token do we use for that? Yeah, we need to have at least one route which is not secured with JWT which will accept login details and issue the JWTs to the users. Besides, there could be other API end points which we can keep open to everyone, we don’t need authentication on those routes. How do we achieve that?
Luckily, Koa allows us to use multiple routers and each router can have their own set of middlewares. We will keep our current router open and add the routes to obtain the JWT. We will create a separate route which will use the middleware and be secured. We will call this one the “secured router” and the routes would be “secured routes”.
|
// Create a new securedRouter const router = new Router(); const securedRouter = new Router(); // Add the securedRouter to our app as well app.use(router.routes()).use(router.allowedMethods()); app.use(securedRouter.routes()).use(securedRouter.allowedMethods()); |
We modified our existing codes. We now have two routers and we added them both to the app. Let’s now move our old CRUD routes to the secured router and apply the JWT middleware to just the secured router.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
// Apply JWT middleware to secured router only securedRouter.use(jwt.errorHandler()).use(jwt.jwt()); // List all people securedRouter.get("/people", async (ctx) => { ctx.body = await ctx.app.people.find().toArray(); }); // Create new person securedRouter.post("/people", async (ctx) => { ctx.body = await ctx.app.people.insert(ctx.request.body); }); // Get one securedRouter.get("/people/:id", async (ctx) => { ctx.body = await ctx.app.people.findOne({"_id": ObjectID(ctx.params.id)}); }); // Update one securedRouter.put("/people/:id", async (ctx) => { let documentQuery = {"_id": ObjectID(ctx.params.id)}; // Used to find the document let valuesToUpdate = ctx.request.body; ctx.body = await ctx.app.people.updateOne(documentQuery, valuesToUpdate); }); // Delete one securedRouter.delete("/people/:id", async (ctx) => { let documentQuery = {"_id": ObjectID(ctx.params.id)}; // Used to find the document ctx.body = await ctx.app.people.deleteOne(documentQuery); }); |
We removed the previously setup JWT middleware from the app and used it on securedRouter instead. Remember, the JWT middleware must be setup before we setup the routes themselves. Ordering of middleware matters.
If we try to visit “http://localhost:3000/”, we will no longer get the auth error, rather will see “not found” (we didn’t define any routes for the root url). However, if we try to visit “http://localhost:3000/people”, we will get the authentication error again. Exactly what we wanted.
Issuing JWTs
We now need to create the route to issue JWTs to our users. We will be accepting their login (username and password) and if they’re valid, we will issue them JWTs which they can use to further access our APIs.
The koa-jwt
package no longer supports issuing tokens. We have to use the jsonwebtoken
package for that instead. Personally, I like to create a helper function in my custom jwt.js
module like this:
|
// Import jsonwebtoken const jsonwebtoken = require("jsonwebtoken"); // helper function module.exports.issue = (payload) => { return jsonwebtoken.sign(payload, SECRET); }; |
Then we can write a new route on our public router like this:
|
router.post("/auth", async (ctx) => { let username = ctx.request.body.username; let password = ctx.request.body.password; if (username === "user" && password === "pwd") { ctx.body = { token: jwt.issue({ user: "user", role: "admin" }) } } else { ctx.status = 401; ctx.body = {error: "Invalid login"} } }); |
We have hardcoded the username and password here. In production environments, we would store the details in a database and we would hash the password. No one in their right mind should store password in plain text.
In this view, we are accepting a JSON payload and checking the username and password. And then if the details match, we are issuing the token. To test if it’s working, we can make a curl request and checkout the response:
|
curl -X POST \ http://localhost:3000/auth \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{"username": "user", "password": "pwd"}' |
If it worked, we will get a JSON back with a token
value containing the JWT.
Using the JWT
We made a request and got the following response:
|
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTUwMjI3MTM0Nn0.GWtjeECIHFQr7vI_MphfUle06Pav_zx4sLmSrd3HE8g"} |
That is our token. Now we can start using it in the Authorization header. The format should be like:Authorization: Bearer <Token>
. We can make a request to our secured “/people” resource using curl with this header:
|
curl -X GET \ http://localhost:3000/people \ -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTUwMjI2OTg4MX0.Ugbh4UwN9tRwhIQEQUHoo-affUf5CAsCztzAXncBYt4' \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ |
We will now get back the list of people we have stored in our mongodb.