먼저 약관동의 후 계정 정보를 등록하면 애플리케이션 등록을 할 수 있다.
네이버 로그인 api를 사용하는 경우, 제공 정보 선택

환경추가 > PC웹 선택 > 서비스 URL과 Calback URL을 입력한다

애플리케이션이 '개발중'인 상태이기 때문에 테이스ID를 등록해야 한다
서비스에 적용하려면 '네이버 로그인 검수요청'을 해야한다

마찬가지로 이전의 코드에서 네이버 소셜 로그인 부분이 추가되었다.
카카오나 구글과는 달리 매개변수에 state가 필수로 포함된다.
RandomStringUtil라는 유틸리티를 만들어서 10자리수의 랜덤 문자열을 생성하여 state로 사용하였다.
state: 사이트 간 요청 위조(CSRF)공격을 방지하기 위해 애플리케이션에서 생성한 상태 토큰값. URL 인코딩을 적용한 값을 사용
public socialConnection = async (type: string) => {
try {
let authorizationUrl: string | undefined;
if (type === "kakao") {
authorizationUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_REST_API_KEY}&redirect_uri=${KAKAO_REDIRECT_URI}&response_type=code`;
} else if (type === "google") {
authorizationUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=code&scope=email profile`;
} else if (type === "naver") {
const STATE = RandomStringUtil.generateRandomString(10);
console.log("state= ", STATE);
authorizationUrl = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${NAVER_CLIENT_ID}&redirect_uri=${NAVER_REDIRECT_URI}&state=${STATE}`;
}
return authorizationUrl;
} catch (err) {
console.log(err);
throw new InternalServerError(`${type}-login : 연결 실패`);
}
};
headers에 client_id와 client_secret을 같이 전송해준다.
public getAccessToken = async (code: string, type: string, state: string) => {
try {
let api_url = "";
let data: any;
let header = {
"Content-Type": "application/x-www-form-urlencoded",
};
if (type === "kakao") {
api_url = "https://kauth.kakao.com/oauth/token";
data = {
grant_type: "authorization_code",
client_id: KAKAO_REST_API_KEY,
redirect_uri: KAKAO_REDIRECT_URI,
code: code,
};
} else if (type === "google") {
api_url = "https://oauth2.googleapis.com/token";
data = {
grant_type: "authorization_code",
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_REDIRECT_URI,
code: code,
};
} else if (type === "naver") {
api_url = "https://nid.naver.com/oauth2.0/token";
data = {
grant_type: "authorization_code",
client_id: NAVER_CLIENT_ID,
client_secret: NAVER_CLIENT_SECRET,
redirect_uri: NAVER_REDIRECT_URI,
code: code,
state: state,
};
interface CustomHeaders {
"Content-Type": string;
"X-Naver-Client-Id"?: string;
"X-Naver-Client-Secret"?: string;
}
header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Naver-Client-Id": NAVER_CLIENT_ID,
"X-Naver-Client-Secret": NAVER_CLIENT_SECRET,
} as CustomHeaders;
}
// 2-1. 엑세스 토큰 발급
const response = await axios.post(api_url, data, { headers: header });
const ACCESS_TOKEN = response.data.access_token;
// 2-2. 카카오 로그인인 경우, 이메일 정보가 없을 시 회원가입 불가
if (
type === "kakao" &&
(!response.data.scope || !response.data.scope.includes("account_email"))
) {
return;
}
return ACCESS_TOKEN;
} catch (err) {
console.log(err);
throw new InternalServerError(`${type}-login : 토큰 발급 실패`);
}
};
기존의 코드에서 newUserData 함수를 만들어서 중복을 최소화 시켰다.
🤔 issue : 테스트 하는 중, 네이버 계정의 이메일과 카카오 계정의 이메일이 같아 기존에 가입된 사용자임에도 불구하고 회원 조회가 되지 않는 문제가 발생하였다.
✅ solve : 가입된 사용자일 경우, 소셜 타입을 검사하는 로직을 추가하고 소셜 타입을 프론트로 반환하여 알림창을 통해 해당 소셜 타입으로 로그인하도록 유도하였다.
public getUserInfo = async (token: string, type: string) => {
try {
// 3-1. 토큰을 이용하여 소셜 회원 정보 취득 후
let api_url = "";
let userData: any;
if (type === "kakao") api_url = "https://kapi.kakao.com/v2/user/me";
else if (type === "google")
api_url = "https://www.googleapis.com/userinfo/v2/me";
else if (type === "naver")
api_url = "https://openapi.naver.com/v1/nid/me";
const res = await axios.get(api_url, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const newUserData = (id: string, nickname: string, email: string, type: string) => {
const userData = {
social_id: id,
nickname: nickname,
email: email,
type: type,
};
return userData;
};
if (type === "kakao" && res.data) {
const data = res.data.kakao_account;
userData = newUserData(res.data.id, data.profile.nickname, data.email, type);
} else if (type === "google" && res.data) {
const data = res.data;
userData = newUserData(data.id, data.name, data.email, type);
} else if (type === "naver" && res.data) {
const data = res.data.response;
userData = newUserData(data.id, data.nickname, data.email, type);
}
// 3-2. 가입여부 확인
const existingUser = await User.findOne({
where: { email: userData.email },
});
// 3-3. 가입되지 않은 사용자일 경우, 회원가입
if (existingUser === null) await this.addUser(userData);
// 3-4. 가입된 사용자일 경우, 소셜 타입 검사
else if (existingUser.type !== type)
throw new BadRequest(
`이미 가입된 회원 입니다.\n${existingUser.type}로 다시 시도해주십시오.😅`
);
// 3-5. 회원 조회하여 id, nickname 취즉
const user = await User.findOne({
where: {
email: userData.email,
social_id: userData.social_id,
},
});
const userInfo = {
id: user?.id,
nickname: user?.nickname,
type: type,
};
if (!userInfo) throw new BadRequest(`${type}-login : 사용자 정보 취득 실패`);
return userInfo;
} catch (err) {
console.log(err);
throw err;
}
};
서비스 전용 토큰은 access_token과 refresh_token을 발급하도록 수정했다.
public generateToken = async (userInfo: any) => {
try {
const generateTokenUtil = new GenerateTokenUtil(SECRET_KEY);
const newToken = (expiresIn: string) => {
const token = generateTokenUtil.generateToken(
userInfo.id,
userInfo.nickname,
expiresIn
);
return token;
};
const access_token = newToken("2h");
const refresh_token = newToken("24h");
return { access_token: access_token, refresh_token: refresh_token };
} catch (err) {
console.log(err);
throw new InternalServerError(
`${
userInfo.type !== null
? `${userInfo.type}-login : 서비스 전용 토큰 발급 실패`
: "서비스 전용 토큰 발급 실패"
}`
);
}
};