-
Notifications
You must be signed in to change notification settings - Fork 0
authentication
With this chapter, if you want to follow along, check out the authentication-start
branch and go to the following along section.
If you just want a final version of the project as of the end of the chapter, check out the authentication-end
branch, and proceed to the upon completion section. Note that if you do so, you may want to check out some of the HTTPie route tests that are embedded in each of the following sections.
Unlike some of the other documentation chapters, the Authentication chapter of the docs is meant to stand on its own as a full end-to-end implementation. We're going to tie it in with the nest-cats app we've been building. To do that, we need to make a couple of slight modifications to the code presented in the Authentication chapter, but what we're doing remains highly faithful to the concepts and code documented in the chapter.
One challenge with this is that we don't have a real front end. So be prepared, when we get to actually logging in, for rolling up your sleeves a bit to get the testing to work properly. Don't worry, we can get through this! I just want you to know that the water there is slightly turbulent :)
Refer to the comments below, corresponding to chapter sections, to guide you through the code changes for nest cats as you proceed through the docs chapter.
Read through the Authentication chapter and perform each step as documented until you get to the Login route section, then return here.
Our folder structure should now look like this:
nest-cats
└───src
└───auth
└───cats
│ └───dto
│ └───interfaces
└───common
| └───decorators
| └───filters
| └───guards
| └───helpers
| └───interceptors
| └───middleware
| └───pipes
└───users
We're going to create our login route in the AppController
. Go ahead and paste the code for the login route from Login route section of the docs chapter into src/app.controller.ts
Now continue with the steps in the next two sections of the docs, implementing each step as described:
- JWT functionality section of the docs chapter
- Implementing Passport JWT section of the docs chapter.
When you get to the end of the Implementing Passport JWT section, you can test the login functionality with HTTPie:
login attempt
http POST :3000/api/login username=john password=changeme
This should return an access_token with a JWT as the value. Hooray! We're making great progress!
When you get to Implement protected route and JWT strategy guards section, return here as we'll implement these features on our CatsController
instead of in the AppController
as shown in the docs.
Open up src/cats/cats.controller.ts
and:
- Add this line to the imports
import { AuthGuard } from '@nestjs/passport';
- Add the JWT auth guard to the
create()
route. It should look like the following. We haven't quite tied together our@Roles()
guard yet, so we'll leave that commented out for the moment.
...
@Post()
@UseGuards(AuthGuard('jwt'))
// @Roles('admin')
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
console.log('running create()');
this.catsService.create(createCatDto);
}
...
Let's make one minor adjustment to the JWT settings to make life a little easier while testing. The docs show setting the expiration at 60s
to demonstrate automatic handling of expired tokens. You can experiment with this to see how it works as described in the documentation. Once you're happy with that, I recommend you change it to 60m
so that it doesn't time out while we're testing our integrated app here.
Change src/auth/auth.module.ts
to look like this:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60m' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
Logging in (getting a valid JWT)
With HTTPie, you can run:
http POST :3000/api/login username=john password=changeme
You'll need to capture the JWT, which is returned as the value of the access_token
property of the response to the above request.
Adding a cat (accessing a protected route)
Note: I had some difficulty with HTTPie sending a bearer token, though perhaps you'll have better luck here than I did. Instead, I'm using Curl below. Even this is admittedly a bit tricky (mainly, dealing with long, wrapped strings on a command line), and you may want to explore other options, like Postman. Postman makes it very easy to copy and paste a bearer token, and do other advanced things with your request. See how to use Postman for this step, below if you're interested.
In the line below, substitute your access_token from the login query for <your-token-here>
. You may want to do this in a text editor, depending on what OS you're using and whether it handles wrapping a long line properly. The access token does not have any line breaks, so make sure it is all on one line. Again, you may consider using Postman or another similar tool that handles bearer tokens well for this part of the test.
curl -X POST http://localhost:3000/cats \
-d '{"name": "Fred", "age": 3, "breed": "alley cat"}' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-token-here>
"
Postman lets you easily append a Bearer token to a request. We want to issue a POST /cats
request with a body containing the new cat. Here are the steps you want to take with Postman:
- Create a new
POST
request - Set the URL to
localhost:3000/cats
- Create a raw Body object, pasting in the following new cat JSON object:
{ "name": "Fred", "age": 5, "breed": "Alley Cat" }
-
Select JSON(application/json) as the type of body. (This adds a
Content-Type: application/json
) header. You can do this manually, or by selecting the Body type as shown in the screen shot below. -
Add the
JWT
(copied from theaccess_token
when you did thePOST /api/login
request above). In Postman, go to the Authorization tab [(1) in the screen shot below], selectBearer Token
for type, and paste the token in the Token field, [(2) in the screen shot below]. -
Run the request by clicking on the
Send
button. Also, save the request, which you can run again. If the JWT expires, you can simply run the login route again, as described above, and paste the new token in using the Authorization tab.
Once you've successfully authenticated and added new cats with the POST /cats
route from the previous step, you can retrieve them as usual.
Get cats added in previous step
http GET :3000/cats
We're almost done. We're not yet utilizing Roles
to implement authorization. Let's say we want to limit the create cat function (POST /cats
) to only users with the admin
role. We need to do several things to implement this:
- Uncomment the
@Roles('admin')
guard on thecreate
method. Enable theRolesGuard
on this route. It should now look like this
@Post()
@UseGuards(RolesGuard)
@UseGuards(AuthGuard('jwt'))
@Roles('admin')
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
console.log('running create()');
this.catsService.create(createCatDto);
}
- Assign roles to users. In our simplified in-memory implementation, we'll make this change directly to our mock user data in the
UsersService
. In real life, you'll manage this through your Users store. The concepts should be similar, but for ease-of-testing, we'll just modifyUsersService
. Go ahead and opensrc/users/users.service.ts
and replace the code with the following:
import { Injectable } from '@nestjs/common';
export type User = any;
@Injectable()
export class UsersService {
private readonly users: User[];
constructor() {
this.users = [
{
userId: 1,
username: 'john',
password: 'changeme',
roles: ['admin'],
},
{
userId: 2,
username: 'chris',
password: 'secret',
roles: ['user'],
},
{
userId: 3,
username: 'maria',
password: 'guess',
roles: ['user'],
},
];
}
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
The following two steps add the roles
property to the JWT payload handling
- Update the
validate()
method of theJwtStrategy
to return roles. Replace the contents ofsrc/auth/jwt.strategy.ts
with this code:
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return {
userId: payload.sub,
username: payload.username,
roles: payload.roles,
};
}
}
- Update the
login()
method of theAuthService
to includeroles
in the payload. Replace the login method insrc/auth/auth.service.ts
with this code:
async login(user: any) {
const payload = {
username: user.username,
sub: user.userId,
roles: user.roles,
};
return {
access_token: this.jwtService.sign(payload),
};
}
At this point, we've got a basic functioning role-based authorization system.
To test roles, you can try logging in again with each of the different users, capturing the JWT, and attempting to add a cat using their associated token. Only user 'john'
should be able to add a cat. Other users should fail with a 403
error:
{
"statusCode": 403,
"timestamp": "2019-08-27T22:46:43.809Z",
"path": "/cats"
}
If you've left the tracing code from the Cats Enhancements chapter in place, you'll also see that the RolesGuard
correctly reports that these users (e.g., chris
and maria
), do not have the required role:
>> [M:]LoggerMiddleware Request...
>>>> [G:]RolesGuard hasRole: false
>>>>>>>>>> [E:]HttpExceptionFilter Returning HTTP Status Code 403 from /cats