NextAuth.js 사용하기
투데이 링크에서는 사용자 인증을 위해 NextAuth
를 이용하고 있습니다.
NextAuth
는 Next.js
로 구축된 웹 어플리케이션에서 사용자 인증 과정을 아주 쉽게 구현할 수 있도록 도와주는 npm 패키지입니다.
이전에 저는 jsonwebtoken
이라는 패키지를 통해 간단한 인증을 구현해 본 경험이 있었기 때문에
투데이 링크 인증에도 jsonwebtoken
을 사용할 계획이 있었습니다.
그런데, Next.js
를 학습하며 NextAuth
라는 기술을 알게 되었고, NextAuth
의 여러 장점들을 알게 되었습니다.
NextAuth
가 처리해 줍니다.useSession
, getServerSession
등)NextAuth
가 제공하는 signIn
, signOut
등의 함수를 통해 간단하게 로그인, 로그아웃을 구현할 수 있습니다.Firebase FireStore
, MongoDB
, MySQL
, Postgres
등 데이터베이스의 종류에 상관없이 사용자 인증을 구현할 수 있습니다.저는 이러한 장점들이 투데이 링크 프로젝트에 아주 적합하다고 생각했기 때문에 NextAuth
를 이용하여 투데이 링크의 인증을 구현했습니다.
회원가입 구현하기
NextAuth
는 사용자 인증을 아주 쉽게 구현할 수 있도록 도와주지만, 사용자 유저의 정보를 관리하는 것은 개발자가 직접 수행해야 합니다. (회원가입 및 유저 정보를 데이터베이스에 저장하는 것)
투데이 링크는 유저의 정보를 관리하기 위해 Firebase Firestore
를 이용하고 있습니다.
유저 데이터의 구조는 다음과 같습니다.
id
: 유저별 고유 IDuserId
: 투데이 링크 서비스 이용 시 사용하는 유저 계정password
: 계정 암호 email
: 본인 인증을 위한 방법으로 이메일을 사용하고 이메일을 통해 아이디 찾기, 비밀번호 찾기가 가능합니다.회원가입 시 사용자가 보는 클라이언트 측 UI는 다음과 같습니다.
회원가입시 요청하는 서버 측 api
의 구조는 다음과 같습니다.
pages
├── api
└── auth
└── signUp
├── checkId.ts
├── index.ts
└── verifyEmail.ts
이처럼, 투데이 링크 회원가입 과정에서는 별도로 중복된 아이디를 체크하기 위한 단계와 이메일 인증을 위한 단계가 존재합니다.
먼저, 중복된 아이디를 체크하기 위해 클라이언트 측에서 입력된 정보로 서버 측 API pages/api/auth/signUp/checkId
를 호출합니다.
회원가입 API 코드 보기
그리고 서버 측에서는 다음과 같은 순서로 클라이언트의 요청을 처리합니다.
res.status(200).json({ message: "success", isExistsId });
결과적으로 아이디 중복 체크를 수행하는 클라이언트 측 UI는 다음과 같이 보여집니다.
또한 회원가입은 본인 인증을 위한 이메일 인증 과정도 거쳐야 합니다.
이메일 인증은 이메일을 전송하는 과정이 필요한데, 이메일 전송은 이메일 서버가 필요합니다.
하지만, 이메일 서버를 직접 구축하는 것은 많이 부담스럽고 어려운 작업이었기 때문에 직접 메일 서버를
구축하지 않고 nodemailer
패키지를 사용했으며, 메일 전송을 위해 네이버의 POP3/SMTP
를 이용했습니다.
네이버의 POP3/SMTP
는 네이버 웹 메일에 직접 접속하지 않고서도 외부 메일 프로그램을 이용해서 네이버 메일을 보내고 받을 수 있는 기능입니다.
POP3/SMTP 참고
이메일 인증을 위한 서버 측 API pages/api/auth/signUp/verifyEmail
을 호출하게 되면, 서버 측은 다음과 같은 과정을 거칩니다.
nodemailer
패키지를 이용해 인증 코드가 담긴 메일을 전송합니다.결과적으로 이메일 인증을 수행하는 클라이언트 측 UI는 다음과 같이 보여집니다.
로그인 구현하기
로그인 과정에는 사용자를 인증하고 권한을 부여하는 작업이 필요합니다.
NextAuth
를 이용한다면 사용자를 인증하고 권한(토큰)을 가지는지의 여부를 쉽게 확인할 수 있습니다.
Next.js
로 구축된 프로젝트에서 NextAuth
를 이용하기 위해선 별도의 동적 API 라우트를 추가해야 합니다.
API 경로는 다음과 같습니다 pages/api/auth/[...nextauth].ts
동적 라우트인 이유는 NextAuth
패키지는 내부적으로 /api/auth/~~
에 해당하는 여러 라우트를
사용하고 특정 경로에 따라 적절하게 처리해 주기 때문입니다. 참고 문서
예를 들어, NextAuth
패키지를 사용해서 로그인을 구현할 경우 클라이언트 측에서는 signIn()
메소드를 호출해야 하고,
signIn()
메소드를 호출하게 되면, NextAuth
내부에서는/api/auth/signin/:provider
경로로 POST 요청을 하게 됩니다.
따라서 pages/api/auth/[...nextauth].ts
API는 사용자의 로그인 요청 시에 호출되며,
이 안에서 사용자 입력값이 유효한지, 비밀번호가 맞는지, 데이터베이스에 유저 정보가 존재하는지에 대한 코드를 작성할 수 있습니다.
우선, pages/api/auth/[...nextauth].ts
경로에서 NextAuth
를 적절하게 사용하기 위해선 옵션을 설정해 주어야 합니다.
먼저, Provider
옵션을 설정해야 하는데, Provider
옵션은 인증 방식을 결정하는 옵션입니다.
인증 방식에는 대표적으로 OAuth
, Email
, Credentials
가 있습니다.
투데이 링크에서는 자체적으로 아이디와 패스워드를 통해 사용자를 인증하므로 CredentialsProvider
옵션을 사용했습니다. 참고 문서
또한 CredentialsProvider
옵션에서 중요한 것은 authorize
메소드 입니다.
authorize
메소드는 로그인 요청이 있을 때 Next.js
가 호출하는 메소드입니다.
이 메소드의 인자로 사용자가 입력한 데이터(아이디, 비밀번호)와 요청 객체 req
가 전달되기 때문에
이 메소드 안에서 유효성 검사를 수행하거나 데이터베이스에 접근하는 등 자체적인 인증 로직을 작성할 수 있습니다.
결과적으로 실패할 경우, null
또는 false
를 반환하고, 성공하게 되면 객체를 반환하는데, 이때 반환되는 객체는JWT
으로 부호화됩니다.
또한 로그인 요청이 성공할 경우 Next.js
는 Json Web Token
을 쿠키에 보관합니다.
/api/auth/[...nextauth] 전체 코드 보기
이후 사용자가 인증이 되었는지(세션 객체 존재 여부)를 확인하고 싶다면 클라이언트 측에서는 useSession
훅 또는 getSession
을 사용할 수 있으며,
서버 측에서는 getServerSession
을 통해 확인할 수 있습니다. 참고 문서
그리고 이 세션 객체(토큰)를 기반으로 권한이 필요한 라우트 접근을 제한하거나 API 요청을 제한할 수 있습니다.
권한 검사를 통한 라우트 보호 및 API 요청 제한
마이페이지와 비밀번호 변경 페이지는 사용자가 로그인해야만 접근할 수 있는 페이지입니다.
즉, 사용자는 이 라우트(페이지)에 접근하기 위한 권한이 필요합니다.
저는 사용자가 해당 라우트(페이지)에 접근하는 시점에서 권한을 체크하기 위해getServerSideProps
함수와 NextAuth
의 getServerSession
함수를 이용했습니다.
(참고: 저는 조금 더 빠른 시점에서 권한을 체크하기 위해 getServerSideProps
를 이용했습니다. 같은 로직을 클라이언트 측에서 getSession
이나 useSession
을 사용해서 구성할 수도 있습니다.)
getServerSession
이라는 함수는 NextAuth
에서 제공하는 함수이며 세션 객체(토큰)를 반환합니다.getServerSession 참고 문서
저는 이 함수를 통해 세션의 존재 유무를 검사했고, 세션이 존재하면 라우트(페이지) 접근을 허용하고
세션이 존재하지 않으면, 사용자를 로그인 페이지로 리다이렉트했습니다.
최종 코드는 다음과 같습니다.
// pages/mypage/changePassword
// pages/mypage
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getServerSession(context.req, context.res, authOptions);
if (!session) {
return {
redirect: {
destination: "/auth/signIn",
permanent: false,
},
};
}
return {
props: {
session,
},
};
};
권한이 없을 경우, 클라이언트 측 라우트(페이지)의 접근을 막는 것뿐만 아니라 서버 측 API 요청도 제한할 필요가 있습니다.
왜냐하면, 서버 측 API에 요청하는 행동은 클라이언트 앱을 통해서만 할 수 있는 것이 아니라
Postman
등 다른 방법들을 통해서도 가능하기 때문입니다.
따라서 저는 클라이언트 측에서도 비밀번호 변경 페이지의 접근을 제한했을 뿐만 아니라
서버 측 비밀번호 변경 API에서도 들어온 요청에 대해 적절한 권한이 있는지를 검사하고 있습니다.
서버 측 권한 검사를 위한 코드는 다음과 같습니다.
전체 코드 보기