Testing secure APIs by mocking JWT and JWKS
Recently, I've had some time to get back to my pet project Catkin. I'm working on gradually improving the testing which was sorely neglected when I created the initial prototype app.
When implementing end to end API tests I quickly ran into the issue of a missing authentication token as there is no logged-in user. As I'm using Auth0 to keep things nice and simple for my user login implementation, I have no easy way to login a user from an endpoint directly on the backend which is the usual approach.
In this article I'll explain how I solved that problem.
I use the Jest for running my tests. In writing this I'm assuming that you have the basic framework up and running already so that you can run tests against your API. The full setup of jest is not covered.
The Catkin user authentication process
First let's look at how users log in to Catkin. In the Catkin login flow the following happens:
- The application frontend connects directly to Auth0 to get a JWT token.
- The token is then added to the authorisation header of each request from the frontend to the backend API.
- Upon receiving a request, the backend validates that the token was generated by Auth0 and is valid for Catkin. This is done by the Auth0 JWKS endpoint.
- If the token is valid, the requested query/mutation is executed. If not, then a
401 Unauthorized
code is returned.
Quick definitions
Just in case you're not familiar with the terms, two fundamental things to know are:
- JWT: JSON Web Token - a secure token signed by the authentication provider using a secret key. This contains the details of the authenticated user and can be used to securely store other information such as user security roles. Read more.
- JWKS: JSON Web Key Set is a list of the public keys which can be used to verify the JWT. They are stored by the authentication provider and used in step 3 of the process described above. For Auth0 the JWKS is always found at
https://your_auth_domain.xx.auth0.com/.well-known/jwks.json
Read more.
For the artists among you
Here's a picture ...
Image sourced from https://auth0.com/docs/architecture-scenarios/web-app-sso/part-1.
And here's another one. Simpler. Better. But you have to imagine that instead of REST it says GraphQL ๐.
Image sourced from https://hceris.com/setting-up-auth0-with-terraform/.
With that covered, it's now time to think about how we can test our API with this additional layer of complexity.
Testing approach
I need to test:
- That Catkin the Catkin GraphQL API returns the correct query results/performs the expected mutation.
- That the security applied to the API works.
With the authentication flow that, is in place any unauthenticated user will be rejected. This obviously makes testing the API a little more difficult, as tests must run as an authenticated user.
The two most obvious approaches to test the secured API are:
- Connect to Auth0 during test execution to get a token.
- Mock a JWKS endpoint and use that for testing.(A JWKS endpoint is the thing that actually validates that the JWT is legitimate).
I would prefer to avoid option one, even though the Auth0 free tier would be enough to support my testing needs. Option two is cleaner, and my chosen approach which I will cover below. It means that if anybody else wants to use the Catkin code they would not be tied in to using only Auth0 or having an external connection available.
Implementation
Now that we know the theory and have decided the approach, let's have a go at implementing it.
Mocking the JWT and JWKS
To fully mock the authentication process, we need to achieve the following:
- Create a JWT without depending on Auth0.
- Allow the backend to verify the JWT without connecting to Auth0.
To do both things, we can use a lovely little library called mock-jwks which was created for exactly this use case.
Mock-jwks works by intercepting calls to Auth0 (or actually any OAuth service) using nock. Nock helps us to perform isolated testing of modules which make HTTP requests by intercepting those requests before they are sent to the external service and allowing us to act on them. Once the request to the JWKS endpoint has been intercepted, mock-jwks can then validate (or not) the JWT which is being passed to it.
First, install the libraries:
yarn add mock-jwks nock --dev
Now in our tests we can create a mock Auth0 endpoint with the following code:
const jwks = createJWKSMock('https://catkin-dev.eu.auth0.com/');
jwks.start();
Then generate a token as below. For the Auth0 token you should specify the reserved claims audience (aud
) and issuer (iss
) as you have set up in your environment variables. The https://catkin.dev/permissions
is specific to Catkin and an example of how you can use custom data in Auth0 which will be added to your token:
const token = jwks.token({
aud: "https://catkin.dev",
iss: `https://catkin-dev.eu.auth0.com/`,
'https://catkin.dev/permissions': [
{
"group": "*",
"role": "admin"
}
],
});
The token can then be added to any request header:
it('Creates an item when user is logged in', async () => {
const res = await request(global.app.getHttpServer())
.post('/graphql')
// add the token to the request header
.set('Authorization', 'Bearer ' + global.validAuthToken)
.send({
operationName: null,
query: createItemQuery,
})
const data = res.body.data.createItem;
expect(data.title).toBe(item.title);
});
Now whenever your backend tries to check something with Auth0, mock-jwks will intercept the request using nock, and do the check instead. No external connection is required.
Likewise, we can also test that our endpoint rejects unauthenticated users by omitting the Authorization
header:
it('Throws an error when API is called with no token', async () => {
const res = await request(global.app.getHttpServer())
.post('/graphql')
// send the request without the auth token
.send({
query: CREATE_ITEM_GQL,
variables: {
createItem: item,
},
});
expect(res.body.errors).toBeTruthy;
expect(res.body.errors[0].extensions.exception.status)
.toBe(401);
});
Finally, at the end of the tests, or if we want to break the auth service for further testing, simply stop the JWKS server.
jwks.stop();
Cleaning up the code
The basic test is now in place but the implementation is a bit messy. To help with re-use of the code, let's implement a helper file which contains all code for setting up the JWKS mock, generating tokens, etc. Auth service settings should also not be hard-coded; they will instead be passed to this helper function allowing us to provide incorrect details in the token to simulate an invalid token.
import createJWKSMock, { JWKSMock } from 'mock-jwks';
export function startAuthServer(jwksServer: string): JWKSMock {
const jwks = createJWKSMock(jwksServer);
jwks.start();
return jwks;
}
export function getToken(
jwks: JWKSMock,
authDomain: string,
authAudience: string): string {
const token = jwks.token({
aud: [`${authAudience}`, `${authDomain}/userinfo`],
iss: `${authDomain}/`,
'https://catkin.dev/permissions': [
{
group: '*',
role: 'admin',
},
],
sub: 'testprovider|12345678',
});
return token;
}
export function stopAuthServer(jwks: JWKSMock) {
jwks.stop();
}
These functions are then called from my global setup.ts file beforeAll()
and afterAll
functions, providing a global JWKS endpoint and JWT that can easily be reused in all tests. Take a look at the full setup here: https://github.com/MeStrak/catkin.
Wrap up
As the objective of Catkin is to provide a hosted environment for several organisations, the security must be rock solid. Thanks to mock-jwks it was straightforward to mock the whole authentication process allowing the API to be fully tested, including fail cases for unauthenticated users.
I now have a simple framework in place allowing me to quickly write tests simulating authenticated or unauthenticated users.
The next step will be to simulate authenticating as users with different roles to check that granular security levels work correctly.
Thanks for reading! I deliberately kept this fairly brief to provide an overview. I hope that even at this high level the article is still useful. As always, I'm happy to answer any questions you may have.
Fin.