[NestJS + GraphQL + Mongoose] 비밀번호 암호화 및 로그인 API 만들기

Kylie·2022년 10월 26일
0
post-thumbnail
post-custom-banner

들어가기 전

지난 포스팅에서 GrpahQL과 Mongoose를 통해 db에 데이터를 넣는 것까지 해봤다.
테스트 데이터를 유저로 만든김에 조금 더 코드를 다듬어 보기로 했다.
아무래도 비밀번호를 입력하니깐 암호화 처리도 해보고 내친김에 로그인 기능까지 구현해보고자 한다. 로그인도 그냥 하면 재미없으니깐 JWT Token을 발행해보고자 한다.


목표

  1. 회원가입 할 때 입력받은 비밀번호를 bcryp를 사용하여 암호화 처리한다.
  2. 로그인을 하면 JWT Token을 발행한다.

지난 코드

깃허브 코드 보기


회원가입

bcryp을 사용하여 비밀번호 암호화하기

  • 암호화/복화화 하는 방법은 다양하지만 많이 사용하고 있는 bcryp를 사용해보겠다.

패키지 설치

npm install bcrypt

src/users/users.service.ts

  • 기존 코드 중 creteUser에 password를 암호화해서 받도록 수정
import * as bcrypt from "bcrypt";

...

  async createUser(user: UserInputType) {
    try {
      const saltRounds = 10; // 추가
      // CREATE DATA
      const data = {
        ...user,
        password: await bcrypt.hash(user.password, saltRounds), // 추가
        date_crated: new Date()
      };

      const result = await this.userModel.create(data);
      return {
        uid: result._id,
        ...data
      };

    } catch (e) {
      throw new ApolloError(e);
    }
  }

Graphql Playground에서 createUser 해보기

console 확인

{
  email: 'cat@test.com',
  displayName: '냥이😽',
  password: '$2b$10$sy.g98/CHQVlSSLWy1nMDOfPd0GaQsbMgOlmeiREjIvoy4a.taz9q',
  date_crated: 2022-10-01T02:26:18.399Z
}

몽고 디비 확인

  • 비밀번호가 암호화 되어 몽고 디비에 입력된 것을 확인할 수 있다.



로그인

  • 비밀번호 복호화를 테스트 하기 위해 로그인 API를 만들어 보겠다.

src/schemas/user.schema.ts

  • 기존 코드에 LoginInputType 추가
... 

@ArgsType()
@InputType()
export class LoginInputType {
  @Field()
  email: string;

  @Field()
  password: string;
}

src/users/users.service.ts

  • 기존 코드에 login() 추가하여 확인하기
import { Injectable, Inject } from "@nestjs/common";
import { User, UserInputType, LoginInputType } from "../schemas/user.schema"; //LoginInputType 추가
import { Model } from "mongoose";
import * as bcrypt from "bcrypt";
import { ApolloError } from "apollo-server-express";

@Injectable()
export class UsersService {
  constructor(
    @Inject("USER_MODEL")
    private readonly userModel: Model<User>
  ) {
  }

... 

  async login(input: LoginInputType) {
    try {
      const user = await this.userModel.findOne({ email: input.email });
      if (!user) throw new ApolloError("Please check your email");

      const check_pw = await bcrypt.compare(input.password, user.password);
      if (!check_pw) throw new ApolloError("Please check password");

      user.uid = user._id;
      return user;
    } catch (e) {
      throw new ApolloError(e);
    }
   }
  }

src/users/users.resolver.ts

import { Query, Resolver, Args, Mutation } from "@nestjs/graphql";
import { User, UserInputType, LoginInputType } from "../schemas/user.schema";
import { UsersService } from "./users.service";
import { ApolloError } from "apollo-server-express";

@Resolver()
export class UsersResolver {
  constructor(private usersService: UsersService) {
  }
  
  ...
  
  @Mutation(() => User)
  async login(@Args("input")  input: LoginInputType) {
    try {
      return await this.usersService.login(input);
    } catch (e) {
      throw new ApolloError(e);
    }
  }
 }

GrphaQL playground에서 login 해보기

💡 비밀전호를 복화하여 유저 정보를 반환하는 것을 볼 수 있다.


방금 작성한 user.service에 있는 login() 삭제


Congigue 패키지 설치

  • JWT Token을 발급하기 전에 .env 를 사용하기 위해 NestJS에서 제공하는 Congifue 패키지를 먼저 설치해보자

패키지 설치

npm i --save @nestjs/config

src/app.module.ts

  • CongifueModule import 하기
  • 나중에 context 정보를 받아오기 위해 미리 GraphQLModule에 context 정보를 작성해두자
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { UsersModule } from "./users/users.module";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ".env",
      isGlobal: true
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: "schema.gql",
      installSubscriptionHandlers: true,
      context: ({ req, connection }) => {
        if (req) {
          const user = req.headers.authorization;
          return { ...req, user };
        } else {
          return connection;
        }
      }
    }),
    UsersModule],
  controllers: [],
  providers: []
})
export class AppModule {
}

JWT TOKEN 발급

1. JWT Token 발급 코드 작성

npm 설치

$ npm install --save @nestjs/jwt passport-jwt @nestjs/passport
$ npm install --save-dev @types/passport-jwt

auth module & service 만들기

$ nest g module auth
$ nest g service auth

src/users/users.service.ts

  • 기존 코드에 findOneByEmail 함수 추가
async findOneByEmail(email: string): Promise<User | undefined> {
  try {
    return await this.userModel.findOne({ email: email });
  } catch (e) {
    throw new ApolloError(e);
  }
}

src/auth/jwt.strategy.ts

import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";


@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
  constructor(
    private readonly configService: ConfigService
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>("JWT_TOKEN")
    });
  }

  async validate(payload: any) {
    return {
      uid: payload.uid,
      email: payload.email,
      displayName: payload.displayName
    };
  }
}

src/auth/jwt-auth.guard.ts

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

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

src/auth/auth.module.ts

import { forwardRef, Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { UsersModule } from "../users/users.module";
import { PassportModule } from "@nestjs/passport";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt.strategy";
import { ConfigService } from "@nestjs/config";

@Module({
  imports: [
    forwardRef(() => UsersModule),
    PassportModule,
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>("JWT_TOKEN"),
        signOptions: { expiresIn: "1 day" }
      }),
      inject: [ConfigService]
    })

  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService]
})
export class AuthModule {
}

src/auth/auth.service.ts

import { Injectable, Inject, forwardRef } from "@nestjs/common";
import { UsersService } from "../users/users.service";
import { User } from "../schemas/user.schema";
import { JwtService } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt.strategy";
import * as bcrypt from "bcrypt";

@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
    private jwtService: JwtService
  ) {
  }

  async validateUser(email: string, pass: string): Promise<any> {
    try {
      const user = await this.usersService.findOneByEmail(email);
      if (user) {
        if (await bcrypt.compare(pass, user.password)) {
          delete user.password;
          user.uid = user._id;
          return user;
        }
      }
      return null;
    } catch (e) {
      throw e;
    }
  }

  async generateUserCredentials(user: User) {
    try {
      const payload = {
        uid: user.uid,
        email: user.email,
        displayName: user.displayName,
        photoURL: user.photoURL ? user.photoURL : "",
        intro: user.intro ? user.intro : "",
        date_crated: user.date_crated
      };
      return {
        access_token: this.jwtService.sign(payload)
      };

    } catch (e) {
      throw e;
    }
  }
}

src/users/users.modules.ts

  • AuthModule 추가
import { Module } from "@nestjs/common";
import { UsersResolver } from "./users.resovler";
import { UsersService } from "./users.service";
import { UsersProviders } from "./users.providers";
import { DatabaseModule } from "../database.module";
import { AuthModule } from "../auth/auth.module"; // 추가

@Module({
  imports: [DatabaseModule, AuthModule], //AuthModule 추가
  providers: [UsersResolver, UsersService, ...UsersProviders],
  exports: [UsersService]
})
export class UsersModule {
}

src/users/users.service.ts

  • AuthService 추가
  • login() 추가
import { Injectable, Inject } from "@nestjs/common";
import { LoginInputType, User, UserInputType } from "../schemas/user.schema";
import { Model } from "mongoose";
import * as bcrypt from "bcrypt";
import { AuthService } from "../auth/auth.service"; //추가 
import { ApolloError } from "apollo-server-express";


@Injectable()
export class UsersService {
  constructor(
    @Inject("USER_MODEL")
    private readonly userModel: Model<User>,
    private readonly authService: AuthService //추가 
  ) {
  }

  async findAll(): Promise<User[]> {
    try {
      return this.userModel.find().exec();
    } catch (e) {
      throw new ApolloError(e);
    }
  }

  async findOneByEmail(email: string): Promise<User | undefined> {
    try {
      return this.userModel.findOne({ email: email });
    } catch (e) {
      throw new ApolloError(e);
    }
  }

  async createUser(user: UserInputType) {
    try {
      // CREATE DATA
      const saltRounds = 10;
      const data = {
        ...user,
        password: await bcrypt.hash(user.password, saltRounds),
        date_crated: new Date()
      };

      const result = await this.userModel.create(data);
      return {
        uid: result._id,
        ...data
      };

    } catch (e) {
      throw new ApolloError(e);
    }
  }

  async login(input: LoginInputType) {
    try {
      const user = await this.authService.validateUser(input.email, input.password);
      if (!user) {
        throw new ApolloError("Email or password are invalid");
      } else {
        const access_token = await this.authService.generateUserCredentials(user);
        console.log("access_token", access_token);

        user.uid = user._id;
        user.access_token = access_token.access_token;

        console.log(user);
        return user;
      }
      return null;

    } catch (e) {
      throw new ApolloError(e);
    }
  }
}

2. GraphQL Playgroud에서 login() 해보기

💡 access_token이 제대로 발급된 것을 확인할 수 있다.

profile
올해보단 낫겠지....
post-custom-banner

0개의 댓글