최근들어 NextAuth
의 사용성이 높아지고 Oauth
인증 관련 활용할 수 있는 기능들이 많아 학습해 보고자 한다.
학습한 내용을 바탕으로 ID & PW 로그인
, 카카오 로그인
, 네이버 로그인
을 적용할 예정이며
그 과정을 기록해 두려고 한다. 누군가에게 조금이나마 도움이 됐으면 좋겠다. 😄
next.js 로 개발된 페이지에서 로그인을 쉽게 구현할 수 있도록 여러 기능을 제공하는 라이브러리 이다.
특히, Oauth Provider
를 제공해 주어 Oauth 인증 방식의 로그인 서비스를 쉽게 구현 할 수 있어 핵심 로직에 집중할 수 있다.
로그인에 있어 JWT Access Token
을 안전하게 보관하는 것이 굉장히 중요하다.
일반적으로 localstorage 와 Cookie에 저장하게 되는데 각각의 장단점이 존재한다.
로컬스토리지에 저장하면 매우 간편하지만 XSS 공격에 취약하다.
로컬 스토리지는 자바스크립트로 접근이 가능하므로 사용자의 브라우저가 악의적인 코드에 노출되게 된다면 해커에게 토큰이 탈취 당할 위험이 있다. 😷
👿 XSS(Cross Site Scripting attack) 👿
해커가 사용자의 브라우저 환경에서 악의적인 자바스크립트 코드를 실행하는 공격이다.
먼저, 쿠키에 http-only
와 Secure
설정을 하면 자바스크립트로 쿠키에 접근할 수 없기 때문에 XXS 공격으로부터 안전하다.
하지만, 4KB 용량 제한이 있기 때문에 큰 토큰의 경우 저장이 불가능하고 CSRF
공격에 취약할 수 있다.
그러나 next-auth는 쿠키에 토큰을 저장할 때 기본적으로 Same-site 속성을 Lax로 저장한다. 이 속성을 사용하면 브라우저가 cross-site
에 HTTP 요청을 보낼 때 쿠키에 해당정보를 담지 않겠다는 뜻이므로 토큰이 유출되지 않는다. 🧚🏻
😈 CSRF 공격 😈
CSRF 공격
은 사용자의 의도와 무관하게 특정 HTTP 요청을 하게 만드는 공격 방법이다.
토큰은 HTTP 요청마다RequestHeader
에 담겨 보내지기 때문에 갈취당할 위험이 있다.
만약 서버에서 쿠키에 담긴 토큰 정보를 이용해 사용자 권한을 검증한다면 문제가 발생한다.
http-only
설정을 하게되면 자바스크립트로 쿠키에 접근하지 못해 XXS 로부터 안전할 수 있으나 사용자도 접근이 불가능하다.
next-auth 에서는 jwt
, session 콜백 함수
를 이용해 토큰 정보 일부를 노출시킬 수 있고, useSession
과 같은 훅을 통해 이를 참조할 수 있다.
API 라우트는 서버사이드에서 동작하는 코드로, NextAuth가 Next.js의 동적 API 라우트
를 이용하기 때문에 설정해 주어야 한다.
반드시 /app/api/auth/[...nextauth]
경로에 route.ts
파일을 만들어 주어야 한다.
// /app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
const handler = NextAuth({
providers: [ ... ],
callbacks:{...},
pages:{...}
})
export { handler as GET, handler as POST }
NextAuth는 Naver
, Kakao
, ID & PW
등 로그인 방식에 따라 provider 를 제공한다.
개발자들은 이를 활용해 로그인 기능을 쉽게 구현할 수 있어 핵심 로직에 집중할 수 있다.
// /app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import NaverProvider from "next-auth/providers/naver"
const handler = NextAuth({
providers: [
// 네이버 Provider
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_SECRET,
}),
],
callbacks:{...},
pages:{...}
})
export { handler as GET, handler as POST }
// /app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import KakaoProvider from "next-auth/providers/kakao"
const handler = NextAuth({
providers: [
// 카카오 Provider
KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_SECRET,
})
],
callbacks:{...},
pages:{...}
})
export { handler as GET, handler as POST }
각 Provider의 ClientId
와 ClientSecret
은 개발자 센터에 앱을 등록하고
할당받은 ID와 Secret을 넣으면 된다.
카카오의 경우 RestAPI Key
를 넣고
로그인 > 보안 탭에서 Client Secret를 생성해 넣어주면 된다.
🚨 주의!!
ClientId
와ClientSecret
을 환경변수로 관리할 때NEXT_PUBLIC
를 prefix로 붙이면 외부에 노출되기 때문에 붙이면 안된다.
로그인을 하기 위해 개발자 센터에서 Redirect URL을 등록해야 한다.
개발 및 배포 환경에 따라 도메인 주소 뒤에 api/auth/callback/(naver OR kakao)
를 붙여주면 된다.
http://localhost:3000/api/auth/callback/(naver OR kakao)
http://www.stg-project.com/api/auth/callback/(naver OR kakao)
http://www.project.com/api/auth/callback/(naver OR kakao)
SNS 로그인을 위한 세팅은 이게 전부이다.
💡 next-auth 가 지원하는 소셜 로그인
네이버
,카카오
,애플
,구글
,페이스북
,인스타그램
,트위터
,깃랩
,줌
,틱톡
,링크드인
,디스코드
,코인베이스
등등
Credential Provider
는 개발자가 직접, 로그인에 필요한 정보들(credentials)을 구성하여 로그인을 할 수 있도록 만들어 주는 provider 이다.
// 로그인 화면에서 로그인 버튼
const onSubmit:FormEventHandler<HTMLFormElement> = async (e) => {
...
try{
const response = await signIn("credentials", {
username: id,
password,
redirect: false,
})
...
}catch (e) {
...
}
};
// /api/auth/[...nextauth]/route.ts
import CredentialsProvider from "next-auth/providers/credentials"
...
providers: [
CredentialsProvider({
name: 'Credentials'
},
async authorize(credentials, req) {
const res = await fetch(`${process.env.NEXTAUTH_URL}/api/signin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: credentials?.username,
password: credentials?.password,
}),
})
const user = await res.json()
console.log('$$$user: ', user)
if (user) {
// Any object returned will be saved in `user` property of the JWT
return user
} else {
// If you return null then an error will be displayed advising the user to check their details.
return null
// You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
}
},
})
],
callbacks:{
async jwt({ token, user }) {
// 리턴되는 값들은 token에 저장된다.
return { ...token, ...user};
},
async session({ session, token }) {
session.user = token as any;
return session;
},
},
...
위의 코드는 next-auth 로그인 구현
실습에서 사용하는 로그인 기능이다.
authroize()
는 credentials
값을 통해 사용자의 유효성을 체크해 로그인을 제어할 수 있다.
인증에 성공하면 User 객체를 반환하고 그렇지 않으면 null or false
값을 반환한다.
jwt
jwt
콜백에서의 token
은 Next.js 의 JWT 토큰이며 user
는 authroize()의 반환 값이다.
return 값은 token에 저장되므로 jwt() 에서는 token에 사용자 정보를 추가해주는 역할을 한다.
session
session
콜백에서는 토큰 정보를 Session 에 넣어주는 역할을 한다.
사용자 로그인
로그인하기 버튼을 클릭하면 ID & PW
값을 인자로 넣어 next-auth의 signIn
함수를 호출한다.
인증확인
CredentialsProvider
에서 authorize()
가 credentials
를 인자로 받아 백엔드와 통신한다.
인증에 성공할 경우 authorize()
는 사용자 정보를
을 반환하고 callbakcs
가 실행된다.
JWT 생성
authorize()
의 반환값들이 jwt()
콜백에 전달된다.
이때, 반환된 토큰이 jwt
콜백 내에서 token 객체
에 전달 되고 이를 session
콜백에 전달한다.
세션 생성
session
콜백은 이 토큰
을 세션 객체에 할당하고 클라이언트에게 반환한다.
클라이언트에서 사용
클라이언트에서는 useSession
훅을 통해 세션에 접근할 수 있다.
즉, authorize()
에서 발급된 토큰은 jwt
콜백을 통해 Next.js 서버의 JWT 토큰
에 저장되고, 이 정보는 다시 session
콜백을 통해 세션 객체로 변환되어 클라이언트에 전달된다.
💡 User 객체 타입
// type.d.ts export interface DefaultUser { id: string; name?: string | null; email?: string | null; image?: string | null; } export interface User extends DefaultUser { }
id
속성은 필수값으로 넣어야 하며 속성을 추가할 수 있다.
💡 next-auth 에서 제공하는 로그인 폼
next-auth 는 기본적인 login 페이지를 지원해준다. (
localhost:3000/api/auth/signin
)
보통은 커스텀 로그인 페이지를 따로 만들어 사용해label
과placeholder
속성은 필요로하지 않는다.
소셜 로그인에서 사용되는 Client ID
와 Secret
값 외에 두가지가 더 있다.
NEXTAUTH_SECRET
은 토큰정보를 암호화 & 복호화 하기 위해 필요하며 문자열을 입력해주면 되고 ( ex. secretstring )
NEXTAUTH_URL
은 보안성의 이유로 로그인 기능이 포함된 도메인 주소를 작성해주면 된다. (ex. http://localhost:3000
)
참고
NextAuth 공식문서
환경변수 설정
NextAuth callbacks
@dosomething
@zooyaho
@cloud_oort