NextAuth 로그인 구현(3) - 유효성 검사(with JWT token)

김재한·2024년 2월 21일
2
post-thumbnail
post-custom-banner

⬅️ 로그인 구현(2) - 회원가입, 아이디 로그인

게시물 조회 API 구현

/app/api/user/[id] 경로에 route.ts 파일을 만들어 준다.

API 에서도 페이지와 같이 Dynamic Route 를 사용할 수 있다.

여기서는 [id] 값을 slug로 사용한다.

//app/api/user/[id]/route.ts

import prisma from '@/app/lib/prisma'

export async function GET(
  request: Request,
  { params }: { params: { id: string } },
) {
  console.log(params)

  // 테이블에서 id 값은 Int 로 정의되어 있기 때문에 형변환
  const id = Number(params.id)

  const userPosts = await prisma.post.findMany({
    where: {
      authorId: id,
    },
    include: {
      author: {
        select: {
          email: true,
          name: true,
        },
      },
    },
  })
  return new Response(JSON.stringify(userPosts))
}

findMany 는 조건과 일치하는 데이터들을 전부 가져오는 함수이다.

prisma studio 를 통해 데이터를 수기로 입력해준다.

authorId를 필수로 입력해 주어야하며, User 테이블에 있는 id 값을 입력해준다.

게시물 조회 API 테스트

Path Valiable 로 API를 호출하면 다음과 같은 Response를 얻을 수 있다.

성공!

Token 을 사용해 Session 보호

현재는 Request url 만 알고 있으면 로그인을 하지 않아도 해당 데이터를 조회해 볼 수 있다.

Next-Auth에서는 인가되지 않은 사용자의 접근을 막기 위해 JWT Token 을 다루는 기능들을 제공한다.

☝🏻 먼저, 로그인 시 리턴되는 User 객체에 JWT Token 을 추가할 것이고

✌🏻 이 토큰을 NextAuth의 Session에 저장해 로그인 된 상태에서 사용 가능하도록 할 것이다.

💡 토큰을 세션에 저장하면 외부에 노출하지 않고 안전하게 사용할 수 있다.

🤟🏻 마지막으로 게시물 조회 API에서도 이 accessToken 이 있어야 호출될 수 있도록 변경할 것이다.

JWT 패키지 설치 및 토큰 생성🧑🏻‍⚕️

$ npm install jsonwebtoken

// type도 설치해준다.
$ npm install -D @types/jsonwebtoken

설치가 완료되었으면 .env 파일에 SECRET_KEY 환경변수를 만들어 준다.

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=topsecret
SECRET_KEY=nextauthsecretkey // << 추가

DATABASE_URL="file:./dev.db"

그 다음 app/lib 경로에 토큰을 생성하는 jwt.ts 를 만들어준다.

// app/lib/jwt.ts
import jwt, { JwtPayload } from "jsonwebtoken";

interface SignOption {
  expiresIn?: string | number;
}

const DEFAULT_SIGN_OPTION: SignOption = {
  expiresIn: "1h",
};

export function signJwtAccessToken(payload: JwtPayload, options: SignOption = DEFAULT_SIGN_OPTION) {
  const secret_key = process.env.SECRET_KEY;
  const token = jwt.sign(payload, secret_key!, options);
  return token;
}

signJwtAccessToken() 함수는 jwt.sign()를 통해 토큰을 리턴해준다.

토큰을 만들었으니 API 콜이 왔을 때 토큰의 유효성 체크를 해주는 verifyJwt() 함수도 만들어 준다.

// app/lib/jwt.ts

~~
  ~~  
  
export function verifyJwt(token: string) {
  try {
    const secret_key = process.env.SECRET_KEY;
    const decoded = jwt.verify(token, secret_key!);
    return decoded as JwtPayload;
  } catch (error) {
    console.log(error);
    return null;
  }
}
  

로그인 시 accessToken 전달

기존 api/signin/route.ts 의 로직을 일부 변경해야한다.

// api/signin/route.ts

import prisma from '@/app/lib/prisma'
import * as bcrypt from 'bcrypt'
import {signJwtAccessToken} from "@/app/lib/jwt";

interface RequestBody {
    username: string;
    password: string;
}

export async function POST(request: Request) {
    const body: RequestBody = await request.json()

    const user = await prisma.user.findFirst({
        where: {
            email: body.username,
        },
    })

    if (user && (await bcrypt.compare(body.password, user.password))) {
        const { password, ...userWithoutPass } = user

        // 토큰 생성 (추가된 부분)
        const accessToken = signJwtAccessToken(userWithoutPass);
        const result = {
            ...userWithoutPass,
            accessToken,
        };

        return new Response(JSON.stringify(result))
    } else return new Response(JSON.stringify(null))
}

Postman을 통해 로그인 api 를 호출하면 accessToken 이 추가된걸 볼 수 있다.

실제 로그인을 하게되면 콘솔로 확인할 수 있다.

💡 api 라우트는 서버사이드이기 때문에 서버 콘솔에서 확인 가능
/app/apit/auth/[...nextahtu]/route.ts에 있는 authorize() 함수의 console.log 내용이다.

authorize() 를 수정해 토큰을 Session 에 저장해보자!

// /app/apit/auth/[...nextahtu]/route.ts
import NextAuth from 'next-auth/next'
import CredentialsProvider from 'next-auth/providers/credentials'

const handler = NextAuth({
    providers: [
        CredentialsProvider({
            ...
            async authorize(credentials, req) {
          		// 로그인 로직
                ...
          
                console.log('$$user: ', user)
            },
        }),
    ],
      // 추가된 부분
    callbacks:{
        // token 정보와 user 정보를 하나의 object로 return
        async jwt({ token, user }) {
            return { ...token, ...user };
        },

        async session({ session, token }) {
            console.log('$$$ token: ', token)
            session.user = token as any;
            console.log('$$$ session: ', session)
            return session;
        },
    }
})

export { handler as GET, handler as POST }

callbacks 옵션은 로그인 로직( authorize() )을 수행한 이후 실행되는 부분이다.

jwt() 콜백 함수에서 tokenuser 정보를 object 로 만들어 리턴하면 session() 콜백 함수에 token 으로 전달된다.

session() 콜백에서는 전달받은 값을 session.user 에 넣어 리턴해주어야 Client 에서 사용이 가능하다.

클라이언트에서는 const {data: session} = useSeesion() 를 호출해 사용할 수 있다.

Session.user의 type 커터마이징 하기

위의 console 을 보면 session.useraccessToken 값이 들어가 있지만

Next-Auth 의 type을 보면 accessToken 이 정의되어있지 않다.

그렇기 때문에 type 커스터마이징이 필요하며, /app/type 경로에 next-auth.d.ts 파일을 만들어 구현한다.

// app/types/next-auth.d.ts

import NextAuth from "next-auth";
declare module "next-auth" {
  interface Session {
    user: {
      id: number;
      name: string;
      email: string;
      accessToken: string;
    };
  }
}

✅ 생겼다!

API 호출 시 accessToken 유효성 검사

게시물 조회 api인 /api/user/[id]/route.ts 를 호출 할 때 토큰을 검사하는 로직을 추가해 볼 것이다.

// /api/user/[id]/route.ts

import prisma from '@/app/lib/prisma'
import { verifyJwt } from '@/app/lib/jwt'
export async function GET(
    request: Request,
    { params }: { params: { id: string } },
) {
    // accessToken 유효성 검사 
    const accessToken = request.headers.get('authorization')
    
    if (!accessToken || !verifyJwt(accessToken)) {
        return new Response(JSON.stringify({ error: 'No Authorization' }), {
            status: 401,
        })
    }

    console.log(params)

    const id = Number(params.id)

    const userPosts = await prisma.post.findMany({
        where: {
            authorId: id,
        },
        include: {
            author: {
                select: {
                    email: true,
                    name: true,
                },
            },
        },
    })
    return new Response(JSON.stringify(userPosts))
}

request.headersauthorizationaccessToken 이 없거나

verifyJwt() 유효성 검사를 통과하지 못하면 401(Unauthorized) 에러를 리턴한다.

⭕️ 통과

❌ 차단

Middleware를 이용한 유효성 검사

App Router 에서는 Middleware 기능을 제공한다.

Middleware는 페이지가 렌더링 되기 전에 로그인 여부, 권한 체크 등의 작업을 수행할 수 있다.

활용 방법은 무궁무진하지만 이번에는 페이지 진입 전, 로그인 여부를 판단해 접근을 차단하는 작업만 하겠다.

/app 과 동일한 레벨에 middleware.ts 파일을 만들어 설정한다.

export { default } from 'next-auth/middleware'

export const config = {
  matcher: ['/userposts/:path*'],
}

config 안에 있는 matcher에 페이지 경로를 등록하면 해당 페이지는 로그인한 사용자만 접근할 수 있다.

✅ 로그인 하지않은 사용자가 접근하게되면 로그인 화면으로 리다이렉트 시킨다.

로그인 구현(4) - 커스텀 로그인 페이지 ➡️

post-custom-banner

0개의 댓글