ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • nest.js authGuard and authUser decorator
    Project using Nest.js/E-commerce App 2021. 9. 2. 18:43

    개요

       nest.js의 authGuard로 authorization을 준다. Roles decorator를 사용하여 권한을 사용자의 role에 따라 나눈다. app.module.ts에 GraphQlModule에 context를 줘서 GqlExecutionContext.create(context)가 gql의 context에 접근하게 한다.

    app.module.ts

        GraphQLModule.forRoot({
          playground: process.env.NODE_ENV !== 'production',
          autoSchemaFile: true,
    
          context: ({ req }) => {
            const TOKEN_KEY = 'authorization';
    
            let token: string = null;
            let authorization: string = null;
            if (req.headers.hasOwnProperty(TOKEN_KEY)) {
              authorization = req.headers[TOKEN_KEY];
            }
    
            if (authorization.includes('Bearer')) {
              token = authorization.split(' ')[1];
            }
    
            return {
              token,
            };
          },
        }),

      보통 customizing header key를 만들 x-<header_name>로 하는 경우가 있다. 하지만 이제는 그렇게 하지말라고 권고 하고 있다. 그리고 token key 같이 auth와 관련된 값은 미리 정의된 헤더 key 값이 있으므로 authorization을 사용하자. context에 있는 req를 사용하여 token 값을 가져온다. 그리고 그 값에 있는 토큰 value만 떼어내서 반환한다. 

    roles.decorator.ts

    import { SetMetadata } from '@nestjs/common';
    import { UserRole } from 'src/users/entities/user.entity';
    
    export type AllowedRoles = keyof typeof UserRole | 'Any';
    
    export const Roles = (roles: AllowedRoles[]) => SetMetadata('roles', roles);

       SetMetadata를 이용해서 custom metadata를 생성할 수 있다. roles.decorator.ts 모듈을 auth folder에 생성하고 @Roles()를 생성하자. @Roles는 이제 Resolver나 Controller에서 사용할 수 있다. @Roles에 할당되는 인자에 따라 부여되는 권한이 달라진다. 즉 해당 인자의 역할을 가진 user만 해당 api를 사용할 수 있다.

    auth.module.ts

    import { Module } from '@nestjs/common';
    import { APP_GUARD } from '@nestjs/core';
    import { UsersModule } from 'src/users/users.module';
    import { AuthGuard } from './auth.guard';
    
    @Module({
      imports: [UsersModule],
      providers: [
        {
          provide: APP_GUARD,
          useClass: AuthGuard,
        },
      ],
    })
    export class AuthModule {}
    • UsersModule에 있는 UsersService를 사용해야 하기 때문에 imports를 해준다. 위 Module은 Binding Guards를 위해 생성 했다. Here를 참고하라

    auth.guard.ts

    import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    import { GqlExecutionContext } from '@nestjs/graphql';
    import { JwtService } from 'src/jwt/jwt.service';
    import { UsersService } from 'src/users/users.service';
    import { AllowedRoles } from './roles.decorator';
    
    @Injectable()
    export class AuthGuard implements CanActivate {
      constructor(
        private readonly reflector: Reflector,
        private readonly jwtService: JwtService,
        private readonly usersService: UsersService,
      ) {}
    
      async canActivate(context: ExecutionContext): Promise<boolean> {
        const roles = this.reflector.get<AllowedRoles[]>(
          'roles',
          context.getHandler(),
        );
    
        if (!roles) {
          return true;
        }
    
        let token: string = null;
        const host = GqlExecutionContext.create(context);
    
        if (host) {
          if (host.getType() === 'graphql') {
            const gqlContext = host.getContext();
            token = gqlContext.token;
          } else if (host.getType() === 'http') {
            const req = host.getArgByIndex(0);
            token = req.headers['authorization'];
          }
        }
    
        if (token) {
          const decoded = this.jwtService.verify(token);
    
          if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
            const { ok, user } = await this.usersService.findById(decoded['id']);
            if (ok) {
              const gqlContext = host.getContext();
              gqlContext['user'] = user;
            }
    
            if (roles.includes('Any')) {
              return true;
            }
    
            return roles.includes(user.role);
          }
        }
    
        return false;
      }
    }

       Here에 roles .guard.ts 코드를 토대로 만들었다. 먼저 const host = GqlExecutionContext.create(context);를 보자. Graphql context를 반환해준다. 여기에는 http obj도 있을 수 있다. 있을 수 있다고 표현한 이유는 host.getType()에 따라 달라지기 때문이다. host.getType()에 따라 가져올 token의 경로가 달라진다. Client 측에서 gql api를 사용해서 토큰을 전송하냐 rest api를 이용해서 전송하냐에 따라 달라진다.

       http일 경우 host.getArgByIndex(0)는 request를 반환한다. index가 1이면 response를 반환한다.(참조) token 값을 잘 받았다면 jwtService.verify()를 이용하자 jwtService는 JwtModule에서 @Global()을 사용했으므로 굳이 AuthModule에서 import 하지 않아도 사용할 수 있다. verify가 성공하면 decoded 값을 반환해주는데 type은 object이고 여기에 user id 값이 들어 있다. user id 값으로 user를 찾을 수 있도록 userService에서 findById() 함수를 만들어줬다.

       성공적으로 user를 찾았다면 user를 gqlContext에 추가하고 user의 roles를 확인하여 그에 따라 true or false를 반환한다.

    user.module.ts

    import { Module } from '@nestjs/common';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { User } from './entities/user.entity';
    import { UsersResolver } from './users.resolver';
    import { UsersService } from './users.service';
    
    @Module({
      imports: [TypeOrmModule.forFeature([User])],
      providers: [UsersResolver, UsersService],
      exports: [UsersService],
    })
    export class UsersModule {}

    auth.guard.ts에서 UsersService를 사용하기 위해서 user.module.ts에서 UsersService를 export를 하였다.

    users.service.ts

      async findById(id: number): Promise<UserProfileOutput> {
        try {
          const user = await this.users.findOne({ id });
          return {
            ok: true,
            user,
          };
        } catch (error) {
          console.error(error);
          return {
            ok: false,
            error: 'User not found',
          };
        }
      }

    user-profile.dto.ts

    import { Field, InputType, ObjectType } from '@nestjs/graphql';
    import { CoreOutput } from 'src/common/dtos/output.dto';
    import { User } from '../entities/user.entity';
    
    @InputType()
    export class UserProfileInput {
      @Field((type) => Number)
      userId: number;
    }
    
    @ObjectType()
    export class UserProfileOutput extends CoreOutput {
      @Field((type) => User, { nullable: true })
      user?: User;
    }

    auth-user.decorator.ts

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { GqlExecutionContext } from '@nestjs/graphql';
    
    export const AuthUser = createParamDecorator(
      (data: unknown, ctx: ExecutionContext) => {
        const gqlContext = GqlExecutionContext.create(ctx).getContext();
        const user = gqlContext.user;
        return user;
      },
    );

       custom decorator다. auth.module.ts에 있는 GraphQlModule.forRoot({})에서 token 값을 가져왔고 auth.guard.ts에서 token 값을 받아와서 user를 찾아서 gqlContext['user']에 추가했다. auth-user.decorator.ts는 해당 user를 받아서 반환하는 역할을 한다.

    user.resolver.ts

    import { Query, Resolver } from '@nestjs/graphql';
    import { AuthUser } from 'src/auth/auth-user.decorator';
    import { Roles } from 'src/auth/roles.decorator';
    import { User } from './entities/user.entity';
    import { UsersService } from './users.service';
    
    @Resolver((of) => User)
    export class UsersResolver {
      constructor(private readonly usersService: UsersService) {}
      
      @Query((returns) => User)
      @Roles(['Any'])
      me(@AuthUser() user: User): User {
        return user;
      }
    }

       me resolver를 보면 @AuthUser를 사용한다. client가 정확한 token을 보냈다면 해당 user를 받을 것이고 이를 반환할 것이다.

      graphql playground에서 실험한 결과이다. header에 login 때 얻은 token 값을 전달해주니 user에 대한 data를 반환해주었다.

    참고 자료

    Github link

    https://github.com/zpskek/houpang-backend-v1/commit/3fd36234ff290cc68efe71c575605144d5de3119

    https://github.com/zpskek/houpang-backend-v1/commit/10d7dbd048d549cb5905c93e820b508e1c82a009

    댓글

Designed by Tistory.