FeathersJS Authentication: typed and tested

Katarina Golbang
5 min readMar 15, 2019
Image by “Timi az en vaok hat ki mas lenne xD” (https://de.freeimages.com/photo/test-me-1420159)

Nowadays, almost every application requires Authentication. That’s why I have been looking for the way how to do it in FeathersJS. I’ve found https://hackernoon.com/setting-up-email-verification-in-feathersjs-ce764907e4f2 and followed the docs. It was clear so far what to do. However, one thing that I’ve really missed was a precise overview of each authentication related scenario. I usually do it by writing tests because:

  • They serve as a specification.
  • They empower us to implement only those features being tested. Not more, not less.
  • They give us confidence.
  • They save us many headaches.

That’s why I’ve reverse-engineered what’s going on under the hood, writing the tests for each known scenario. Of course, the problem with postponed tests is that they just reflect the existing implementation, not caring whether the things are needed as they are. Although this is not the proper way, it’s still better than no tests at all.

(After all, the authentication deserves more care, doesn’t it?)

Project setup

I’ve generated the project as explained by

(Thank you!). In my case, I’ve decided to migrate to Typescript for reasons of strong typing.

Prior to the migration to Typescript, the number of the *.js files is less than 30, so changing the extension won’t be a big deal.

$find ./src ./test -type f -name ‘*.js’ | wc -l
24

First, we install additional @type namespaced libraries for the typings. Second, we are going to equip our testing environment by installing Chai, Sinon, and Factory Girl. Last but not least, we also need Typescript ecosystem libraries in place.

npm i --save-dev @types/body-parser @types/chai @types/chai-as-promised @types/cors @types/express @types/feathersjs__feathers @types/helmet @types/mocha @types/node @types/request @types/request-promise @types/sequelize @types/serve-favicon @types/sinon @types/factory-girl @types/winston chai chai-as-promised factory-girl sinon ts-node tslint tslint-no-unused-expression-chai typescript

Writing tests

I’ve ended up with 42 tests. The following scenarios are covered:

/authentication
It works with the verified users who try to sign in.

POST /users
It works with the passed user credentials that could be correct or incorrect. The user creation is the first step towards successful authentication.

/authManagement
It works with both, the verified and unverified users (depends on the particular scenario). Unverified users go through the verification process (resendVerifySignup, verifySignupLong). Verified users, however, might perform password or identity changes (sendResetPassword, resetPwdLong, passwordChange, identityChange).

securing services
It works with any user attempting to access a secured service.

How to test

Software testing is a large topic. Since software consists of many layers (just think of the MVC), it comes that both, single parts (sole functions, services, …), and the application as a whole need test. The bigger gets the tested piece of code, the more likely are we going to write an integration test.

Due to the usage of the libraries (@feathersjs/authentication, @feathersjs/authentication-jwt, …) I’ve decided to write integration tests. In this case, it actually means that a request is being issued as it resides on the very top of the application.

Basically, a typical scenario (“describe”/”it”) looks as follows:

I’m using a Sinon spy in order to grab the user’s verifyToken created during the signup. This, in turn, is used for the mailer service test (the mailer create method is stubbed since we don’t really want to send an email during the test execution).

The code excerpt responds to the authentication requirements:

  • is the correct HTTP status code returned?
  • does the user have the right state (e.g. isVerified = false)?
  • are some (critical) properties removed from the response?
  • was the verification/confirmation email sent?
  • does the email have the proper content?
  • does the response body have the proper format (e.g. accessToken is a JWT)?

Few words about the test setup

It’s a good habit to reset the database for every “it”. This is achieved by running a common “beforeEach”. Furthermore, we don’t really want to deal with the INSERT INTO’s (neither by running plain SQL nor by instantiating Sequelize models). There’s where Factory Girl comes in play (in fact, it’s borrowed from the Ruby on Rails ecosystem).

The function “setupUserRelatedFactory” defines basic recipes on how to create instances of UserModel. This boils down to:

The usage is easy:

let verifiedUser = await fg.factory.create('verifiedUser');
let unverifiedUser = await fg.factory.create('unverifiedUser');

Another thing is the testing “friendliness” of the implementation. For instance, anonymous functions (such as the notifier) are hard to test. That’s why the notifier is implemented as a utility class allowing the clients to spy on its static methods:

let notifierSpy = sinon.spy(Notifier, 'onNotify');

In this context, I have to mention that the user instance traverses a couple of methods until it gets saved and serialized as JSON. Obviously, Sinon does not take snapshots of the method arguments as they were at the time of calling. The introduction of the “onNotify” method alongside the actual “notify”, and the cloning of the user instance fixes the issue.

this.onNotify(type, JSON.parse(JSON.stringify(user)), notifierOptions);

Issues revealed by the tests

  • The mounted “authManagement” service might throw ugly exceptions such as “Cannot read property ‘password’ of undefined”. Not sure the clients should see these.
  • Another thing I’m uncertain about is the behavior of the actions sendResetPassword, resetPwdLong, passwordChange, and identityChange. They work regardless of whether the user is verified or not. Just change the test setup from
… fg.factory.create('verifiedUser');

to

… fg.factory.create('unverifiedUser');

and re-run the tests. They will pass. In my opinion, this functionality should require verified users only.

  • Empty passwords and invalid email addresses are accepted. The provided hooks fix this.

Final thoughts

One may argue that the used libraries are already well tested by the issuer. This is partially true; they are. However, the integration of those adds another level of complexity. Just think of the config files, database changes, and hooks. The application might be misconfigured. The table DDL might get modified. The hooks could be removed. The order of the hooks could change. It affects the application and can lead to unpleasant security issues.

Repository

--

--

Katarina Golbang

Software Developer @vienna passionate about software quality