인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

yjin0034님의 프로필 이미지
yjin0034

작성한 질문수

[코드캠프] 부트캠프에서 만든 고농축 백엔드 코스

토큰 재발급 API 실습 중 restoreAccessToken 401 에러

해결된 질문

작성

·

767

·

수정됨

0

강사님 안녕하세요?

토큰 재발급 API 실습 중에 해결되지 않는 부분이 있어 질문드려요.

 

관련 코드는 강의에서 진행하는 대로 모두 작성하였고, 마지막 실습 부분에서 막힙니다.

 

login


 

fetchUser 까지는 진행이 잘 되는데




문제는,

restoreAccessToken 부분에서 401 에러가 발생합니다.



관련 에러 명령 프롬프트 화면입니다. 

관련 쿠키 값 입니다.

(login 시도 시 쿠키 값)


코드는 실습대로 다 작성했구요.

실습도 그대로 따라하는 중 restoreAccessToken 부분만 에러가 나네요.

 

auth.resolver.ts 의 @UseGuards 데코레이터를 빼보기도 하고, 제 나름대로 해결책을 찾아보려 했는데 잘 모르겠네요. 구글링 해봐도 안되고,

혹시 제가 빼먹은 부분이나 잘못한 부분이 있을까요?

도움 부탁드립니다 :)

 

아래는 관련 제 코드들 입니다.

 

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthResolver } from './auth.resolver';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { UserService } from '../users/user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../users/entities/user.entity';
import { JwtRefreshStrategy } from 'src/commons/auth/jwt-refresh.strategy';

@Module({
  imports: [
    JwtModule.register({}), //
    TypeOrmModule.forFeature([User]),
  ],
  providers: [
    JwtRefreshStrategy, //
    AuthResolver,
    AuthService,
    UserService,
  ],
})
export class AuthModule {}

 

auth.resolver.ts

import { UnprocessableEntityException, UseGuards } from '@nestjs/common';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { UserService } from '../users/user.service';
import * as bcrypt from 'bcrypt';
import { AuthService } from './auth.service';
import { GqlAuthRefreshGuard } from 'src/commons/auth/gql-auth.guard';
import { CurrentUser } from 'src/commons/auth/gql-user.param';

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

  @Mutation(() => String)
  async login(
    @Args('email') email: string, //
    @Args('password') password: string,
    @Context() context: any,
  ) {
    // 1. 로그인(이메일이 일치하는 유저를 DB에서 찾기)
    const user = await this.userService.findOne({ 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.authService.setRefreshToken({ user, res: context.res });

    // 5. 이메일과 비밀번호 모두 일치한다면 -> accessToken(=JWT)을 만들어서 브라우저에 전달하기
    return this.authService.getAccessToken({ user });
  }

  @UseGuards(GqlAuthRefreshGuard)
  @Mutation(() => String)
  restoreAccessToken(
    @CurrentUser() currentUser: any, //
  ) {
    return this.authService.getAccessToken({ user: currentUser });
  }
}

 

auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

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

  setRefreshToken({ user, res }) {
    const refreshToken = this.jwtService.sign(
      { email: user.email, sub: user.id },
      { secret: 'myRefreshKey', expiresIn: '2w' },
    );

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

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

  getAccessToken({ user }) {
    return this.jwtService.sign(
      { email: user.email, sub: user.id },
      { secret: 'myAccessKey', expiresIn: '30s' },
    );
  }
}

 

jwt-refresh.strategy.ts

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

export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
  constructor() {
    super({
      jwtFromRequest: (req) => {
        const cookie = req.headers.cookie;
        const refreshToken = cookie.replace('refreshToken=', '');
        return refreshToken;
      },
      secretOrKey: 'myRefreshKey',
    });
  }

  validate(payload) {
    console.log(payload); // { email: c@c.com, sub: qkwefuasdij-012093sd }
    return {
      email: payload.email,
      id: payload.sub,
    };
  }
}

 

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 ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

export class GqlAuthRefreshGuard extends AuthGuard('refresh') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

 

app.module.ts

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './apis/auth/auth.module';
import { BoardModule } from './apis/boards/boards.module';
import { ProductModule } from './apis/products/product.module';
import { ProductCategoryModule } from './apis/productCategory/productCategory.module';
import { UserModule } from './apis/users/user.module';
// import { AppController } from './app.controller';
// import { AppService } from './app.service';

@Module({
  imports: [
    AuthModule,
    BoardModule,
    ProductModule,
    ProductCategoryModule,
    UserModule,
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'src/commons/graphql/schema.gql',
      context: ({ req, res }) => ({ req, res }),
    }),
    TypeOrmModule.forRoot({
      type: 'mysql', // 데이터 베이스 타입
      host: 'localhost', // local 환경으로 진행
      port: 3306, // mysql은 기본 port는 3306
      username: 'root', // mysql은 기본 user는 root로 지정
      password: 'bada332@', // 본인의 mysql password
      database: 'myproject03', // 연결할 데이터 베이스명
      entities: [__dirname + '/apis/**/*.entity.*'], // 데이터 베이스와 연결할 entity
      synchronize: true, // entity 테이블을 데이터베이스와 동기화할 것인지
      logging: true, // 콘솔 창에 log를 표시할 것인지
    }),
  ],
  // controllers: [AppController],
  // providers: [AppService],
})
export class AppModule {}

 

답변 1

1

안녕하세요. yjin0034님

문의 주신 내용을 확인했을 시, 코드에는 큰 문제가 없는 것으로 파악됩니다.
브라우저에 쿠키값이 두개가 들어오게 되어 만들어진 로직으로는 인가 부분이 제대로 실행되지 않은 것으로 파악되니 이 부분 참고하시어 브라우저 쿠키 삭제 후 다시 시도해 보시길 바랍니다. 감사합니다.

yjin0034님의 프로필 이미지
yjin0034
질문자

말씀대로 쿠키 삭제 후 재시도 해보니 정상적으로 되네요.
답변 감사합니다!

yjin0034님의 프로필 이미지
yjin0034

작성한 질문수

질문하기