Nest.js - 토큰 재발급 API

Temporary·2024년 9월 28일
0

Nods.js

목록 보기
38/39

토큰 재발급 API 구현

이번에는 토큰이 만료되었을 경우 refreshToken을 사용해 accessToken을 재발급하는 API를 만들어보자

이때, refreshToken을 가지고 인가를 해주는 단계도 거쳐야한다.

전체 로직은

  1. accessToken이 만료됨
  2. refreshToken을 통해 accessToken을 재발급 받기전에, refreshToken의 인가 과정을 진행한다.
  3. 인가가 완료되면 refreshToken을 이용하여 accessToken을 재발급 받는다.
  4. 만든 accessToken을 브라우저로 넘겨준다.


restoreAccessToken 실습

auth.resolver.ts 파일에 accessToken을 재발급하는 API을 추가하겠습니다.

// 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, //
  ) {}
  
  // API 생략
	
  //  refreshToken을 통해 accessToken을 재발급해줌
  @Mutation(() => String)
  restoreAcessToken(
    @Context() context: IContext, //
  ): string {
    return this.authService.restoreAccessToken({ user: context.req.user });
  }
}
  • 만들어진 accessToken을 브라우저로 넘겨주기 위해 AuthServicerestoreAccessToken 로직을 사용해준다. 이렇게 파일을 분리하여 비즈니스 로직을 만들어두면, 필요할 경우 언제든지 해당 비즈니스 로직을 사용할 수 있으며 중복 로직이 필요하지 않게 되는 장점이 있다.

하지만 누구든지 토큰을 재발급 받을 수는 없기에 Guard 설정을 해주어야 한다.

지난번에 GraphQL 용 AccessGuard를 만들었다.

이번에는 GraphQL 용 refreshToken을 인가하는 Guard가 필요한 것이다.

gql-auth.guard.ts 파일에 GraphQL용 GqlAuthRefreshGuard 를 추가해준다.

// gql-auth.guard.ts

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

export class GqlAuthAccessGuard extends AuthGuard('access') {
  getRequest(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context);
    return gqlContext.getContext().req;
  }
}

// 추가된 부분
export class GqlAuthRefreshGuard extends AuthGuard('refresh') {
  getRequest(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context);
    return gqlContext.getContext().req;
  }
}

이전에 미리 추가했던 부분을 보면 gql에서 guard를 직접적으로 사용하지 못하기 때문에 중간단계를 만들어줘야 했다.

현재 gql-auth.guard.ts 파일을 보면 상속받은 AuthGuard의 차이만 있을 뿐
같은 로직이 반복되고 있기 때문에 코드를 줄이기 위한 간단한 리팩토링을 해보자

gql-auth.guard.ts파일을 아래와 같이 수정해서 리팩토링 할 수 있다.

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

// 리팩토링: 반복되는 부분을 줄이기
export const GqlAuthGuard = (name) =>
  class GqlAuthGuard extends AuthGuard(name) {
    getRequest(context: ExecutionContext) {
      const gqlContext = GqlExecutionContext.create(context);
      return gqlContext.getContext().req;
    }
  };

이제 다시 auth.resolver.ts 파일에서 login API에 지금까지 만든 Guard를 적용해준다.

auth.resolver.ts 파일에 아래와 같이 Guard를 추가해준다.

// auth.resolver.ts

import { UseGuards } from '@nestjs/common';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { IContext } from 'src/commons/interfaces/context';
import { AuthService } from './auth.service';
import { GqlAuthGuard } from './guards/gql-auth.guard';

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

  @Mutation(() => String)
  async login(
    @Args('email') email: string, //
    @Args('password') password: string,
    @Context() context: IContext,
  ): Promise<string> {
    return this.authService.login({ email, password, context });
  }
	
  // 추가된 Gaurd 부분
  @UseGuards(GqlAuthGuard('refresh'))
  @Mutation(() => String)
  restoreAcessToken(
    @Context() context: IContext, //
  ): string {
    return this.authService.restoreAccessToken({ user: context.req.user });
  }
}
  • @UseGuards(GqlAuthGuard('refresh'))를 사용해서 refreshToken을 검사한다.
  • 인가에 성공하면 validate의 payload를 열어서 사용자의 정보를 반환하기 때문에 유저 정보를 꺼내올 수 있다.
    따라서, 현재 유저 정보인 context 내 user에 대한 accesstoken을 새로 만들어 authService를 통해 새로 발행한 토큰을 다시 클라이언트(프론트)에게 응답한다.

GqlAuthGuard의 방식이 변경되었기 때문에 fetchUser에서 사용한 UseGuards() 수정할 것을 잊지말자.

users.resolver.ts 파일의 fetchUserUseGuards()를 아래와 같이 수정해준다.

// users.resolver.ts
import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/guards/gql-auth.guard';
import { IContext } from 'src/commons/interfaces/context';

@Resolver()
export class UsersResolver {
  constructor(
    private readonly usersService: UsersService, //
  ) {}

  @UseGuards(GqlAuthGuard('access')) // 수정된 부분
  @Query(() => String)
  fetchUser(
    @Context() context: IContext, //
  ): string {
    // 유저 정보 꺼내오기
    console.log('================');
    console.log(context.req.user);
    console.log('================');
    return '인가에 성공하였습니다.';
  }
}


11-05-login-refresh-restoresrcapisauthstrategiesjwt-refresh.strategy.ts 파일을 만들어준다

// jwt-refresh.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

// import { KakaoStrategy } from 'passport-kakao'
// import { NaverStrategy } from 'passport-naver'
// import { GoogleStrategy } from 'passport-google'

export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
  constructor() {
    super({
      jwtFromRequest: (req) => {
        console.log(req);
        const cookie = req.headers.cookie; // refreshToken=asdlkfjqlkjwfdjkl
        const refreshToken = cookie.replace('refreshToken=', '');
        return refreshToken;
      },
      secretOrKey: '나의리프레시비밀번호',
    });
  }

  validate(payload) {
    console.log(payload); // { sub: asdkljfkdj(유저ID) }

    return {
      id: payload.sub,
    };
  }
}
  • 이번에도 동일하게 passport 모듈을 사용해준다.
  • 쿠키 내에 존재하는 refreshToken을 가져오는 함수를 만들어준다.
    • req의 헤더 내 cookies가 존재하고, cookie 안에 자동으로 첨부된 refreshToken만(refreshToken= 제외) 빼낸다.
  • req 내 헤더의 cookies를 가져오는데 만약 존재할 경우 문자열로 반환해서 발행했던 secretOrKey를 사용해 토큰을 열어준다.
  • 인가에 성공하면 validate로 넘어가 토큰의 payload를 열어서 사용자의 정보를 반환한다.


auth.service.ts 파일에 restoreAccessToken 함수를 만들어준다.

import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
import {
  IAuthServiceGetAccessToken,
  IAuthServiceLogin,
  IAuthServiceRestoreAccessToken,
  IAuthServiceSetRefreshToken,
} from './interfaces/auth-service.interface';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,

    private readonly usersService: UsersService, //
  ) {}

  async login({
    email,
    password,
    context,
  }: IAuthServiceLogin): Promise<string> {
			// 생략
  }

  // 추가된 부분
  restoreAccessToken({ user }: IAuthServiceRestoreAccessToken): string {
    return this.getAccessToken({ user });
  }

  setRefreshToken({ user, context }: IAuthServiceSetRefreshToken): void {
		// 생략
  }
}

IAuthServiceRestoreAccessToken interface도 추가해 타입을 정의해준다.

// auth-service.interface.ts

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

export interface IAuthServiceLogin {
  email: string;
  password: string;
  context: IContext;
}

export interface IAuthServiceGetAccessToken {
  user: User | IAuthUser['user'];
}

export interface IAuthServiceSetRefreshToken {
  user: User;
  context: IContext;
}

export interface IAuthServiceRestoreAccessToken {
  user: IAuthUser['user'];
}

이제 login API를 진행할 때 JwtRefreshStrategy를 사용하게 되었다.

하지만 반드시 login API뿐 아니라, 다른 API에서도 사용할 수 있다.

따라서, 마지막으로 auth.module.ts 파일에서 JwtRefreshStrategy 를 주입해줘야 한다.

// auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthResolver } from './auth.resolver';
import { AuthService } from './auth.service';
import { JwtAccessStrategy } from './strategies/jwt-access.strategy';
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';

@Module({
  imports: [
    JwtModule.register({}),
    UsersModule, //
  ],
  providers: [
    JwtAccessStrategy,
    JwtRefreshStrategy, // 추가된 부분
    AuthResolver, //
    AuthService,
  ],
})
export class AuthModule {}

이제, refreshToken을 통해 accessToken 재발행이 잘 이루어지는지 검증해보자

accessToken을 발행하자마자 만료시켜 요청을 보내보자

auth.service.ts 파일에서 accessToken expiresIn을 10s로 변경해서 생성되어 10초 만에 만료되게 변경해주고

fetchUser API를 요청할 때, 만약 10초 이내에 API를 요청하였으면 fetchUser가 이루어지며 10초 이후에 API를 요청하게 되면 fetchUser가 실패하게 될것이다.

이를 통해 지금까지 만들어준 restoreAccessToken API를 사용하여 만료시간이 10초인 accessToken 재발행이 가능한 것을 확인할 수 있다.

서버를 실행해주고 http://localhost:3000/graphql 에 접속해서 플레이그라운드에서 api 요청하고

login을 해서 accessToken을 발급받는다.

그리고 발행된 accessToken을 header에 넣어 fethUser 요청을 보내보면

만료시간 내에 요청을 보내면 fetchUser가 정상적으로 이루어진다.

10초가 지났다면 토큰이 만료되었을 것이고 이 때, 만료시간 후에 요청을 보내게되면 Unauthorized 에러가 발생한다.

이때, 로그인을 다시 하는 것이 아닌다.
로그인을 다시 해서 해결한다면, 매번 로그인을 다시 해야한다는 번거로움이 발생하게 된다.

따라서, 이제부터 refreshToken을 이용해서 accessToken을 재발행해보자.

이렇게 재발행이 가능한 이유는 cookie에 refreshToken이 저장되어 있기 때문이다.

재발행된 accessToken을 다시 header에 넣어 fetchUser 요청을 보내보면 fetchUser가 정상적으로 이루어 지는걸 확인할 수 있다.

profile
Temporary Acoount

0개의 댓글