Skip to content

authentication

John Biundo edited this page Aug 25, 2019 · 2 revisions

Authentication Chapter

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.

Following along

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.

Login route section

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:

When you get to the end of the Implementing Passport JWT section, you can test the login functionality with HTTPie:

HTTPie requests to test login functionality

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!

Implement protected route and JWT strategy guards section

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:

  1. Add this line to the imports import { AuthGuard } from '@nestjs/passport';
  2. 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);
  }
...
Testing authentication

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 {}
HTTPie routes for testing

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.

Using Curl to access protected routes

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>"

Using Postman to access protected routes

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:

  1. Create a new POST request
  2. Set the URL to localhost:3000/cats
  3. Create a raw Body object, pasting in the following new cat JSON object:
{ "name": "Fred", "age": 5, "breed": "Alley Cat" }
  1. 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.

  2. Add the JWT (copied from the access_token when you did the POST /api/login request above). In Postman, go to the Authorization tab [(1) in the screen shot below], select Bearer Token for type, and paste the token in the Token field, [(2) in the screen shot below].

  3. 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.

HTTPie routes to get cats

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

Adding roles

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:

  1. Uncomment the @Roles('admin') guard on the create method. Enable the RolesGuard 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);
  }
  1. 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 modify UsersService. Go ahead and open src/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

  1. Update the validate() method of the JwtStrategy to return roles. Replace the contents of src/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,
    };
  }
}
  1. Update the login() method of the AuthService to include roles in the payload. Replace the login method in src/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),
    };
  }

Upon completion

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