next-auth 서버 에러 받기 & session에 커스텀 데이터 넣기 & 권한에 따라 페이지 접근하기

안녕하세요. 제로초입니다.

많은 분들이 next-auth에서 한 번 고통을 겪어보셨을 것 같은데요. 아직 베타라서 그동안 좀 불안정한 게 많았으나 이제야 좀 잡혀가는 듯합니다. 그래서 강의에서는 아직 기능이 없어 다루지 못했다가 추가된 것들 세 가지를 소개해드리겠습니다.

signIn시 프론트에서 서버 에러 받기

로그인 시 서버에서는 다양한 에러를 줄 수 있습니다. 예를 들어 1. 유저가 없는 경우 2. 비밀번호가 틀린 경우 3. 기타 등등. 그런데 이런 걸 프론트에 넘겨야 상황에 맞는 메시지를 표시할 것 아니겠습니까? 근데 이 기본적인 기능이 지금까지 없다가 이제야 추가되었습니다.

auth.ts에서 다음과 같이 수정합니다.

import NextAuth, {CredentialsSignin} from "next-auth"
...
providers: [
  CredentialsProvider({
    async authorize(credentials) {
      const authResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/login`, {
        ...
      })
      // 여기 주목!!! 서버에서 에러가 발생할 때 그 에러 내용이 서버에 담겨 있을 겁니다.
      console.log(authResponse.status, authResponse.statusText)
      if (!authResponse.ok) {
        const credentialsSignin = new CredentialsSignin();
        if (authResponse.status === 404) {
          credentialsSignin.code = 'no_user';
        } else if (authResponse.status === 401) {
          credentialsSignin.code = 'wrong_password';
        }
        throw credentialsSignin;
      }

      const user = await authResponse.json()
      console.log('user', user);
      // id, name, image, email만 허용
      return {
        id: user.id,
        name: user.nickname,
        image: user.image,
      }
    },
  }),
]

이제 return null 하지말고 CredentialsSignin 에러를 throw 하면 됩니다. 에러의 속성인 code에 에러 메시지를 적으면 되는데 여기의 코드를 프론트에서 받으려면 signIn 부분을 redirect: true로 만들어주어야 합니다. redirect: false면 무조건 response.ok가 true면서 에러 정보는 없습니다.

const response = await signIn("credentials", {
  username: id,
  password,
  redirect: true, // true!!!
})

이렇게 바꾸면 이제 로그인 실패 시 페이지가 리다이렉트되면서 쿼리스트링으로 에러 코드가 같이 나오게 됩니다.

image새로고침이라 불편하긴 하지만 next.js에는 useSearchParams()라는 쿼리스트링 조회 훅이 있으므로 code 값을 가져올 수 있고 화면에 code에 따른 메시지를 표시할 수 있습니다. redirect가 된다는 불편함은 있지만 어쨌든 구현은 가능하게 됩니다.

session 객체에 커스텀 데이터 넣기

공식 문서에 따르면 현재 authorize 함수의 return에는 id, email, name, image만 넣을 수 있습니다. 이것만 해도 제한이 큰데 id는 넣을 수 있다고 하면서도 넣으면 useSession()의 데이터에서는 사라져버립니다.

  1. 그럼 id는 어디로 갔냐?

  2. 다른 커스텀 데이터는 어떻게 넣냐?

    하실 수 있는데 다음과 같이 session을 직접 만드실 수 있습니다.

auth.ts

export const {
  handlers: { GET, POST },
  auth,
  signIn,
} = NextAuth({
  pages: {
    signIn: '/i/flow/login',
    newUser: '/i/flow/signup',
  },
  callbacks: {
    async session({ session, token }) {
      console.log('session callback', session, token);
      const authResponse = await fetch(내정보를 가져오는 서버 API);
      const userData = await authResponse.json();
      (session as any).userData = userData;
      return session;
    }
  },
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        ...
        // id, name, image, email만 허용
        return {
          id: user.id,
          name: user.nickname,
          image: user.image,
        }

이렇게 callbacks 속성에 async session 메서드를 작성하시면 됩니다. 여기 안에서 내 정보를 서버로부터 한 번 더 불러오면 됩니다. 그리고 그 응답값을 session 객체에 넣어서 반환하는 겁니다.

  1. 아까 사라졌던 id는 이 메서드의 token.sub에 들어 있습니다.

  2. 여기서 return하는 값이 auth()나 useSession()의 데이터가 됩니다. authorize에서 return한 값이 최종 값이 아니라 여기서 한 번 더 수정되는 것입니다.

이렇게 하면 auth()나 useSession()에서 user.userData를 확인하실 수 있습니다.

권한에 따라 페이지 접근하기

이제 session 객체에 커스텀 데이터를 넣을 수 있게 되었으므로 권한에 따라 페이지를 접근할 수 있습니다. callbacks.session async 함수에서 role 같은 것을 서버로부터 받아서 넣어주면 됩니다. session.userData.role에 admin 또는 normal 권한이 있다고 칩시다. 그리고 role이 admin인 경우 어드민 페이지(/admin)에 접속 가능하고 normal이면 안 된다고 해봅시다. 이걸 어떻게 구현할 수 있을까요?

현재 middleware.ts는 다음과 같이 되어있는데, 이러면 config에 적은 라우트에서만 실행되므로 config를 전부 제거합니다.

import { auth } from "./auth"
import {NextResponse} from "next/server";

export async function middleware() {
  const session = await auth();
  if (!session) {
    return NextResponse.redirect('http://localhost:3000/i/flow/login');
  }
}

// See "Matching Paths" below to learn more
export const config = {
  matcher: ['/compose/tweet', '/home', '/explore', '/messages', '/search'],
} 

그리고 middleware 함수 내부에 수동으로 작성하면 됩니다. request.nextUrl.pathname에 현재 접근하고자 하는 pathname이 들어있습니다.

import { auth } from "./auth"
import {NextResponse} from "next/server";

export async function middleware() {
  const session = await auth();
  if (['/compose/tweet', '/home', '/explore', '/messages', '/search'].includes(request.nextUrl.pathname) && !session) {
    return NextResponse.redirect('http://localhost:3000/i/flow/login');
  }
}

이렇게 수정한 후 저희는 auth()의 session에서 session.userData.role로 권한에 접근할 수 있으므로 다음과 같이 검사 후 리다이렉트 시키면 됩니다.

import { auth } from "./auth"
import {NextResponse} from "next/server";

export async function middleware() {
  const session = await auth();
  if (['/compose/tweet', '/home', '/explore', '/messages', '/search'].includes(request.nextUrl.pathname) && !session) {
    return NextResponse.redirect('http://localhost:3000/i/flow/login');
  }
  if (request.nextUrl.pathname.startsWith('/admin') && session?.userData.role !== 'admin') {
    return NextResponse.redirect('http://localhost:3000/권한없음_알리는_모달_주소');
  }
}

이렇게 부족한 next-auth를 어떻게서든 활용해볼 수 있습니다.

다음에 또 업데이트 되는 사항이 있으면 알려드리겠습니다. 감사합니다!

오일중 프로필
오일중 2달 전 import NextAuth, { CredentialsSignin } from "next-auth" 여기서 CredentialsSignin <-없습니다 라고 뜨는데 next-auth 버전 몇입니까?
조현영 프로필
조현영 4일 전 5버전입니다