Full Stack Development Series Part 8: User Authentication and JWT Support in NestJS
Add user authentication to your NestJS API endpoints and implement support for JWTs in requests.
If you've been following along in this series, you'll know that the codebase provides a basic to-do tracker and not much else. However these posts are meant to address the most common aspects of modern, full-stack applications, and user authentication is a major part of public-facing web apps. In this article we'll accomplish the following:
- Add JWT support to most of the API endpoints
- Add a
user
table to our database - Define a one-to-many relationship between to-do items and a user
- Update Swagger documentation with JWT support
- Add E2E tests to cover authenticated API calls
Other posts in this series:
- An Introduction
- Part 1: Getting Started With Nx, Angular, and NestJS
- Part 2: Creating a REST API
- Part 3: Connecting Angular to a REST API
- Part 4: Data Persistence with TypeORM and NestJS
- Part 5: Design Systems and Angular Component Development with Storybook
- Part 6: Application Deployment and CI/CD
- Part 7: Unit and Integration Testing
- Part 8: User Authentication and JWT Support in NestJS (this post)
If you want to skip ahead to the code, you can checkout out the repository: wgd3/full-stack-todo@part-08
Create A New Shared Data Structure
Before we even begin to think about our database or API endpoints, we should consider what a User
object should look like and make that interface available to any application or library in the repository:
The ITodo
interface will require a small update to reflect this new one-to-many relationship:
Create New Libraries
With our data structures established, it's time to add a new Nest module with a collection of /auth
endpoints. Since the commands are almost identical, I went ahead and created the User module as well:
# controller for /auth endpoints
$ npx nx generate @nrwl/nest:library FeatureAuth \
--controller \
--directory=libs/server \
--importPath=@fst/server/feature-auth \
--service \
--strict \
--tags=type:feature,scope:server
# controller for /users endpoint
npx nx generate @nrwl/nest:library FeatureUser \
--controller \
--directory=libs/server \
--importPath=@fst/server/feature-user \
--service \
--strict \
--tags=type:feature,scope:server
I also took this opportunity to rename the server-data-access-todo
library to server-data-access
. If our application was larger I would advocate for dedicated data-access
libraries to separate concerns, but for now I feel comfortable grouping our (soon to be created) services together.
Instead of worrying about manually updating imports and moving files, Nx provides a generator that handles this "migration" for us:
$ npx nx generate @nrwl/workspace:move server/data-access \
--projectName=server-data-access-todo \
--importPath=@fst/server/data-access
In our newly-renamed data access library we'll add an ORM entity schema for our User objects:
We'll have to add a relations
key to the TodoEntitySchema
to match the definition above:
Not too difficult, right? Our repository has an established pattern for file names and locations, so adding elements like interfaces and entities is straightforward. There's a problem now though - our to-do DTO is out of sync with our interface, and a user DTO is needed as well. As our repository grows, it's important to keep in mind which libraries rely on others, and since these DTOs will be referenced in multiple libraries I migrated DTOs to the recently-renamed server-data-access
library.
Here's the DTO I came up with for our user objects:
user
and user_id
properties of ITodo
were not optional - my logic was that a todo requires a user, so why make it optional? As this code was developed I decided to make them optional so that they were not required properties in the to-do DTOs. Any API request that involves to-do entities will be "protected" with a JWT in the request header, and our service will (soon) read the user ID from the token before interacting with the database. The service will inject the user ID as necessary on behalf of the user.Set Up Authentication Support
Next we need to focus on implementing authentication. Following the NestJS docs, there are a few dependencies we'll need to install:
$ npm i --save @nestjs/passport passport passport-jwt @nestjs/jwt bcrypt
$ npm i -D @types/passport-jwt @types/bcrypt
If you'll recall, we used the --service
flag when generating our UserModule, and we can now update that service with some basic methods. We'll follow the same pattern as the TodoService: injecting a Repository
reference in the constructor, and use async/await calls to interact with the database layer:
Once a user is created, they'll need to be sent access tokens to make authenticated API requests. The JwtService
from @nestjs/jwt
will do this for us in the AuthSerivce:
forwardRef
to get around circular dependencies. I think a case could be made to refactor our User and Auth services into the server-data-access
library, and that will most likely happen soon. I left this code as-is for now to bring awareness to the circular dependency issue, and show how it is "remedied".I was a bit bothered with the code for the AuthService - the method generateAccessToken
was missing a return signature. I try to ensure that all inter-application data is defined in an interface, so I added interfaces not only for the token response, but also for the content of the decoded JWT.
Additionally, it's likely that more data will be added to a user's
access_token
or a refresh_token
will be added. Premature optimization is a weakness of mine, but I believe these interfaces are worth it at this time.Now we can explicitly define the return signature for generateAccessToken
:
The ServerFeatureAuthModule
is going to contain the majority of code related to user auth, which makes it a fitting place for the JwtModule
registration:
In the above code block I've reference 2 new environment variables:
JWT_SECRET
should be a long, secure string that is not shared anywhere or checked into Git. In deployment environments (such as Render!) there is often a "secrets" or environment variable management page where you can define these as well.JWT_ACCESS_TOKEN_EXPIRES_IN
is also a string. This module can interpret various time expressions such as10m
and3600s
(read more about the configuration options here)
If you remember the list of packages that were installed earlier, passport is used quite heavily for our authentication needs. Passport introduces the concept of strategies for "drag-n-drop" authentication mechanisms. The homepage for Passport includes a large list of the strategies available, but we're going to stay focused on passport-jwt
here.
We'll create a simple extension of the strategy provided by passport-jwt
:
$ npx nx generate @nrwl/nest:service JwtStrategy \
--project=server-feature-auth \
--directory=lib \
--flat
We can now add JwtStrategy
to the list of providers
in the ServerFeatureAuthModule
and everything will be ready to go!
Surprise - tests have broken!
Since the beginning of this post, there have been at least 7 services/controllers added, as well as numerous other classes and interfaces added. It was at this point that I ran nx run-many --target=test --all
and sure enough server-feature-user
and server-feature-auth
failed.
I didn't document the code I added for tests at this point, because it was just enough to prevent the tests from failing. The repository tag for this post will include the whole suite of unit and integration tests for these new libraries. A few things to note however:
- Factories in
lib/shared/util-testing
had to be updated to support the User <-> Todo relationship. This broke a few other test suites that rely on those factories, and needed to be updated with user IDs. You can check out this commit to see how all tests were remedied. - Tests for the new libraries failed immediately because I had started writing functional code and injecting dependencies without reconciling the test suites. The commit that fixed all the new tests is here
Adding User Endpoints
Given that we've significantly reduced our code coverage percentage, we're going to write some unit tests in conjunction with the /user
endpoints to verify functionality. First we'll create a POST endpoint to create a new user, and that endpoint should receive and validate a CreateUserDto
object:
create
returns the full user, so we strip out the hashed password from the return value. Since the user is new, we can hard code todos
to an empty array as well.Next, a test to make sure the controller returns a "sanitized" payload even though the service's method returns a full user object:
Note that the above does not test the DTO's validation, just the method itself. DTO validation is handled by the ValidationPipe
outside the scope of the test - therefore that belongs in the integration tests.
# test user sign up
$ http -b localhost:3333/api/v1/users [email protected] password="Password1\!"
{
"email": "[email protected]",
"id": "98dd8297-af15-43bb-a767-a6c33324e33a",
"todos": []
}
Adding Auth Endpoints
Repeating the process for the POST endpoint that was just created, let's add (and test) that our new users can log in and retrieve access tokens:
# test token generation
$ http -b localhost:3333/api/v1/auth/login [email protected] password="Password1\!"
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IndhbGxhY2VAdGhlZnVsbHN0YWNrLmVuZ2luZWVyIiwic3ViIjoiOThkZDgyOTctYWYxNS00M2JiLWE3NjctYTZjMzMzMjRlMzNhIiwiaWF0IjoxNjc5NTAwNjIwLCJleHAiOjE2Nzk1MDEyMjB9.G0UqHUfsHtuSZurjhwnOhWRqZ0dTZaCSDrVUNKKLVR4"
}
# parsed output from jwt.io
{
"email": "[email protected]",
"sub": "98dd8297-af15-43bb-a767-a6c33324e33a",
"iat": 1679500719,
"exp": 1679501319
}
Bearer Auth Integration
We're almost there. Our API supports user creation and login requests, but we need to add a few things to enforce our JWT strategy. I started by copy/pasting some code that I've used for years, and originated in some StackOverflow post I can no longer find. The following creates a custom parameter decorator that gives us quick access to the req.user
property of a request:
JwtStrategy
. The strategy starts by attempting to pull the JWT from the headers of an incoming request. If the Authorization
header is present, and the token is valid, then Passport modifies the incoming request and adds a user
property to it. To show this in use, check out the getUser
method in the UserController. It reads the user ID from the JWT, compares it the to user ID requested in the URL, and throws a 404 if they don't match (don't want users reading each other's data!).
Once again quoting the NestJS documentation, we're going to add a custom Guard at the application-level so that it applies to the whole app:
What's a Reflector
and what is it doing in the auth Guard? NestJS provides this in order to manipulate and access the execution context of a request. In the above code, we're looking for a bit of metadata that indicates whether or not we need to validate tokens on for a given request. The metadata in question gets set by another custom decorator:
This decorator will be used on any and all endpoints we deem to be "public", such as the POST request to create a new user:
One more bit of "enforcement" that I wanted to address concerns the use of the ApiBearerAuth()
decorator on both routes and controllers. There's a distinct difference in the two: if a controller is decorated, then all of it's routes will default to requiring valid tokens.
When added to a route however, only that specific route will require a valid token:
We're also going to add one line to the main.ts
file while will tell the Swagger documentation to provide authentication options in it's UI:
const config = new DocumentBuilder()
.setTitle(`Full Stack To-Do REST API`)
.setVersion('1.0')
.addBearerAuth() // <-- add this!
.build();
You'll now be able to paste tokens into this prompt, and Swagger will automatically add the Authorization
header to each request.
Updating To-Do Endpoints to Enforce Authentication
A large update was made at this time that covered all of the /todo
routes: they all needed to use the @ReqUserId
decoration to retrieve an ID from the request's user
object, and every method in the TodoService
needed to be update with support for this variable.
For example, the flow for retrieving a specific to-do item looks like this:
I think it would be a bit repetitive to see all the routes/services that were changed to reflect this requirement, but the final code for this tag includes all the necessary updates.
Updating E2E Tests with Authentication
There has been a large amount of code added, and like I mentioned previously should be mindful of the code coverage percentage. I've shown some of the unit tests that focused on controllers, now it's time to look at the entire request-response cycle. Before I wrote any tests, I knew that we would need at least 1 test user as well as a valid JWT for that user, so I added this to my beforeAll()
code block:
I used the same request
/expect
pattern for all the tests in this suite, and used nested describe
block to group the various HTTP verbs:
PASS server-e2e apps/server-e2e/src/server/todo-controller.spec.ts
ServerFeatureTodoController E2E
GET /todos
✓ should return an array of todo items (13 ms)
✓ should not return an array of todo items without auth (5 ms)
POST /todos
✓ should create a todo item (20 ms)
✓ should prevent adding a to-do with an ID (9 ms)
✓ should prevent adding a to-do with an existing title (11 ms)
✓ should prevent adding a todo item with a completed status (7 ms)
✓ should enforce strings for title (8 ms)
✓ should enforce strings for description (16 ms)
✓ should enforce a required title (10 ms)
✓ should require an access token to create (4 ms)
PATCH /todos
✓ should successfully patch a todo (21 ms)
✓ should return a 404 for a non-existent todo (6 ms)
✓ should return a 404 for a todo that doesn't belong to the user (9 ms)
✓ should prevent updating a to-do with an ID (7 ms)
✓ should enforce strings for title (8 ms)
✓ should enforce strings for description (8 ms)
✓ should enforce boolean for completed (8 ms)
PUT /todos
✓ should successfully put a todo (14 ms)
✓ should return a 400 for a todo that doesn't belong to the user (9 ms)
✓ should prevent updating the ID of a todo (17 ms)
✓ should enforce strings for title (7 ms)
✓ should enforce strings for description (7 ms)
✓ should enforce boolean for completed (7 ms)
DELETE /todos
✓ should delete a todo (13 ms)
✓ should not delete a todo of another user (7 ms)
✓ should require authorization (4 ms)
Once I ironed out some issues with the ConfigModule
, actual testing database, and environment variables I was able to start writing tests:
One of the more complicated tests (relative to the above) were tests that involved database manipulation before making HTTP calls. Here's the PUT test for attempting to updated a different user's to-do:
it("should return a 400 for a todo that doesn't belong to the user", async () => {
// create new user
const altUser = await userRepo.save({
email: randEmail(),
password: 'Password1!',
});
// create todo for that user so that the UUID is already taken
const altUserTodo = await todoRepo.save({
title: 'foo',
description: 'bar',
user: {
id: altUser.id,
},
});
// use ID from new user's todo
const url = `${baseUrl}${todoUrl}/${altUserTodo.id}`;
const payload = {
id: altUserTodo.id,
title: 'foo',
description: 'bar',
completed: false,
};
return (
request
.default(app.getHttpServer())
.put(url)
// use the access token with our user ID instead of new user
.auth(access_token, { type: 'bearer' })
.send(payload)
.expect('Content-Type', /json/)
.expect(HttpStatus.BAD_REQUEST)
);
});
Bonus: Module Boundaries and Dependency Graph
During the development of this post, I realized that I was writing code counter to everything we've structured so far: the ServerFeatureAuthModule
relies on a forwardRef
to ServerFeatureUserModule
in order to properly reference the user service. In an ideal world, feature-level libraries have no dependency on one another at all. I then updated the root .eslintrc.json
file to properly enforce our tag boundaries:
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
},
{
"sourceTag": "scope:server",
"onlyDependOnLibsWithTags": ["scope:server", "scope:shared"]
},
{
"sourceTag": "scope:client",
"onlyDependOnLibsWithTags": ["scope:client", "scope:shared"]
},
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
},
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:util",
"type:domain"
]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:data-access",
"type:util",
"type:ui",
"type:domain"
]
},
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": [
"type:data-access",
"type:util",
"type:domain"
]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": [
"type:ui",
"type:util",
"type:domain"
]
},
{
"sourceTag": "type:util",
"onlyDependOnLibsWithTags": ["type:util", "type:domain"]
}
]
}
]
Going forward, this rule set will enforce the library hierarchy we've been aiming for throughout this series. There is one exception to the rule, for the aforementioned user service:
Until a refactor occurs which renders this feature-on-feature connection obsolete, this rule tells the linter that it's OK for the auth module to depend on the user module.
So what does our dependency graph look like now that we've added a bunch of libraries?
It's not small. 13 libraries and 4 applications (including the e2e apps, not shown above), makes for a large repo. But it's well organized and makes a pretty graph!
Summary
User authentication is not a small addition to any application. It affects the majority of the code base, changes the structure of the database, and requires a lot of testing to ensure expected behavior. But, with some practice, it doesn't have to be a massive hurdle.
The next post in this series will add user registration/login and token management from the client side of the stack. In the mean time, all of the code for this post is available on GitHub: wgd3/full-stack-todo@part-08
See you next time!
Speed Bumps
ReferenceError: jest is not defined
appeared when I attempted to runserver
after adding a bunch of tests. This was a fun one to track down - running the application should pull in any test-related code at all! It boiled down to path mapping intsconfig.base.json
and the use of testing tools inlibs/server/util
. Since I was importingQueryErrorFilter
from a barrel file (libs/server/util/src/index.ts
), and that barrel file also exported test utilities, the test utilities were being bundled with the application. I resolved this by making two changes:
1) Removing the testing exports from the barrel file
2) Adding a specific path mapping for testing utils in that library ("@fst/server/util/testing": ["libs/server/util/src/lib/testing/index.ts"]
)- Did not account for unique constraint on userId + todo.title. Couldn't figure out why the definition wasn't being enforced. This was due to not using migrations - updating the constraints on the table did not replace the existing constraints on the database table. Fixed by deleting the database and restarting the application (which rebuilt the database using the most recent schema)