Application - Server - Authenticaion

9/18/2021 NestJsGraphQL

# Authentication

Authentication sử dụng jwt-token

# Config

  • JWTService:

    import { JwtService } from '@nestjs/jwt';
    import { jwtCredentials } from '../config';
    
    export const jwtService = new JwtService({
      signOptions: {
        algorithm: 'RS256',
      },
      ...jwtCredentials,
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • Credential:

    import * as Path from 'path';
    import * as FS from 'fs-extra';
    
    export const jwtCredentials = {
      privateKey: FS.readFileSync(
        Path.resolve(__dirname, '..', '..', '..', 'etc', 'cert', 'private.key'),
        'utf8',
      ),
      publicKey: FS.readFileSync(
        Path.resolve(__dirname, '..', '..', '..', 'etc', 'cert', 'public.key'),
        'utf8',
      ),
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# Login:

  • GraphQL Schema: UserGraphql

    Định nghĩa input, response và mutation login. Thực hiện login bằng email và password. Sau khi login thành công sẽ tạo Access Token, Refresh Token Và publish data đến subscription notify. Các client đang lắng nge subscription này sẽ nhận được message "{user name} logged".

    ### INPUT ###
    input LoginUserInput {
        email: String!
        password: String!
    }
    
    ### TYPES ###
    type User {
        _id: String!
        name: String!
        email: String!
        userType: Int
        createdAt: String!
        updatedAt: String!
        roles: [Role]
    }
    type LoginResponse {
        token: String
        refreshToken: String
        user: User
    }
    
    ### MUTATION ###
    type Mutation {
        login(input: LoginUserInput!): LoginResponse
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
  • Resolver: UserResolver

    @Mutation(() => LoginResponseUserDto)
    async login(@Args('input') input: LoginUserDto) {
        return await this.authService.login(input);
    }
    
    1
    2
    3
    4
  • AuthService:

    Refresh Token được lưu vào db, dùng để tạo mới access token khi token này hết hạn. Xem chi tiết xử lí ở TokenService

    async login(input: LoginUserDto): Promise<LoginResponseUserDto> {
        try {
            const {email, password} = input;
    
            const user = await this.authRepository.findOne({email});
            if (!user || !(await user.matchesPassword(password))) {
                return null;
            }
    
            /* Create Access Token */
            const token = await this.tokenService.createAccessToken({
                id: user._id,
            });
    
            /* Create Refresh Token */
            const refreshToken = await this.tokenService.createRefreshToken({
                userId: user._id,
            });
    
            return {token, refreshToken, user};
    
        } catch (error) {
            logger.error(error.toString(), {});
            throw new AuthenticationError(error.toString());
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

# Verify JWT Token:

# Query & mutation:

Handle context của request tại GraphqlService: Get token từ headers, verify token và merge thông tin user vào context.

context: async ({req, res, connection}) => {
    if (connection) {
        return {
            req: connection.context,
        };
    }

    let currentUser: any;

    const {token} = req.headers;

    const excludeOperations = ['loginQuery', 'refreshTokenQuery'];

    if (req.body && !excludeOperations.includes(req.body.operationName) && token) {
        currentUser = await this.userService.findByToken(token);
    }

    return {
        req,
        res,
        currentUser,
    };
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Subscription:

Xử lí tương tự nhưng token được get từ connectionParams của WebSocketLink Apollo client.

subscriptions: {
    onConnect: async (connectionParams: any) => {
        const authToken = connectionParams.authToken;
        // extract user information from token
        const currentUser = await this.userService.findByToken(authToken);
        // return user info to add them to the context later
        return {currentUser};
    },
},
1
2
3
4
5
6
7
8
9

# GraphQl Schema directive:

Dùng directive để tạo middleware cho các operator. VD:

users: [User] @hasRole(role: "admin")
user(_id: String!): User @isAuthenticated
1
2
  • AuthenticateDirective: Sau khi handle context chỉ cần kiểm tra thông tin user.

    import { SchemaDirectiveVisitor, AuthenticationError } from 'apollo-server-express';
    import { defaultFieldResolver, GraphQLField } from 'graphql';
    
    export class AuthenticateDirective extends SchemaDirectiveVisitor {
      visitFieldDefinition(field: GraphQLField<any, any>) {
        const {resolve = defaultFieldResolver} = field;
    
        field.resolve = function(...args) {
          const { currentUser } = args[2];
    
          if (!currentUser) {
            throw new AuthenticationError('Authentication token is invalid.');
          }
    
          return resolve.apply(this, args);
        };
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • RoleDirective: Có thêm parameter role.

    export class RoleDirective extends SchemaDirectiveVisitor {
      visitFieldDefinition(field: GraphQLField<any, any>) {
        const {resolve = defaultFieldResolver} = field;
    
        const { role } = this.args;
    
        field.resolve = function(...args) {
          const {currentUser} = args[2];
    
          if (!currentUser) {
            throw new AuthenticationError('Authentication token is invalid.');
          }
    
          if (!currentUser.roles) {
            throw new AuthenticationError('Your Account has No Role.');
          }
    
          const userRoles = currentUser.roles.map(r => r.code);
    
          if (!userRoles.includes(role)) {
            return new AuthenticationError('Your Account has No Role.');
          }
          return resolve.apply(this, args);
        };
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

Series:

GitHub: https://github.com/ninhnguyen22/nestjs-angular-graphql (opens new window)