[Next.js] authjs(NextAuth)를 활용한 인증 기능 구현하기(1) : 구글 로그인, 조건부 라우트

문지은·2024년 1월 24일
7

Next.js - App Router

목록 보기
12/20
post-thumbnail

authjs 라이브러리 (구. NextAuth)를 사용하여 구글 로그인 기능을 구현하고, middleware를 사용하여 조건부 라우트를 설정하는 방법에 대해 알아보자.

NextAuth 설정

  • Setup with OAuth 클릭하면 공식문서를 확인할 수 있다.

  • 공식 문서에 따라 먼저 프로젝트에 인증 기능에 필요한 패키지를 설치한다.
npm install next-auth
  • 설치가 완료되면 인증을 처리할 API 라우트 핸들러 추가
    • api/auth/[…nextauth] 폴더 생성 후 해당 폴더에 route.ts 파일 생성
      • /api/auth 로 시작하는 모든 경로에 대한 요청 처리 가능
    • handler 변수를 선언하고 next-auth가 리턴하는 값을 할당
    • api/auth 경로에 들어오는 GET 요청과 POST 요청을 처리할 수 있도록 TypeScript as 문법을 사용해 export

api/auth/[…nextauth]/route.ts

import NextAuth from "next-auth";

const handler = NextAuth({});

export { handler as GET, handler as POST };
  • 환경 변수 두 가지를 추가해야 한다.
    • NEXTAUTH_URL : 웹 사이트 기본 주소 할당
    • NEXTAUTH_SECRET : 인증키를 암호화하고 서명하는데 사용할 암호 문자열
  • 아래 명령어를 통해 암호 문자열을 생성할 수 있다.
    • OpenSSL 이라는 도구로 임의의 바이트 시퀀스 생성
    • rand : 무작위 값 생성
    • base64 : Base64 알고리즘 사용하여 인코딩
    • 32 : 문자열 크기 (바이트)
openssl rand -base64 32
  • 생성한 문자열을 NEXTAUTH_SECRET 환경 변수로 사용한다.

.env

NEXTAUTH_URL="http:localhost:3000"
NEXTAUTH_SECRET="This is an example"

Google Provider

  • authjs 사이트의 API Reference 탭에서 authjs 에서 제공하는 provider를 확인할 수 있다.

  • provider는 사용자 인증을 위해 사용되는 서비스나 방법을 의미한다.
    • authjs 를 사용하면 애플을 비롯한 전 세계의 다양한 로그인 서비스를 연동할 수 있다.
      • 그 중에서 구글 계정으로 로그인하는 기능을 구현해보자!
  • 좌측 메뉴에서 provider/google 항목을 클릭하면 설정 방법에 대한 문서를 확인할 수 있다.

  • 설정 방법에 필요한 속성 값을 입력하기 위해서는 먼저 구글 클라우드 플랫폼에서 애플리케이션을 등록해야 한다.

구글 클라우드 플랫폼에 앱 등록

구글 클라우드 플랫폼 계정을 생성하고 설정을 진행해보자.

  • 구글 프로젝트 생성 후 생성한 프로젝트로 이동
    • Project Name : My Next App

  • OAuth 동의 화면 설정
    • User Type : 외부 (누구나 구글을 통해 로그인할 수 있는 공개 웹사이트)

앱 등록 수정

  • OAuth 동의 화면 - 필수 항목 (앱 이름, 사용자 지원 이메일) 입력

  • 범위 - 저장 후 계속

  • 테스트 사용자 - ADD USERS 버튼 눌러 사용자 지원 이메일 추가

  • 요약 - 내용 확인 후 대시보드로 돌아가기

OAuth Client ID 생성

  • OAuth ?
    • Open Authentication 의 약자로 구글, 페이스북, 트위터 등에서 사용되는 표준 인증 프로토콜
    • 사용자가 구글을 통해 로그인 하려는 경우, 애플리케이션은 사용자를 구글로 리디렉션하고 이후 구글에서 제공되는 인증 절차를 통해 사용자를 식별
    • 정상 식별된 경우 애플리케이션에 정상 식별되었다는 응답 전달
  • 좌측 사용자 인증 정보 탭에서 OAuth Client ID를 생성해보자.

  • 아래 항목 입력 후 OAuth Client ID 생성
    • 애플리케이션 유형 : 웹 애플리케이션
    • 앱 이름 : My Next App
    • 승인된 자바스크립트 원본 : 프로젝트 루트 경로 ( 여기서는 http://localhost:3000)
    • 승인된 리디렉션 URI : 구글 인증 리다이렉트 경로
      • For production: https://{YOUR_DOMAIN}/api/auth/callback/google
      • For development: http://localhost:3000/api/auth/callback/google

  • 생성된 정보 환경 변수에 추가

.env

GOOGLE_CLIENT_ID="YOUR CLIENT ID"
GOOGLE_CLIENT_SECRET="YOUR CLIENT SECRET"

Google Provider 설정

  • api/auth/[…nextauth]/route.ts 파일로 이동하여 공식 문서를 참고하여 코드 구성
    • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET 뒤에 느낌표 를 붙인 이유는 provider 속성 값이 undefined 를 허용하지 않기 때문
      • TypeScript 컴파일러에게 확실히 값이 있음을 확신시키는 목적으로 정의

api/auth/[…nextauth]/route.ts

import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
});

export { handler as GET, handler as POST };

로그인 링크 추가

  • 이제 네비게이션 컴포넌트에 로그인 링크를 추가해보자.

app/NavBar.tsx

import React from "react";
import Link from "next/link";

const NavBar = () => {
  return (
    <div>
      <Link className="mr-5" href="/">
        Next.js
      </Link>
      <Link href="/users">Users</Link>
      <Link href="/api/auth/signin">Google Login</Link>
    </div>
  );
};

export default NavBar;

  • 구글 로그인 위젯 출력 확인

Authentication Session

  • 사용자가 로그인에 성공하면, authjs는 해당 사용자를 위한 인증 세션을 생성한다.
    • 기본적으로 이 세션은 JSON Web Token 으로 표현되고, 쿠키에 저장된다.
    • 쿠키는 클라이언트와 서버 간의 매 요청에 교환되는 작은 정보 조각이다.
      • 클라이언트가 서버로 요청을 보낼 때마다 이러한 쿠키 또는 정보 조각이 서버로 전송된다.
  • 구글 로그인을 한 후에 개발자 도구를 열고 애플리케이션 탭으로 이동해 쿠키를 확인해보면, next-auth.session-token을 확인할 수 있다.
    • JSON Web Token 값 이며, 클라이언트 자체를 식별하는 데 사용되는 일종의 신분증이다.

  • authjs는 위와 같은 JSON Web Token 을 디코딩할 수 있는 방법을 제공한다.

한번 확인해보자.

  • 먼저, auth 폴더에 token 이라는 폴더를 생성하고 route.tsx 파일을 생성한다.
    • next-auth/jwt 에서 제공하는 getToken 함수를 사용하여 JSON 형태로 token 을 리턴하는 함수 작성

app/api/auth/token/route.tsx

import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const token = await getToken({ req: request });
  return NextResponse.json(token);
}
  • http://localhost:3000/api/auth/token 에 접속하여 확인해보면, JSON Web Token 으로부터 ****현재 로그인된 사용자의 정보가 디코딩 된 것을 볼 수 있다.
    • 이 정보는 구글로부터 수신한 정보이고, sub, iat 는 발행시간을 나타내며 기본적으로 토큰은 30일 동안 유효하다.
    • 로그인된 클라이언트는 매 요청마다 이 토큰을 서버에 전달하고 서버를 이 토큰을 통해 로그인 여부를 검증한다.

세션 접근하기

Client Session Access

  • 웹사이트에서 사용자가 로그인하면, 네비게이션 바의 로그인 버튼이 로그아웃 으로 바뀐다.
    • 이는 웹 사이트가 사용자의 로그인 상태를 추적하여 사용자 인터페이스(UI)를 그에 맞게 조정하는 것
    • 이렇게 클라이언트 컴포넌트가 세션에 접근해 각 상황에 맞게 렌더링할 수 있다.
  • 클라언트에서 인증 세션에 접근하려면, root layout 에서 애플리케이션을 SessionProvider 로 감싸주어야 한다.
    • Session Provider는 내부적으로 React Context를 사용해 Session을 컴포넌트 트리 혹은 자식 컴포넌트로 전달

app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "./NavBar";
import { SessionProvider } from "next-auth/react";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" data-theme="winter">
      <body className={inter.className}>
        <SessionProvider>
          <NavBar />
          <main className="p-5">{children}</main>
        </SessionProvider>
      </body>
    </html>
  );
}
  • 이후 브라우저를 확인하면 오류 발생

  • 서버 컴포넌트에서 클라이언트 컴포넌트에서만 사용할 수 있는 컨텍스트에 접근했기 때문
    • 최상단에 use client 지시문을 추가하고 다시 테스트하면 또 다른 오류 발생 ^^;
    • 컴포넌트에서 클라이언트 전용 기능을 사용하면서 동시에 데이터를 내보내려고 해서 발생한 것

  • 별도의 SessionProvider 컴포넌트를 만들어서 해결할 수 있다.
  • AuthProvider 컴포넌트 생성

app/auth/provider.tsx

"use client";

import { ReactNode } from "react";
import { SessionProvider } from "next-auth/react";

const AuthProvider = ({ children }: { children: ReactNode }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

export default AuthProvider;
  • layout 파일 수정
    • SessionProvider 대신에 AuthProvider 사용
    • 이전에 작성했던 use client 지시문 삭제

app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "./NavBar";
import AuthProvider from "./auth/provider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" data-theme="winter">
      <body className={inter.className}>
        <AuthProvider>
          <NavBar />
          <main className="p-5">{children}</main>
        </AuthProvider>
      </body>
    </html>
  );
}
  • 이제 오류가 발생하지 않는다.

  • 작성한 AuthProvider 는 자식 요소인 내비게이션 컴포넌트로 세션 정보를 제공한다.
    • 세션 정보를 전달받은 자식 컴포넌트는 useSession 훅을 사용하여 세션에 접근할 수 있다.
    • useSession 훅이 반환하는 객체에는 세션의 상태를 나타내는 status와 데이터를 포함하는 data 프로퍼티가 포함되어 있다.

클라이언트 컴포넌트 NavBar에서 직접 확인해보자!

  • 가독성을 높이기 위해 useSession 가 반환하는 data 속성을 session으로 수정하겠다.
  • status 에는 3가지 상태값이 존재한다.
    • authenticated: 인증됨
    • loading: 로딩중
    • unauthenticated : 비인증됨
  • 이제 이를 활용해 상태값이 비인증된 상태인 경우에만 로그인 링크가 렌더링 되도록 수정한다.

app/NavBar.tsx

"use client"; // 클라이언트 컴포넌트

import React from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";

const NavBar = () => {
  const { status, data: session } = useSession();

  // 로딩 중인 경우
  if (status === "loading") {
    return null;
  }

  return (
    <div className="flex">
      <Link className="mr-5" href="/">
        Next.js
      </Link>
      <Link href="/users" className="mr-5">
        Users
      </Link>
      {/* 인증 된 경우 사용자 이름 출력 */}
      {status === "authenticated" && <div>Hello {session.user!.name}</div>}
      {/* 인증되지 않은 경우 로그인 버튼 출력 */}
      {status === "unauthenticated" && (
        <Link href="/api/auth/signin">Google Login</Link>
      )}
    </div>
  );
};

export default NavBar;

Server Session Access

  • 이번에는 서버 컴포넌트에서 세션 데이터에 접근하는 방법에 대해 알아보자.
  • authjs 에서 제공하는 GetServerSession 함수를 사용한다.
    • 함수 호출 시, 인증 옵션을 전달하여야 하는데 이는 NextAuth 설정에 사용한 객체를 사용하면 된다.
      • 설정 객체를 import 해서 사용할 수 있도록 설정 파일 수정

api/auth/[…nextauth]/route.ts

import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
  • app 폴더의 page 파일에 GetServerSession 함수를 호출하고 session 가져오기
    • 세션이 존재하는 경우 사용자 이름을 렌더링 하도록 코드 작성

app/page.tsx

import Link from "next/link";
import ProductCard from "@/components/ProductCard";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";

export default async function Home() {
  const session = await getServerSession(authOptions);
  return (
    <main>
      <h1>{session && <span>{session.user!.name}</span>} 반갑습니다.</h1>
      <Link href="/users">Users</Link>
      <ProductCard />
    </main>
  );
}
  • 로그인된 사용자 이름이 잘 출력되는 것을 볼 수 있다.

로그아웃 구현하기

  • authjs 공식문서에서 제공하는 API 주소를 사용하여 로그아웃 기능을 구현할 수 있다.

  • NavBar 컴포넌트에 로그아웃 링크를 추가해보자.

app/NavBar.tsx

"use client";

import React from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";

const NavBar = () => {
  const { status, data: session } = useSession();

  // 로딩 중인 경우
  if (status === "loading") {
    return null;
  }

  return (
    <div className="flex">
      <Link className="mr-5" href="/">
        Next.js
      </Link>
      <Link href="/users" className="mr-5">
        Users
      </Link>
      {/* 인증 된 경우 사용자 이름과 로그아웃 링크 출력 */}
      {status === "authenticated" && (
        <div>
          <span className="mr-2">Hello {session.user!.name}</span>
          <Link href="/api/auth/signout">Sign Out</Link>
        </div>
      )}
      {/* 인증되지 않은 경우 로그인 버튼 출력 */}
      {status === "unauthenticated" && (
        <Link href="/api/auth/signin">Google Login</Link>
      )}
    </div>
  );
};

export default NavBar;
  • 로그아웃 링크 동작 확인
    • 다시 로그인 버튼으로 바뀌는 것을 볼 수 있다.

  • 쿠키를 확인해보면 보안 세션도 제거되었다.

조건부 라우트 구현하기(Protected Routes)

미들웨어를 사용하여 로그인한 사용자만 접근할 수 있는 컴포넌트를 생성해보자.

Middleware

  • 클라이언트와 서버 사이에서 동작하는 소프트웨어 또는 코드의 집합
  • 클라이언트로부터 오는 요청과 서버로부터의 응답 사이에서 중간 처리 역할을 함
    • 이를 통해 데이터 변환, 메시지 관리, 인증, 로깅 등 다양한 기능을 수행할 수 있다.
    • 예를 들어, 웹 애플리케이션에서 미들웨어는 사용자 인증, 요청 로깅, 데이터 형식 변환 등의 작업을 처리할 수 있다.

컴포넌트를 렌더링 하기 전 사용자 세션을 확인하고, 사용자 세션에 인증된 토큰이 없는 경우 홈페이지로 리다이렉트 하고 인증 토큰이 있는 경우 특정 컴포넌트에 접근할 수 있도록 구현하면 된다.

  • Next.js 에서는 middleware 라는 지정된 파일명을 앱 폴더에 생성하면, 서버에 요청이 들어왔을 때 해당 middleware 파일을 먼저 실행하고 다음 단계로 넘어간다.

리다이렉트할 페이지를 만들고, 미들웨어 파일을 작성해보자.

  • 새로운 페이지 생성

app/new-page/page.tsx

import React from "react";

const NewPage = () => {
  return <div>NewPage</div>;
};

export default NewPage;
  • 미들웨어 파일 작성
    • 미들웨어 파일은 프로젝트 루트 위치에 생성한다.
    • middleware 함수를 정의하고 NextRequest 인자 전달
    • NextResponse 사용하여 /new-page 로 리다이렉트 하도록 작성

middleware.ts

import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/new-page", request.url));
}
  • 정상적으로 리다이렉트 되지만, 모든 요청에 리다이렉트가 적용되면 성능상 악영향을 미칠 수 있다.

  • 따라서, 특정 URL의 요청이 들어온 경우에만 미들웨어가 실행되도록 코드를 구성해야 한다.
    • 미들웨어 파일 내부에 config 라는 변수를 정의하고 내부에 matcher 프로퍼티를 정의하면, Next.js 엔진은 이 프로퍼티에 정의한 URL 패턴에 해당하는 경우에만 미들웨어 함수를 실행한다.
    • * 패턴은 0개 또는 그 이상 일치하는 경우를 의미한다.
    • 이렇게 미들웨어가 적용된 URL이 렌더링하는 컴포넌트를 Protected 컴포넌트 또는 라우터라고 부른다.

middleware.ts

import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/new-page", request.url));
}

export const config = {
  matcher: ["/users/:id*"],
};
  • http://localhost:3000/users 페이지로 접속하면 http://localhost:3000/new-page로 리다이렉션 되는 것을 볼 수 있다.

next-auth middleware

  • next-auth 모듈의 middleware를 사용하면 인증 세션이 없는 경우 기본적으로 로그인 페이지로 리다이렉트 해준다.

middleware.ts

import { NextRequest, NextResponse } from "next/server";
import middleware from "next-auth/middleware";

export default middleware;

export const config = {
  matcher: ["/users/:id*"],
};
  • default 키워드를 사용하면 코드를 단순화할 수 있다.

middleware.ts

export { default } from "next-auth/middleware";

export const config = {
  matcher: ["/users/:id*"],
};
  • 동작 확인

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

1개의 댓글

comment-user-thumbnail
2024년 10월 3일

우와... 이건 교과서인가요? 친절한 설명 감사합니다.

답글 달기