Skip to main content

Security in Nest

You can get the codebase for the previous part by checking out the commit **#20d18**

Now let’s talk about one of the most crucial parts of API development: SECURITY.

Everything we have done so far is great, we have been able to achieve a lot, but our app is dangerous for clients, let’s protect them.

According to the nest documentation, there are several ways to secure NestJS GraphQL APIs:

  1. Authentication: Use JSON Web Tokens (JWT) or OAuth2 to authenticate users and secure their data. NestJS has built-in support for JWT authentication, we should be able to keep the user’s secret credentials(like the user’s password) encrypted and inaccessible.
  2. Authorization: Use GraphQL directives to limit access to specific fields and operations based on user roles and permissions, for example, in our case not every user should be able to add books in the store, or only authenticated users should retrieve books, etc. NestJS has built-in support for GraphQL directives.
  3. Input validation: Use the built-in validation decorators to validate user input and prevent malicious attacks.
  4. CORS: Enable CORS(Cross-Origin Resource Sharing) to prevent cross-site scripting (XSS) attacks. NestJS has built-in support for CORS.
  5. Rate Limiting: Limit the number of requests a user can make to prevent DDoS(distributed denial-of-service) attacks and abuse. NestJS has built-in support for rate limiting.
  6. HTTPS: Use HTTPS to encrypt communication between the client and server and prevent man-in-the-middle attacks.
  7. Error handling: Use proper error handling to prevent sensitive data from leaking to the client and to log errors.
  8. Security headers: Add security headers to protect against common web vulnerabilities.

🚧 It's vital to keep in mind that ensuring the security of your NestJS GraphQL APIs is a continuous effort and the methods used may evolve over time. Therefore, it's critical to stay informed of any new advancements and adapt your security strategies accordingly.

We will go step by step:

  1. Enable CORS Inside the GraphQLModule we will simply enable cors by using the cors property of its forRoot() method like the following:

    ```tsx
    // app.module.ts

    ...
    GraphQLModule.forRoot({
    cors: true
    // or alternatively parse cors options like:
    /**
    cors: {
    origin: '*',
    credentials: true,
    ...
    },
    */
    }),
    ...
    ```
  2. Encrypt password To keep it simple for now, we will use bcrypt to encrypt and decrypt our passwords.

    Let’s start by installing the bcrypt utilities:

    ```bash
    # install bcrypt
    $ yarn add bcrypt && yarn add -D @types/bcrypt
    ```

    Now, let’s inject the `ConfigModule` into the `UserModule` the same way we did for the root directory
    🛠️ Update the `imports` and `providers` arrays of the `userModule` with the following code:

    ```tsx
    // user.module.ts

    ...
    providers: [UserResolver, UserService, ConfigService],
    imports: [
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    ConfigModule.forRoot({
    cache: true,
    }),
    ],
    ...
    ```

    Let’s now add the loginUser and improve the createUser method into the `user.services.ts`

    ```tsx
    // user.service.ts

    ...
    async createUser(createUserInput: CreateUserInput) {
    // GENERATE HASH PASSWORD TO SAVE
    const hash = await bcrypt.hash(
    createUserInput.password,
    Number(this.configService.get<string>('SALT_ROUND')),
    );

    const createdUser = new this.userModel({
    ...createUserInput,
    password: hash,
    });

    return createdUser.save();
    }

    async loginUser(loginInput: LoginUserInput) {
    const { email, password } = loginInput;
    const user = await this.userModel.findOne({ email });

    if (!user) {
    throw new Error('Invalid credentials');
    }

    const isMatch = await bcrypt.compare(password, user.password);

    if (!isMatch) {
    throw new Error('Password or email address incorrect');
    }

    return user;
    }
    ...
    ```

    Get ready for some exciting news! We're taking our user creation to the next level by implementing password encryption using bcrypt. As we start adding features such as JWT tokens, authorization, and a complete registration flow, we don't want our user.service file to become overwhelming. In a NestJS architecture, service files serve the purpose of interacting with the database, so we'll keep our user.service file as the go-to source for retrieving, verifying, and modifying data in the user collection. Instead of including login logic in the user.service file, we'll create a brand new module dedicated to handling authentication (and possibly authorization) in our application.

    That auth module will be responsible for:

    - Creating a new user, making sure their passwords are encrypted correctly(as above)
    - Logging in an existing user making sure it sends a valid jwt token
    - Implementing Guards(sort of middleware) so that we protect some mutations and queries

    This topic is a bit confusing and less documented in the nest official doc so, I will walk through the most important aspects of it:

    1. Let’s generate new auth.module.ts, auth.service.ts and auth.resolver.ts files using nest cli

    ```bash
    # generate auth module files
    $ cd src/app && nest g module auth && nest g service auth && nest g resolver auth
    ```

    2. Install required dependencies to achieve authentication, nest supports passport, and jwt, so we will use that pair to achieve magic.

    ```bash
    # install dependancies
    $ yarn add @types/passport-local @types/passport-jwt -D
    $ yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
    ```

    Those are required dependencies that will help us achieve our end goal.

    3. How do we want our queries and mutations to be proceeded? How do we want to receive auth responses? Well, in real-world scenarios, we expect to receive a token when we log in, using our email address and password(local strategy in good terms), with that said, lets create dto files that prepare us to implement that:

    🛠️ You can follow along and add the `app/auth/dto/login-response.ts` and the `app/auth/dto/login-user.input.ts` files that look like:

    ![login-response.ts file](../../../../static/assets/img/graphql-nest-mongodb/responsedto.png)

    login-response.ts file

    ![login-user.input.ts](../../../../static/assets/img/graphql-nest-mongodb/inputs.png)
    image15 - auth/dto files

    login-user.input.ts
    image15 - auth/dto files

    Nothing strange right? Let’s go ahead with the service and resolvers

    4. In the auth.service.ts file, we need to add a couple of methods that will be the authentication login:
    - a **validateUser** method: this method will retrieve data from the database using the findOneByEmail method, will check either the password matches, then will return a user object or null in case the user isn’t valid, here is how it looks like:

    ```tsx
    // auth.service.ts file

    ...
    async validateUser(loginUserInput: LoginUserInput) {
    const { email, password } = loginUserInput;
    const user = await this.userService.findOneByEmail(email);

    const isMatch = await bcrypt.compare(password, user?.password);

    if (user && isMatch) {
    return user;
    }

    return null;
    }
    ...
    ```

    This looks a bit similar to what we’ve done before, nothing really new, however, you may notice that we are using a `UserService` instance, to be able to do that, don’t forget to import the user module in the auth module, then user the property `exports` from user module file, to be able to access its service in the outside, by default, they are private in nest.

    - a **login** method: this method will only take a validated user(by the previous method) and return an instance of `LoginResponse` object, means contains an authToken in it, here is how it shoud look:

    ```tsx
    // auth.service.ts

    ...
    login(user: User) {
    return {
    user,
    authToken: this.jwtService.sign(
    {
    email: user.email,
    name: user.name,
    sub: user._id,
    },
    {
    secret: this.configService.get<string>('JWT_SECRET'),
    },
    ),
    };
    }
    ...
    ```

    Few things to mention here that may be interesting. First, we are using another environment variable, that holds the jwt secret word(JWT_SECRET). We installed the `@nestjs/jwt` module early, that package has a built-in module that we’ll use to be able to use the `jwtService` instance, just as we did for the config service, as we are getting used to inject modules in our modules, let’s configure the `JwtModule` into our `AuthModule`

    In the `auth.module.ts` file, let’s replace the content by the followings:

    ```tsx
    // auth.module.ts
    // all imports

    ...
    @Module({
    providers: [
    AuthService,
    AuthResolver,
    JwtService,
    ],
    // We nedd to make sure we've imported the userModule, because we're using it's service
    imports: [
    UserModule,
    PassportModule,
    ConfigModule,
    JwtModule.registerAsync({
    inject: [ConfigService],
    imports: [ConfigModule],
    useFactory: (configService: ConfigService) => {
    const properties: JwtModuleOptions = {
    secret: configService.get<string>('JWT_SECRET'),
    signOptions: {
    expiresIn: '24h',
    },
    };
    return properties;
    },
    }),
    ],
    })
    export class AuthModule {}
    ```

    As you can see here, we have used the `registerAsync()` method from JwtModule to be able to inject the secret key asynchronously, then returned the config options using the `useFactory` property, just like we did for the MongooseModule in the root module. As we are in this file, we also added the PassportModule, as we will need it soon for our strategies.
    Well done, we can now use the jwt service instance worry freely.

    - a **signup** method: as you can imagine, this will contain the logic for our signup feature, here is how it looks like:

    ```tsx
    // auth.service.ts

    ...
    async signup(payload: CreateUserInput) {
    // CHECK IF THE USER ALREADY EXISTS
    const user = await this.userService.findOneByEmail(payload.email);

    if (user) {
    throw new Error('User already exists, login instead');
    }

    // GENERATE HASH PASSWORD TO SAVE
    const hash = await bcrypt.hash(
    payload.password,
    Number(this.configService.get<string>('SALT_ROUND')),
    );

    return this.userService.createUser({ ...payload, password: hash });
    }
    ...
    ```

    Same logic here, first we make sure the user doesn’t exist, then we proceed with the creation of the account(interaction with the database), we decide to let this responsability to the user.service logic file.

    As you can see here, each method above has a single responsability, we have simple methods easy to understand and that can be scaled at any moment, just like we did, creating a new module. Well done, let’s move in to the guards, and understand why we need them:

    - Into the `auth.resolver.ts` file, let’s see how we consume those logic

    ```tsx
    // auth.resolver.ts

    ...
    @Mutation(() => LoginUserResponse)
    @UseGuards(GqlAuthGuard)
    login(
    @Args('loginUserInput') loginUserInput: LoginUserInput,
    @Context() context: any,
    ) {
    return this.authService.login(context.user);
    }
    ...
    ```

    This might be a bit confusing but, let’s break it down in a second.

    We are using one of the most important feature of nest in this part, `Guards` . Guards are a way to add middleware logic to specific mutations or queries(routes or controllers for Rest APIs) in NestJS, they can be used to perform tasks such as authentication, authorization, or request validation before a request is handled by the corresponding controller.

    This also means, before running the `auth.service.login()` method, our guards will first be executed. We have used `@UseGuards` from `@nest/common` and we have passed in a custom guard called `GqlAuthGuard` that we haven’t implemented yet. How will it work?

    The guard will receive input that we pass through `loginUserInput`, then verify if the user is valid, and at the end, if the user isn’t valid, it will return an exception, if the user is valid, it will put that user in what we call `context` of the request, that context will contain information about the user who is querying the server, and in this particular use case, that user will also be the one who needs to login.

    Let’s now implement 2 additional files:

    ```tsx
    // gql-auth.guards.ts

    import { ExecutionContext, Injectable } from '@nestjs/common';
    import { GqlExecutionContext } from '@nestjs/graphql';
    import { AuthGuard } from '@nestjs/passport';

    @Injectable()
    export class GqlAuthGuard extends AuthGuard('local') {
    constructor() {
    super();
    }

    getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    const req = ctx.getContext();
    req.body = ctx.getArgs().loginUserInput;

    return req;
    }
    }
    ```

    As this us not a rest api, Nest recommend to build you own guard, that extends the `AuthGuard` We are also returning a request, that has a body property coming from the arguments we have passed `@Args('loginUserInput') loginUserInput: LoginUserInput` that’s pretty much great, we next have to implement a localStategy that will treat that context.

    Here is how our `LocalStategy` class will look like:

    ```tsx
    // auth/local.strategy.ts

    import { Strategy } from 'passport-local';
    import { PassportStrategy } from '@nestjs/passport';
    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { AuthService } from './auth.service';

    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(private authService: AuthService) {
    super({
    usernameField: 'email',
    });
    }

    async validate(email: string, password: string) {
    const user = await this.authService.validateUser({ email, password });

    if (!user) {
    throw new UnauthorizedException();
    }

    return user;
    }
    }
    ```

    First, when extending the `PassportStrategy` class, we have added the `usernameField: 'email',` property, by default, PassportStrategy only accepts username and password as parameters, but in our use case, we don’t have a username, we have an email instead, to make our LocalStrategy accept that, we need to specify that, our `usernameField` is email instead of the username.
    Then, every strategy we will implement has to contain a `validate()` method, this is what the auth-guard will execute first, missing it in our class will make it fail, make sure we have it.
    Next, we are just calling the `validateUser` method from the authService, as we saw before, it will return a valid user if the credentials were correct, or a null if not, in the second case, we throw a native nest error `UnauthorizedException();` telling the user that he’s not authorized to proceed with the request, other wise, we just return the user(or not 🤪)
    The fact is that, as you can see in our login Mutation, the data we are using for the auth.service is already part of the context `return this.authService.login(context.user);` so we can or not return a user, since it won’t be used in our use case.

    Well done, the login is completed.

    We have an additional guard implemented in the `jwt-auth.guards.ts` file which I let you guys discover by yourself, checking our `#54640`

    Now we can protect some resolvers from being queried by an unauthenticated user, let’s say for example the `createBook` mutation, if we add a guard, here is how it looks like:

    ```tsx
    // book.resolver.ts

    ...
    // Only connected users with valid jwt tokens must create a book(Authentication)
    @Mutation(() => Book)
    @UseGuards(JwtAuthGuard)
    createBook(@Args('createBookInput') createBookInput: CreateBookInput) {
    return this.bookService.createBook(createBookInput);
    }
    ...
    ```

    This way, if you are not providing a valid token in the headers, you can’t proceed. I have protected the create, update and delete mutations, as you can see in the GitHub repository.

🤩 Great job getting through that! It may have been a bit dense, but it was definitely worth it. Now that you've learned about it, don't be afraid to re-read the section or practice what you've learned on your own. Remember, even though it may be confusing at first, this is an important part of building a NestJS application.