Local Authentication in a Koa API

Local auth for an API in koa will take a few pieces working together. Here's one way to do it.
The Flow
We're going to require authentication on our rest APIs for a single page app web client. We're going to maintain a session in the API. If you're logged in, you have a session. When a request is made to an API endpoint, a session cookie is passed. In the API, the cookie value is used to lookup the session. If there's a valid session matching the cookie, you're allowed to continue on an fetch the resource you addressed. If there is not a valid session, it means you're not authenticated, and the an error response is sent back to the client.
Session Libs
There are many libraries that will help you establish a sesssion. We're going to use koa-generic-session. This requires another library choice for the session store. There are many options. We're going to use postgres, so koa-pg-session is a great choice.
koa-generic-session, as of this writing, doesn't natively support koa v2 middleware, so you need to use koa-convert to make it compatible.
We're also going to use koa-passport to give us access to login, logout, and isAuthenticated functions.
Install them all:
npm install koa-generic-sesion koa-pg-session koa-convert koa-passport --saveSession Store
I made a auth/session.js module:
const PgStore = require('koa-pg-session')
const store = new PgStore(process.env.DATABASE_URL)
function init() {
return sessionStore.setup()
}
exports.init = init
exports.store = store
A few things to note:
- It's important to connect your
PgStorewith a full database connection string, like:postgres://username:password@host:port/database. Don't store this in source code. We're loding this from the environment variableDATABASE_URL. - We expose
storefor use in multiple places later, and it's important that it's the same instance, so this is a singleton in our app. - We'll use
initlater... stay tuned.
Session Middleware Stack
Now it's a matter of getting your koa (v2, btw) middleware setup the correct way. I'll lay out what I have in my server/index.js file first, and we'll talk through it:
const convert = require('koa-convert')
const koa = require('koa')
const passport = require('koa-passport')
const session = require('koa-generic-session')
const { store } = require('./auth/session')
// ..
const app = new koa()
app.keys = [process.env.SECRET_KEY]
app.use(convert(session({
cookie: {
path: '/',
httpOnly: false,
maxAge: 24 * 60 * 60 * 1000,
rewrite: true,
signed: true
},
key: 'myAppId',
store
})))
app.use(passport.initialize())
app.use(passport.session())
// ..
- The setting of
app.keysis for the cookie signing. Make sure you have this set or you'll get a funky error saying that there's a cookie setting error and the key needs to be a Buffer. (agh!) For me, I'm setting this in an environment variable,SECRET_KEY. - The first middleware is the
koa-generic-sessionmiddleware. Make sure youconvertit to make it compatible with koa v2. - The
cookieattribute contains all the options for how cookies are set in the browser. Notice we're settingsigned: true, hence the need forSECRET_KEY. - The
keyattribute is the cookie name, customizable by you. - The
storeis the singletonPgStorethat we instantiated in theauth/session.jsmodule. Remember to always use this one instance.
Session Store Initialization
Before you start up your app and listen for incoming requests, you want to make sure that the store you plugged into your session middleware above is fully armed and operational. To do this, make sure you're calling store.setup() and waiting until that async action is done before going on. We wrapped this in the auth/session.js module's init function. We might call that in this order:
const session = require('./auth/session')
async function startApp() {
await session.init()
app.listen(process.env.PORT)
}
Now your app is fully initialized and ready to receive requests. Now, to protect the endpoints.
API Endpoint Authentication
To protect an endpoint, we are going to add a middleware that checks for the existence of a valid, active session. First let's write that middleware in auth/login.js:
module.exports = async function requireLogin(ctx, next) {
if (ctx.isAuthenticated()) {
await next()
} else {
ctx.status = 401
ctx.body = {
errors: [{ title: 'Login required', status: 401 }]
}
}
}
ctx.isAuthenticatedcomes fromkoa-passport. This is checking for a valid session.- If we are authenticated, let the request continue.
- If we are not authenticated, the request cannot continue, and we return a response of
401, "Unauthorized".
Now let's use that middleware, putting it in front of a resource that we want to protect. Let's say that our /ringsofpower are precious to us, so we want to require authentication there. We might write this in the controller:
const koa = require('koa')
const route = require('koa-route')
const requireLogin = require('./auth/login')
const app = new koa()
// ..
app.use(requireLogin)
app.use(route.delete('/:id', mountDoom))- We're using another library here,
koa-route, which simply lets you define urls for your endpoints, mapping them to request handlers. You definitely don't need this library for this to work, but it's really nice. requireLoginbecomes the local alias for theauth/login.jsfunction we wrote above.- We stack
app.use(requireLogin)before our precious resources later in theapp.usestack in order to require its execution before anything else.
At this point, all of our requests will start to fail with 401s because we haven't created any way to actually login and create a new session.
Valid Users
To have a set of valid users, we'll need to create user records and store them in our database. We will need to take extra care in storing passwords -- in fact, we'll just store password hashes, no more -- using bcrypt. For the down low on how this might look, check out the post on storing local user passwords using bcrypt. Once we have valid users in the system, we're ready to login.
Logging In
We're assuming that your web app client has a UI for a user to enter their username and password. We're just going to look at the server-side flow for logging in for now. We're going to make a module, auth/index.js that will be the controller to create our new session. First, we register our route for session creation:
const koa = require('koa')
const route = require('koa-route')
const app = new koa()
async function create(ctx) {
// ..
}
app.use(route.post('/', create))
This is all as expected. Now what goes in create? It might look like this:
const bcrypt = require('bcrypt')
const repo = require('./repo')
function serialize(user) {
return {
data: {
username: user.username
}
}
}
async function create(ctx) {
const { username, password } = ctx.request.body
try {
const user = await repo.find(username)
if (!user) {
ctx.status = 401
return ctx.body = { errors: [{ title: 'User not found', status: 401 }]}
}
const matches = await bcrypt.compare(password, user.passwordHash)
if (matches) {
ctx.status = 201
ctx.body = serialize(user)
return ctx.login(user)
} else {
console.log('u, p', username, password)
ctx.status = 401
return ctx.body = { errors: [{ title: 'Password does not match', status: 401 }]}
}
} catch (err) {
ctx.status = 500
return ctx.body = { errors: [{ title: err.message, status: 500, stack: err.stack }]}
}
}
There's a lot here:
- The
usernameandpasswordcome from the client via thectx.request.body. - We lookup the user using a
repo.jsmodule. This is some code that takes a username and matches it to a user in your database. The implementation is up to you and not super important here. bcrypt.compareis the line that determines if the passwords match.- The
serializefunction makes sure we only expose fields we desire to the client. ctx.loginis provided bykoa-passport. It will actually trigger the session creation, storage in our session store, and setting of the cookie.
Now for the response options based on the logic branches in this function:
- 401 - if username is not in our user database
- 401 - if the password does not match
- 500 - if any part of this fails
- 201 - if password matches a valid user
There is one more piece to setup. We need to teach passport how we want the user record that we're ctx.logining in with to be serialized. So in this same file, we can add:
const passport = require('koa-passport')
passport.serializeUser((user, done) => {
done(null, { username: user.username })
})
passport.deserializeUser((user, done) => {
done(null, user)
})
In this configuration, we show we only want to store the user's username.
Logging Out
Now the user has had exposure to the precious resources of our REST API long enough. Before the grow wizen and corrupt, let us revoke their access. They might not come willingly, so we must be firm. Thankfully, revoking access is much simpler than revoking just about anything else in real life. Let's register another endpoint in auth/index.js, and we'll be ready:
async function destroy(ctx) {
ctx.logout()
ctx.status = 204
}
app.use(route.delete('/', destroy))
That's all there is to it. The session record in the store will be removed as will the session cookie.
So it turns out there are lot of things to get in place for a full auth flow through an API. Do you do this in a similar way? What could be simplified or made better?