Next Auth V5

이수빈·2024년 7월 8일
2

Next.js

목록 보기
13/15
post-thumbnail
  • Next.js + Next Auth를 이용한 교육 정리 포스트이다.

  • Next Auth 5 버전을 기준으로 작성되었다.

Next Auth

  • next.js에서 주로 사용하는 오픈소스 인증 라이브러리이다.

  • 기본적인 사용법은 다음과 같다.

auth.ts파일 만들기

  • 공식문서에는 auth.ts라고 나와있는데, 프로젝트에서 사용하는 auth config파일이라고 생각하면 편하다.

  • NextAuth() 호출로 반환되는 handlers, signIn, signOut, auth, update를 프로젝트에서 사용할 수 있다.

import NextAuth from "next-auth"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})
  • 이 NextAuth를 통해 로그인이나 회원가입이 일어났을 때의 로직을 callback을 통해 관리 할 수 있고, provider(공급자)를 설정해 여러 로그인 방식을 설정 할 수 있다.(일반로그인, google, github 등등...)

return

  • handlers: 프로젝트의 인증 관리를 위한 API 라우트(GET, POST 함수) 객체입니다.
  • signIn: 사용자 로그인을 시도하는 비동기 함수입니다.
  • signOut: 사용자 로그아웃을 시도하는 비동기 함수입니다.
  • auth: 세션 정보를 반환하는 비동기 함수입니다.
  • unstable_update(update): 세션 정보를 갱신하는 비동기 함수입니다.
  • 호출 옵션으로 다음과 같은 항목을 지정할 수 있습니다.

  • providers를 제외하면, 모두 선택 항목이다.

params

  • providers: Credentials, Google, GitHub 등의 인증 공급자를 지정합니다. (필수값)
  • session: 세션 관리 방식을 지정합니다.
  • pages: 사용자 정의 페이지 경로를 지정하며, 로그인 페이지의 기본값은 /auth/signin입니다.
  • callbacks: 인증 및 세션 관리 중 호출되는 각 핸들러를 지정합니다.
  • callbacks.signIn: 사용자 로그인을 시도했을 때 호출되며, true를 반환하면 로그인 성공, false를 반환하면 로그인 실패로 처리됩니다.
  • callbacks.redirect: 페이지 이동 시 호출되며, 반환하는 값은 리다이렉션될 URL입니다.
  • callbacks.jwt: JWT가 생성되거나 업데이트될 때 호출되며, 반환하는 값은 암호화되어 쿠키에 저장됩니다.
  • callbacks.session: jwt 콜백이 반환하는 token을 받아, 세션이 확인될 때마다 호출되며, 반환하는 값은 클라이언트에서 확인할 수 있습니다. (2번 이상 호출될 수 있습니다)
  • 코드 예시는 다음과 같다.
import NextAuth from 'next-auth'

export const {
  handlers,
  signIn,
  signOut,
  auth,
  unstable_update: update // Beta!
} = NextAuth({
  providers: [
    // ...
  ],
  session: {
    strategy: 'jwt', // JSON Web Token 사용
    maxAge: 60 * 60 * 24 // 세션 만료 시간(sec)
  },
  pages: {
    signIn: '/signin' // Default: '/auth/signin'
  },
  callbacks: {
    signIn: async () => {
      return true
    },
    jwt: async ({ token, user }) => {
      return token
    },
    session: async ({ session, token }) => {
      return session
    }
  }
})
  • 보통 callback은 다음과 같이 동작하는데, 각 callback이 동작할 때 params도 같이 전달되는 형식이다.

사용자가 로그인(회원가입) => signIn => (redirect) => jwt => session
세션 업데이트 => jwt => session
세션 확인 => session
...

API 라우트 구성

ref) 참고 : https://nextjs.org/docs/app/building-your-application/routing/route-handlers

https://velog.io/@jay/Next.js-13-master-course-router-handler#router-handler-%EC%97%90%EC%84%9C-%EB%8D%94%EB%AF%B8%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0

  • 라우트 핸들러는 app directory에서만 동작한다.

  • Route Handler는 임의의 http 요청에 대해 리스너를 시행시키는 것을 이야기하며 이 리스너에서 next.js에서 정의된 Response 객체를 반환하게 된다.

  • Auth.js는 /api/auth/ 이하 경로에서 인증을 처리한다. => next.js에서 동적경로로 route.ts파일을 매핑해줘야한다.

  • 모든 하위 경로의 동적 일치(Catch all API routes)로 라우트를 제공하고, 기본 구성에서 반환하는 handlers 객체로 라우트의 GET과 POST 함수를 매핑한다.

  • 라우트는 레이아웃이나 페이지같은 클라이언트 측 네비게이션에 참여하지 않고, page.js파일과 같은 경로에 존재 할 수 없다.

  • /app/api/auth/[...nextauth]/route.ts 이와 같이 동적으로, auth하위 경로에 대한 핸들러를 정의해준다.

/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'

export const { GET, POST } = handlers
// export const runtime = 'edge' // Optional!

미들웨어 구성

ref) https://nextjs.org/docs/app/building-your-application/routing/middleware

  • Next.js 12버전부터 추가된 내장기능 => 매 요청이 완료되기전에 middleware.ts에 있는 코드를 실행해 전처리 과정을 진행 할 수 있다. (화면에서 경로를 이동하는 요청포함)

  • root 경로 아래에 middleware.ts 파일을 정의한다.

  • 아래코드처럼 경로별 인증 여부를 확인한다고 할 때, matcher에 해당하는 경로에 올바른 권한이 있다면 , NextResponse.next()를 호출해 다음 요청으로 진행하고,

  • 아니라면 다시 redirect시키기 가능.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from 'path-to-regexp'
import { getSession } from '@/serverActions/auth' 

const matchersForAuth = [
  '/dashboard/:path*',
  '/myaccount/:path*',
  '/settings/:path*',
  '...'
]

export async function middleware(request: NextRequest) {
  if (isMatch(request.nextUrl.pathname, matchersForAuth)) {
    return (await getSession()) // 세션 정보 확인
      ? NextResponse.next()
      : NextResponse.redirect(new URL('/signin', request.url))
      // : NextResponse.redirect(new URL(`/signin?callbackUrl=${request.url}`, request.url))
  }
  return NextResponse.next()
}

function isMatch(pathname: string, urls: string[]) {
  return urls.some(url => !!match(url)(pathname))
}

서버액션 정의

ref) https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

  • Next.js 13버전부터 => app router방식에 서버컴포넌트, 클라이언트 컴포넌트, 서버액션이라는 개념이 생겨났다. 이에 대한 개념을 조금 정리해보고 가자

서버컴포넌트

ref) 서버컴포넌트 이해하기 : https://yozm.wishket.com/magazine/detail/2271/

  • Next.js는 기본적으로 ssr방식이고, 아무 선언을 하지 않았다면, 서버에서 렌더링되는 서버컴포넌트로 정의된다.

  • 클라이언트 측에서 상호작용이 없는 경우 => 서버컴포넌트로 정의, 필요한 데이터를 가져와서 ssr 방식으로 렌더링하는 방식을 통해 리소스 절약이 가능하다.

  • 기본적으로 use~~ hook을 사용 할 수 없다. 해당 hook들은 클라이언트측에서만 가능한 기능이기 때문.

// src/components/UserProfile.js

import { fetchUserData } from '../actions';

export default async function UserProfile({ userId }) {
  const user = await fetchUserData(userId);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

클라이언트 컴포넌트

  • CSR방식으로 렌더링되는 컴포넌트들이다. (기본적인 React가 동작하는 컴포넌트방식과 유사함)

  • 클라이언트 상호작용이나 hook을 사용해야 할때 'use client'로 선언해 클라이언트 컴포넌트로 사용한다.

// src/components/Counter.js

'use client'

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Server Action

  • Next.js의 서버 액션

  • 'use server' 키워드 함께 사용함 => 서버에서 실행되는 로직인데, 서버와 클라이언트 컴포넌트 모두 사용 할 수 있다.

  • 데이터베이스에서 데이터 가져오기, 양식 제출 처리, 서버측 계산 수행과 같은 작업을 처리할 수 있습니다.

  • 서버 작업은 민감한 논리와 작업이 서버에 유지되도록 하여 보안을 유지하는 데 도움이 되며 작업을 클라이언트에서 서버로 오프로드하여 성능을 향상시킬 수 있습니다.

  • 클라이언트에서만 사용 가능한 api에는 접근 할 수 없다.(window, localStroage)

  • 공통로직을 서버 클라이언트 컴포넌트에서 재사용 가능하고, 보안이 강화된다.

// src/actions.js

'use server';

export async function fetchUserData(userId) {
  // Perform server-side operation
  const userData = await getUserFromDatabase(userId);
  return userData;
}
  • 아래와같이 클라이언트, 서버에서 모두 사용할 로직을 serverAction으로 만들 수 있다
'use server'
import { auth, signIn, signOut, update } from '@/auth'

export const signInWithCredentials = async (formData: FormData) => {
  await signIn('credentials', options)
  // ...
}
export const signInWithGoogle = async () => {
  await signIn('google', options)
  // ...
}
export const signInWithGitHub = async () => {
  await signIn('github', options)
  // ...
}
export const signOutWithForm = async (formData: FormData) => {
  await signOut()
}
export {
  auth as getSession, 
  update as updateSession
}

Credentials 공급자

서버액션 정의

  • 회원가입 서버액션을 정의 후 => form에 연결함.
/serverActions/auth.tsTS

'use server'
import { auth, signIn, signOut, update } from '@/auth'

export const signInWithCredentials = async (formData: FormData) => {
  await signIn('credentials', {
    username: formData.get('username') || '', // `'null'` 문자 방지
    email: formData.get('email') || '',
    password: formData.get('password') || '',
    redirectTo: '/'
  })
}
export const signOutWithForm = async (formData: FormData) => {
  await signOut()
}
export {
  auth as getSession, 
  update as updateSession
}

//signup 

 <form
        action={signInWithCredentials}
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'start',
          gap: '10px'
        }}>
          ...
  </form>
  • authorize 함수의 credentials 매개변수는 서버 액션에서 호출한 signIn('credentials', 사용자정보) 메소드의 두 번째 인수(사용자정보)임.

  • 또한 authorize 함수는 회원가입 및 로그인에 성공한 경우, 사용자의 ID(id), 표시 이름(name), 이메일(email), 프로필 이미지(image)의 정해진 속성으로 정보를 반환해야함.

  • signInWithCredentials가 호출되면 => providers Credentails로 로그인을 시도하게 됨.

import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'

export const {
  // ...
} = NextAuth({
  providers: [
    Credentials({
      authorize: async credentials => {
        const { displayName, email, password } = credentials
        let user = { id: '', name: '', email: '', image: '' }

        // 사용자 이름이 있는 경우, 회원가입!
        if (displayName) {
          // <회원가입 로직 ...>
          return user
        }

        // <로그인 로직 ...>
        return user
      }
    })
  ],
  // ...
})

액세스 토큰 관리

  • 프로젝트에서 API를 액세스 토큰과 함께 요청해야 하는 경우, 사용자 세션 정보에 액세스 토큰을 추가할 수 있다.

  • authorize 함수에서 반환하는 사용자 정보에 accessToken 속성을 추가하고, 세션까지 전달해야함.

  • authorize 함수에서 반환하는 사용자 정보는, 로그인이 성공하면 callbacks.jwt 함수의 user 변수로 전달되고, callbacks.jwt 함수에서 반환하는 토큰 정보는 callbacks.session 함수의 token 변수로 전달됨.

  • 마지막으로 callbacks.session 함수에서 반환하는 세션 정보는 각 페이지에서 사용할 수 있다. (getSession)

  • 해당 속성은 auth 라이브러리 interface를 override해서 정의한다.

import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'

export const {
  // ...
} = NextAuth({
  providers: [
    Credentials({
      authorize: async credentials => {
        const { username, email, password } = credentials
        let user = { id: '', name: '', email: '', image: '' }

        // 사용자 이름이 있는 경우, 회원가입!
        if (username) {
          // <회원가입 로직 ...>
          return {
            ...user,
            accessToken: '<ACCESS_TOKEN>'
          }
        }

        // <로그인 로직 ...>
        return {
          ...user,
          accessToken: '<ACCESS_TOKEN>'
        }
      }
    })
  ],
  // ...

  callbacks: {
    signIn: async () => {
      return true
    },
    jwt: async ({ token, user }) => {
      if (user?.accessToken) {
        token.accessToken = user.accessToken
      }
      return token
    },
    session: async ({ session, token }) => {
      if (token?.accessToken) {
        session.accessToken = token.accessToken
      }
      return session
    },
    // ...
  }
})

Google 공급자

클라이언트 ID 및 Secret key 발급

  • 일단 google 소셜로그인을 사용하려면 => Google로부터 OAuth에 대한 동의를 받아야함.

  • 해당과정은 ref의 블로그를 참고

로그인 구현

  • 서버액션을 정의
'use server'
import { auth, signIn, signOut, update } from '@/auth'

export const signInWithGoogle = async () => {
  await signIn('google', { redirectTo: '/' })
}
export const signOutWithForm = async (formData: FormData) => {
  await signOut()
}
export {
  auth as getSession, 
  update as updateSession
}
  • provider로 Google을 사용해서 로직을 작성한다.

  • 만약 signin 함수에서 => error가 발생한다면 해당 쿼리스트링으로 redirect 시키는데, 실제로 error가 발생한 것이 아니므로 해당 에러를 처리하는 페이지를 따로 만들어야함.

  • sign in 함수가 true을 반환한다면 로그인을 시도했던 페이지로 돌아간다.

import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'

export const {
  // ...
} = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
      authorization: {
        params: {
          prompt: 'consent' // 사용자에게 항상 동의 화면을 표시하도록 강제!
        }
      }
    })
  ],
  session: {
    strategy: 'jwt',
    maxAge: 60 * 60 * 24 // 1 day
  },
  pages: {
    signIn: '/signin'
  },
  callbacks: {
    signIn: async ({ account, profile }) => {
      if (account?.provider === 'google') {
        // <사용자 확인 후 회원가입 또는 로그인...>
        return !!profile?.email_verified
      }
      
       if (error) {
        return `/error?message=${encodeURIComponent('<ERROR_MESSAGE>')}`
      } //error경로에 있는 page로 리다이렉트
      return true
    },
    jwt: async ({ token, user, trigger, session }) => {
      if (user?.accessToken) {
        token.accessToken = user.accessToken
      }
      if (trigger === 'update' && session) {
        token = { ...token, ...session.user }
      }
      return token
    },
    session: async ({ session, token }) => {
      if (token?.accessToken) {
        session.accessToken = token.accessToken
      }
      return session
    }
  }
})

ref) https://www.heropy.dev/p/MI1Khc
https://authjs.dev/getting-started/migrating-to-v5

profile
응애 나 애기 개발자

0개의 댓글