Nest.js - 로그인 API 확장

Temporary·2024년 9월 28일
0

Nods.js

목록 보기
37/39

로그인 API 확장

Login ( + JWT기반 refreshToken 쿠키에 저장하기 )

이번에는 refreshToken을 발행하면서 cookie에 refreshToken이 잘 들어가는지 확인해 보고 토큰이 만료되었을 때의 에러를 확인해 보자

auth.resolver.ts 파일을 다음과 같이 수정해준다.

// auth.resolver.ts

import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { IContext } from 'src/commons/interfaces/context';
import { AuthService } from './auth.service';

@Resolver()
export class AuthResolver {
  constructor(
    private readonly authService: AuthService, //
  ) {}

  @Mutation(() => String)
  async login(
    @Args('email') email: string, //
    @Args('password') password: string,
    @Context() context: IContext, // 추가된 부분 - context 
  ): Promise<string> {
    return this.authService.login({ email, password, context });
  }
}
  • @Context() : Request와 Response, header 등에 대한 정보들이 context에 존재한다.

    따라서 데코레이터를 통해 해당 context 정보를 가지고 올 수 있도록 설정해줘야 한다.

그리고 app.module 파일을 아래와 같이 수정해 주세요.

// app.module.ts
@Module({
  imports: [
    AuthModule,
    BoardsModule,
    ProductsModule,
    ProductsCategoriesModule,
    UsersModule,
    ConfigModule.forRoot(),
    
    // 추가해야할 부분
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'src/commons/graphql/schema.gql',
      context: ({ req, res }) => ({ req, res }), // req는 기본적으로 들어오지만, res는 이걸 작성해야만 들어옴
    }),
    
    
    TypeOrmModule.forRoot({
      type: process.env.DATABASE_TYPE as 'mysql',
      host: process.env.DATABASE_HOST,
      port: Number(process.env.DATABASE_PORT),
      username: process.env.DATABASE_USERNAME,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_DATABASE,
      entities: [__dirname + '/apis/**/*.entity.*'],
      synchronize: true,
      logging: true,
    }),
  ],
})
export class AppModule {}

위와같은 설정을 통해 graphql로 들어온 req와 res를 API 들에서 사용할 수 있게끔 설정할 수 있다.

auth.service.ts 파일에 setRefreshToken 이라는 비즈니스 로직을 추가해준다.

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,

    private readonly usersService: UsersService, //
  ) {}

  async login({
    email,
    password,
    context,
  }: IAuthServiceLogin): Promise<string> {
    // 1. 이메일이 일치하는 유저를 DB에서 찾기
    const user = await this.usersService.findOneByEmail({ email });

    // 2. 일치하는 유저가 없으면?! 에러 던지기!!!
    if (!user) throw new UnprocessableEntityException('이메일이 없습니다.');

    // 3. 일치하는 유저가 있지만, 비밀번호가 틀렸다면?!
    const isAuth = await bcrypt.compare(password, user.password);
    if (!isAuth) throw new UnprocessableEntityException('암호가 틀렸습니다.');

    // 4. refreshToken(=JWT)을 만들어서 브라우저 쿠키에 저장해서 보내주기
    this.setRefreshToken({ user, context });

    // 5. 일치하는 유저도 있고, 비밀번호도 맞았다면?!
    //    => accessToken(=JWT)을 만들어서 브라우저에 전달하기
    return this.getAccessToken({ user });
  }
	
  // 추가된 부분
  setRefreshToken({ user, context }: IAuthServiceSetRefreshToken): void {
    const refreshToken = this.jwtService.sign(
      { sub: user.id },
      { secret: '나의리프레시비밀번호', expiresIn: '2w' },
    );

    // 개발환경
    context.res.setHeader(
      'set-Cookie',
      `refreshToken=${refreshToken}; path=/;`,
    );

    // 배포환경
    // context.res.setHeader('set-Cookie', `refreshToken=${refreshToken}; path=/; domain=.mybacksite.com; SameSite=None; Secure; httpOnly`);
    // context.res.setHeader('Access-Control-Allow-Origin', 'https://myfrontsite.com');
  }

  getAccessToken({ user }: IAuthServiceGetAccessToken): string {
    return this.jwtService.sign(
      { sub: user.id },
      { secret: '나의비밀번호', expiresIn: '1h' },
    );
  }
}
  • login로직 내에서 setRefreshToken 로직을 실행해 context 안에 존재하는 res 객체의 cookie에 refreshToken 토큰을 넣어준다.
    • refreshToken은 return을 사용해서 프론트로 보내주는 것이 아니라
      요청(req)에 대한 응답(res)으로, res 안에 cookie가 들어있는 채로 프론트엔드로 보내주게 된다.
    • 즉, refreshToken은 cookie를 통해 받게 되고 accessToken은 payload를 통해 받게 된다.
  • setRefreshToken 로직
    • JwtService의 sign 메서드를 사용해 refreshToken을 발급해준다.
      • secret 는 가급적 AccessToken 비밀번호와 다르게 설정하는 것이 일반적이다.
      • 항상 refreshToken의 expire 시간은 accessToken의 expire 시간 보다 길어야 한다.
    • 개발할 때는 받아온 res내 header(res.setHeader())의 cookie 부분('Set-Cookie’)에 refreshToken을 추가해 준다.
      • 프런트로 보내줄 때, cookie를 통해 refreshToken을 전달하게 된다.
    • refreshToken 만드는 방법이 개발 환경과 실 서비스로 배포할 때는 다르다.
      • 실 서비스 배포 시에는 보안 옵션들을 추가적으로 설정해 주어야 해킹 위험에서 안전하기 때문이다.
    • setRefreshToken 함수에 대한 return 타입은 결괏값을 반환하지 않아도 되기에 void 로 설정한다.
    • { user, res } 에 대한 타입은 IAuthServiceSetRefreshToken을 import한다.
      • authinterfacesauth-service.interface.ts 파일에 IAuthServiceSetRefreshToken을 아래와 같이 만들어서 타입을 정의해준다.
// auth-service.interface.ts

import { User } from 'src/apis/users/entities/user.entity';
import { IContext } from 'src/commons/interfaces/context';

// 추가된 부분
export interface IAuthServiceSetRefreshToken {
  user: User;
  context: IContext; 
}
  • IContext도 express에서 타입을 쓰도록 다음과 같이 수정해준다.
// context.ts

import { Request, Response } from 'express';

export interface IAuthUser {
  user?: {
    id: string;
  };
}

export interface IContext {
  req: Request & IAuthUser;
  res: Response;
}

서버를 실행하고 http://localhost:3000/graphql 에 접속해서 플레이그라운드에서 api 요청을 해보면 아래와 같이 나온다.

먼저 login을 진행해보면

브라우저상에서 쿠키 안에 refreshToken이 쿠키에 넣어져온걸 확인할 수 있다.

현재의 쿠키는 저장될 예정인 쿠키들을 보여준다.

실제 저장된 쿠키Application 탭에서 확인 가능하다.

cookie에서 refreshToken 확인이 안된다면?
🚨 만약 refreshToken이 들어오지 않는다면 플레이그라운드 설정을 다음과 같이 설정해준다.

graphql playground의 setting 값을 적용해야 header의 cookies 값을 확인할 수 있다.
( 플레이그라운드 설정은 오른편 상단 설정⚙️을 클릭하면 설정 화면으로 들어갈 수 있으며, 설정 후 반드시 저장(SAVE SETTINGS) 해야한다. )

profile
Temporary Acoount

0개의 댓글