회사 서비스에서 소셜로그인을 연동하기 위해 next-auth 라이브러리를 사용하기로 결정했다.
필요한 소셜 로그인은 네이버, 구글, 카카오, 애플 이였고
next-auth를 사용하면 쉽고 빠르게 소셜 로그인을 연동할 수 있어서 사용하게 되었다. (애플제외 😂)
말 그대로 네이버, 구글, 카카오는 참고자료도 많고 까다롭지가 않아서 쉽게 연동이 가능하였다.
그런데 애플이 문제였다.
애플로그인 문제상황
애플 소셜 로그인을 연동하는데 있어 다음과 같은 문제들이 존재하였다
이런 문제상황들을 나는 겪게되었고
내가 해결한 부분들을 공유하면서 조금이나마 다른사람들이 애플 로그인을 연동할 때 삽질을 덜 할수 있었으면 한다.
그럼 처음부터 끝까지 연동방법을 적어보도록 하겠다.
애플 개발자 사이트 => Account 에서 Certificates, IDs & Profiles
메뉴 선택
Identifiers
메뉴에서 플러스 버튼을 선택
App IDs
를 선택 후 Continue
버튼 클릭
타입을 App
을 선택하고, Continue
버튼 클릭
앱 이름(Description)
과 고유 ID(Bundle ID)
를 입력
BundleID는 도메인을 역순으로 해서 작성하는것을 애플에서 추천한다. ex) com.도메인명.앱이름
스크롤을 내려 Sign In with Apple
에 체크 후 Continue
버튼 클릭
입력 내용을 확인 후 Register
버튼 클릭
Identifiers
메뉴에서 플러스 버튼을 선택
Services IDs
를 선택 후 Continue
버튼을 누른다.
서비스 이름과, ID를 입력한다.
App ID(Bundle ID)와 중복 될 수 없다.
continue => register를 눌러 등록한다.
방금 등록한 서비스를 클릭한다.
Sign In with Apple
을 활성화(좌측 체크박스) 후 Configure
버튼을 눌러 설정 팝업을 띄운다.
앞에서 추가한 App ID와 연결해 준다.
도메인 명을 입력해 준다.
Redirect URL을 입력한다.
Next => Done => Continue => Save 버튼을 눌러 완료한다.
Keys 메뉴를 선택 후 플러스 버튼을 눌러 Key를 추가한다.
키 이름 입력 후 Sign In with Apple
을 체크하고 Configure
버튼을 눌러 설정 화면으로 이동한다.
설정 내용 확인 후 Register 버튼을 눌러 다운로드한다.
키를 다운로드 한다.
키 생성 시 발급받은 AuthKey_XXXXXXXXXX.p8 파일을 vscode로 열면
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgtE//U5HHjWkwoaas
ELnEnN3FEghMXwR/Z46DVrw6yaOgCgYIKoZIzj0DAQehRANCAAQvC0lpRjnDfe23
OBWGeIvdaOPQ83OLFjzKob3gBdK4lsV40haKQ76LBlUJv/QR/S6iUuRgvHZAPo4i
mClraeXI
-----END PRIVATE KEY-----
이런 형식으로 나타나 있는데 나는 BEGIN PRIVATE KEY와 END PRIVATE KEY 사이에 있는 문자열만 env파일에 따로 저장을 해두었다.
(개행처리된 부분도 적용이 되어야 하기에 \n을 사이에 넣어주었다.)
그리고 이 PRIVATE KEY는 애플토큰을 생성할때 사용이 된다.
다른 소셜로그인의 경우 clientSecret 부분에 바로 입력을 하면 되는데
애플 로그인은 따로 token으로 변형을 해준뒤 넣어주어야만 되었다.
import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";
export default async function auth(req, res) {
const getAppleToken = async () => {
const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;
const appleToken = await new SignJWT({})
.setAudience("https://appleid.apple.com")
.setIssuer(process.env.APPLE_TEAM_ID)
.setIssuedAt(new Date().getTime() / 1000)
.setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
.setSubject(process.env.APPLE_ID)
.setProtectedHeader({
alg: "ES256",
kid: process.env.APPLE_KEY_ID,
})
.sign(createPrivateKey(key));
return appleToken;
};
return await NextAuth(req, res, {
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_CLIENT_SECRET,
}),
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: await getAppleToken(),
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async jwt(data) {
if (data.account) {
data.token.accessToken = data.account.access_token;
data.token.provider = data.account.provider;
}
return data.token;
},
async session({ session, token }) {
if (session) {
session.accessToken = token.accessToken;
session.provider = token.provider;
session.user.id = token.sub;
}
return session;
},
},
});
}
PRIVATE_KEY를 이용하여 애플 토큰을 생성하는 함수를 만들어준뒤 AppleProvider에 clientSecret 부분에 넣어주었다.
나같은 경우에는 로그인을 한 이후 로직을 처리하기 위해 /oauth/apple 경로로 callback이 되기를 원해서 아래와 같이 설정을 해두었다.
signIn(provider, {
callbackUrl: `/oauth/${provider}`,
});
next-auth에서 제공해 주는 signIn 함수를 통해 로그인 성공 후 돌아오는 경로를 설정해 줄수가 있었는데
apple에서는 [...next-auth].js에 추가 설정을 해주어야 동작이 정상적으로 되었다.
import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";
export default async function auth(req, res) {
const getAppleToken = async () => {
const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;
const appleToken = await new SignJWT({})
.setAudience("https://appleid.apple.com")
.setIssuer(process.env.APPLE_TEAM_ID)
.setIssuedAt(new Date().getTime() / 1000)
.setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
.setSubject(process.env.APPLE_ID)
.setProtectedHeader({
alg: "ES256",
kid: process.env.APPLE_KEY_ID,
})
.sign(createPrivateKey(key));
return appleToken;
};
return await NextAuth(req, res, {
/**
* 애플 로그인 시 쿠키옵션 적용해 주어야 callbackUrl이 정상 작동
*/
cookies: {
callbackUrl: {
name: `__Secure-next-auth.callback-url`,
options: {
httpOnly: false,
sameSite: "none",
path: "/",
secure: true,
},
},
},
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_CLIENT_SECRET,
}),
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: await getAppleToken(),
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async jwt(data) {
if (data.account) {
data.token.accessToken = data.account.access_token;
data.token.provider = data.account.provider;
}
return data.token;
},
async session({ session, token }) {
if (session) {
session.accessToken = token.accessToken;
session.provider = token.provider;
session.user.id = token.sub;
}
return session;
},
},
});
}
로그인한 유저의 name값을 가져오고 싶었다.
하지만 next-auth에서 제공해주는 session값에는 아무리 찾아도 name값을 찾을수가 없었다.
여러 삽질끝에 name값을 최초로그인시에 req.body.user에서 찾을수가 있었다.
(애플은 사용자 name값을 최초로그인시 한번만 반환하도록 설정을 해둔거 같다.)
그래서 나는 req.body.user에 값을 session에 넣어서 사용할수 있도록 코드를 수정하였다.
import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";
export default async function auth(req, res) {
// 애플 최초 가입일 경우 req.body에 user.name이 담겨옴
let appleFirstInfo;
if (
req?.url?.includes("callback/apple") &&
req?.method === "POST" &&
req.body.user
) {
appleFirstInfo = await JSON.parse(req.body.user);
}
const getAppleToken = async () => {
const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;
const appleToken = await new SignJWT({})
.setAudience("https://appleid.apple.com")
.setIssuer(process.env.APPLE_TEAM_ID)
.setIssuedAt(new Date().getTime() / 1000)
.setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
.setSubject(process.env.APPLE_ID)
.setProtectedHeader({
alg: "ES256",
kid: process.env.APPLE_KEY_ID,
})
.sign(createPrivateKey(key));
return appleToken;
};
return await NextAuth(req, res, {
/**
* 애플 로그인 시 쿠키옵션 적용해 주어야 callbackUrl이 정상 작동
*/
cookies: {
callbackUrl: {
name: `__Secure-next-auth.callback-url`,
options: {
httpOnly: false,
sameSite: "none",
path: "/",
secure: true,
},
},
},
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_CLIENT_SECRET,
}),
AppleProvider({
clientId: process.env.APPLE_ID,
clientSecret: await getAppleToken(),
profile(profile) {
if (appleFirstInfo) {
profile.name = `${appleFirstInfo.name.firstName} ${appleFirstInfo.name.lastName}`;
}
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
};
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async jwt(data) {
if (data.account) {
data.token.accessToken = data.account.access_token;
data.token.provider = data.account.provider;
}
return data.token;
},
async session({ session, token }) {
if (session) {
session.accessToken = token.accessToken;
session.provider = token.provider;
session.user.id = token.sub;
}
return session;
},
},
});
}
next-auth를 사용하여 애플로그인을 진행하는 글이 많이 없어서 여러 자료를 찾아서 정리를 해보았다.
안녕하세요. 덕분에 많은 도움 되었습니다.
궁금한 것이 하나 있는데 쿠키 옵션에 callbackUrl을 적용했을 때, 애플 외에 다른 sns 로그인은 문제가 없었나요?