본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


앵귤러, 리엑트, 뷰

Next-auth과 401

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2025. 5. 9.

jwt 방식을 사용하여 로그인 기능을 구현하면 리프레시토큰과 엑세스토큰을 사용한 "엑세스토큰 만료 후 갱신"을 하게 됩니다.

물론..보안에 대해서 그냥 넘어가려면 리프레시토큰을 안 쓰는 곳 도 있겠지만요.

 

next-auth에서 일반적으로 소셜 로그인을 사용하지 않고 사용자가 구현한 API 서버를 활용하여 로그인 기능을 구현하려면 일반적으로 CredentialsProvider 함수를 사용 합니다.

아래 코드는 providers의 한 부분 입니다.

 {
  providers: [
    // 아래 방식이 소셜 로그인! 
    // GoogleProvider({
    //   clientId: process.env.GOOGLE_ID,
    //   clientSecret: process.env.GOOGLE_SECRET,
    // }),
    // 요 방식이 소셜로그인 이외의 다른 로그인서버와의 연동!
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        username: { label: "Username", type: "text" },
        password: { label: "Password", type: "password" },
      },
      //요청 샘플 입니다.
      async authorize(credentials) {
      //여기에 api 서버 붙어서 정보를 가져와서 결과를 반환함.
        const requestResult = await fetch(
          `${process.env.API_SERVER_URL}/auth/login`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              username: credentials.username,
              password: credentials.password,
            }),
          },
        );
        const user = await requestResult?.json();  //결과를 원하는 형태로 조립해서 반환!
        return user;
      },
    }),
  ],
};

 

위 코드는 next-auth의 singIn 함수를 실행할 때 username과 password 라는 필드 값을 받아서  CredentialsProvider 함수가 동작하도록 정의한 내용 입니다.

이런 방식으로 API서버에서 사용자 정보를 확인한 뒤 JWT값을 발행하게 됩니다.

여기서 주의해야되는 점은,

api 서버의 JWT값이 아니라 next-auth에서 만들어 준 JWT값을 사용하게 되는 것 이므로 위 코드에서 발행한 JWT 값이 api 서버값과 같다는 생각은 저어어얼대 해서는 안 됩니다.

그래서 아래와 같은 방법을 사용 합니다.

//1. 클라이언트에서 JWT 발행 후 API서버에게 전달
가. 클라이언트에서 api서버로 호출
나. 결과가 성공이면 클라이언트에서 다시 api 서버에게 JWT값을 전달

//2. 클라이언트에서 JWT를 사용하고 서버의 JWT는 따로 관리
가. 클라이언트에서 api서버로 호출
나. 결과가 성공이면 서버에서 준 엑세스, 리프레시 토큰을 본인의 JWT토큰 필드에 추가
다. 미들웨어에서 api서버 호출 시 알아서 토큰을 넣어줌

 

두가지 방법이 있긴한데, 여기서는 2번째 방법을 사용 하였습니다.

"나" 단계는 아래처럼 엑세스토큰과 리프레시 토큰을 할당하였습니다.

이거 만약 안되면 auth 객체에 타입정의가 안되어서 그런겁니다! d.ts파일을 추가해야 합니다!!!!

 

이제 이렇게 추가된 필드가 미들웨어에서 알아서 동작해서 알아서 헤더에 엑세스 토큰을 넣어주는 것 까지 무난하게 되었는데..

문제는 미들웨어에서는 api서버에게 요청과 응답을 잡을 수는 있지만, 401 상태를 못 잡는 것 이였습니다.

그건 nextjs의 어쩔 수 없는 스팩이라고 합니다.

저기 401 조건이 뭘 해도 가지를 못 했습니다.

 

결국에는 클라이언트에서 401상태를 확인하고 다시 요청하는 기능이 들어가야 했기에..

fetch를 사용하다가 부랴부랴 커스텀 fetch함수를 만들었습니다.

커스텀 fetch 함수는 아래 단계를 따르게 됩니다.


1. 사용자가 정의한 요청을 수행 합니다.
2. 401을 감지 합니다.
3. nextjs의 api를 구현하여 api서버에게 리프레시 토큰을 가지고 엑세스 토큰을 받아 옵니다.
4. next-auth의 signin함수를 재 실행 합니다.


 

3번부터가 중요한데, 클라이언트의 행동과 서버의 행동을 구분하기위해 nextjs의 서버사이드가 필요 합니다 (이건 개취..)

물론 클라이언트에서 해도 되는데 그럴 경우 클라이언트 코드가 노출되므로...api 디렉토리에 저는 추가 하였습니다.

nextjs의 api 디렉토리에 아래와 같은 route 파일을 추가 해 주었습니다.

* 경로 : api/auth/refresh/route.ts

import { NextRequest, NextResponse } from "next/server";
import { CommonResponse } from "@/app/ddd/domain/CommonResponse";
import { getToken, decode, encode } from "next-auth/jwt";

export async function GET(request: NextRequest) {
  try {
    //자신의 토큰을 가지고 옴
    const token = await getToken({
      req: request,
      secret: process.env.NEXTAUTH_SECRET,
      raw: true,
      cookieName: "next-auth.session-token",
    });

    //토큰 디코딩
    const savedValue: any = await decode({
      token: token,
      secret: process.env.NEXTAUTH_SECRET || "",
    });

    //api 서버에게 refreshToken을 보내서 accessToken을 받아옴
    const refreshResult = await fetch(
      `${process.env.API_SERVER_URL}/auth/refresh-token`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${savedValue?.serverAccessToken}`,
          cookie: `refreshToken=${savedValue?.serverRefreshToken}`,
        },
      },
    );

    //응답 결과를 반환함
    const { result } = await refreshResult.json();
    const response = NextResponse.json({
      success: true,
      data: {
        accessToken: result?.accessToken,
        refreshToken: savedValue?.serverRefreshToken,
      },
    });
    return response;
  } catch (error: any) {
    console.error(error);
    return NextResponse.json(
      new CommonResponse({
        success: false,
        error: "Internal server error",
        details: error.message,
      }),
    );
  }
}

 

next-auth에서 받은 jwt토큰에서 api서버에서 준 엑세스와 리프레시 토큰을 가져와 api서버에게 토큰을 다시 받습니다.

다시 받은 토큰을 가지고 위에서 설명한 4번단계를 진행 합니다.

아래 사진은 제가 정의한 fetch함수 입니다.

 

기존의 next-auth에서 정의한 크레디셔널 항목은 username과 password 2개 입니다.

저 위에 CredentialsProvider 함수 부분을 보면 username과 password 2개만 정의 하였습니다.

그런데 이 사진에서의 코드는 accessToken이라는 값과 refreshToken 이라는 값 을 추가로 보내고 있습니다.

그러므로 저 위에 CredentialsProvider 함수 가 아래처럼 수정되어야 합니다.

 {
  providers: [
    // 요 방식이 소셜로그인 이외의 다른 로그인서버와의 연동!
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        username: { label: "Username", type: "text" },
        password: { label: "Password", type: "password" },
        accessToken: { label: "Access Token", type: "text" },  //신규 추가!!
        refreshToken: { label: "Refresh Token", type: "text" },  //신규 추가!!
      },
      //요청 샘플 입니다.
      async authorize(credentials) {
        //아래 if 조건이 일반 로그인이 아닌 401에 의한 로그인 입니다.
        if (credentials.accessToken){
        
        } 
        //아래 else 조건이 일반 로그인 입니다.
        else {
        
        }
        return ...;
      },
    }),
  ],
};

 

위 샘플 코드처럼 authorize 하는 부분에서 조건문을 통하여 해당 로그인이 리프레시 행위인지 일반 로그인 행위인지를 결정해야 합니다.

진행 하였던 내용을 정리하여 보면,

 

0. next-auth에서 사용자 정의를 위한 d.ts 파일을 추가하여 줍니다.

  -> 이해가 안되면 : https://github.com/TaeSeungRyu/mono-repo/blob/main/apps/client/app/types/next-auth.d.ts

1. 사용자 정의 fetch 함수(axios, http 기타 등등 다른 걸 써도 무방)을 추가 합니다.

2. nextjs api 디렉토리에 api서버에게 리프레시 요청을 할 route를 추가 합니다.

  2-1. 로그인한 token 값에서 리프레시 토큰을 가져와 쿠키에 넣어 줍니다.

  2-2. 결과를 반환 합니다.

3. fetch 함수 에 401과 관련된 조건을 추가 합니다.

  3-1. 401인 경우 2번을 요청 합니다.

  3-2. 성공인 경우 next-auth의 signIn함수를 실행 합니다.

  3-3. signIn결과가 성공이면 원래 하려던 요청을 재 실행 합니다.

4. next-auth에서 크레디셔널 프로바이더(CredentialsProvider) 에 access, refresh 토큰을 받는 부분을 추가 합니다.

  4-1. access 값이 있으면 갱신을 의미하므로 그에 맞는 행동을 정의 합니다.

  4-2. access 값이 없으면 일반 로그인을 의미 하므로 그에 맞는 행동을 정의 합니다.

5. 미들웨어에서 api서버로 요청인 경우 로그인한 token 값에서 엑세스 토큰을 가져와 헤더에 넣어 줍니다.

 

이정도 단계를 밟았던 것 같습니다.

위 관련된 코드는 아래 모노레포의 client 부분에서 확인 가능 합니다.

https://github.com/TaeSeungRyu/mono-repo

 

GitHub - TaeSeungRyu/mono-repo: 모노레포 연습 리파지토리 입니다!

모노레포 연습 리파지토리 입니다! Contribute to TaeSeungRyu/mono-repo development by creating an account on GitHub.

github.com

 

생각보다 복잡했던 next-auth에서의 401 접근 방법 이였습니다.

next-auth에서 토큰 갱신, 발행에 대한 모든 행위는 nextjs에서 서버사이드가 아니라 클라이언트에서 하도록 정의가 되어 있습니다.

그러므로 번거롭더라도 위 단계를 밟아야 손쉬운 401 해결이 가능하여 보입니다.

 

틀린점 또는 잘못된 부분은 언제든 연락 부탁드립니다!

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글