Skip to main content

Next-Auth-로그인 구현 (2)

작성일 : 2024.12.25

유효성 검사 With JWT Token​

/app/api/user/[id] 경로에 route.ts 파일을 만들어 준다. API 에서도 페이지와 같이 Dynamic Route 를 사용할 수 있다. 여기서는 [id] 값을 slug로 사용한다.

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

import prisma from "@/lib/prisma";

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  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));
}

slug = 동적 라우팅(Dynamic Routing)을 위해 사용되는 URL의 변수 부분

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 occured : ", error);
    return null;
  }
}

로그인 시 accessToken 전달​

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

// api/signin/route.ts

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

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 이 추가된걸 볼 수 있다.

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

// /app/api/auth/[...nextahtu]/route.ts
~
const handler = NextAuth({
  ~
  callbacks: {
    async jwt({ token, user }) {
      return { ...token, ...user };
    },
    async session({ session, token }) {
      console.log("$$$ token : ", token);
      session.user = token as never;
      console.log("$$$ session : ", session);
      return session;
    },
  },
});
~

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

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

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

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

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

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.headers 의 authorization 에 accessToken 이 없거나

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에 페이지 경로를 등록하면 해당 페이지는 로그인한 사용자만 접근할 수 있다.

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