문제상황
펫시터 웹사이트를 구현하며 카카오톡 로그인을 넣기로 했다.
그러나 우리의 프로젝트는 res.session에 있는 user 값을 res.locals에 저장해, 유저의 로그인 유무를 판단했다.
login = async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
throw new customError(
400,
"Bad Request",
"아이디 / 비밀번호를 입력해주세요."
);
}
const responseFromService = await this.authService.login(email, password);
// 여기에서 세션 정보를 수정한다.
req.session.loggedIn = true;
req.session.loggedInUser = responseFromService.data;
return res.status(responseFromService.status).json(responseFromService);
} catch (error) {
next(error);
}
};
위 로직처럼 req.session에 loggedIn(로그인 정보) , loggedInUser(로그인 된 유저 정보)를 저장한다.
//locals에 값을 저장하는 미들웨어
export default (req, res, next) => {
res.locals.loggedIn = Boolean(req.session.loggedIn);
res.locals.loggedInUser = req.session.loggedInUser || null;
next();
};
그 후 res.locals에 값을 저장한 뒤
import response from "../lib/response.js";
export default (req, res, next) => {
if (req.session.loggedIn) {
return next();
} else {
return res
.status(401)
.json(response({ status: 401, message: "로그인 해주세요" }));
}
};
위 미들웨어를 통해 로그인을 유지했다.
passport를 사용하면 카카오 로그인이 쉽게 이뤄지지만, 우리가 사용한 로직엔 적합하지 않았다. req,res값을 얻어올 수 없었기 때문이다.
그래서 Kakao Developers 문서를 보면서 oAuth 로그인을 구현했고, 이를 정리해보려 한다.
Kakao Developers 문서를 살펴보자
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
카카오 로그인 과정은 다음과 같다.
상당히 길지만, 하나씩 차근차근 살펴보자.
Step 1. 인가 코드 받기
인가코드를 받기 위해선
위 주소로 get 요청을 보내야 한다.
그럼 먼저 라우터를 만들고, 그 안에 구현을 해보자!
router.get("/kakao", authController.kakao);
// authController.kakao()
export class AuthController {
kakao = async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/authorize";
};
}
get 요청을 보낼시 필수로 적어줘야할 쿼리파라미터이다.
client_id, redirect_uri 를 얻기 위해선
내 애플리케이션 -> 애플리케이션 추가하기 을 클릭해 애플리케이션을 만들어주면 된다.
Redirect URI를 얻기 위해선 내 어플리케이션 > 제품 설정 > 카카오 로그인으로 들어가야 한다.
위와 같이 Redirect URI를 설정한다.
요청을 위해 필요한 REST API 키, Redirect URI가 있으므로, 이를 통해 요청을 보내면 된다.
아래의 주소로 GET 요청을 보내면 되므로,
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}
로직을 작성한다.
kakao = async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/authorize";
const config = {
client_id: process.env.KAKAO_CLIENT_ID,
redirect_uri: process.env.KAKAO_REDIRECT_URI,
response_type: "code"
};
const configURL = new URLSearchParams(config).toString();
const targetURL = `${baseURL}?${configURL}`;
return res.redirect(targetURL);
};
}
위 코드에서
config 객체를 생성한 뒤 ,
new URLSearchParams(config).toString()
을 통해 configURL을 만들어준다.
( response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI} )
위 부분에 해당한다.
URLSearchParams가 뭔지 모르겠다면 아래 문서를 확인해보자.
URLSearchParams - Web APIs | MDN
The URLSearchParams interface defines utility methods to work with the query string of a URL.
developer.mozilla.org
현재 Step1(인가코드받기)의 5번까지 성공한 상태이다.
문서를 살펴보면
방금 우리의 get 요청은 redirect_uri의 get 요청으로 전달된다고 나와있다.
우리가 정한 redirect_uri는
위와 같으므로,
위 주소로 get 요청을 받을 수 있는 라우터와 컨트롤러를 설정하면 된다.
router.get("/kakao", authController.kakao);
router.get("/kakao/callback", async (req, res) => {
...
위 상태까지 하면, step 1의 6번까지 종료된 상태이다.
문서를 더 살펴보자
현재 우린 인가 코드 받기까지 된 상태이다.
로그인을 위한 토큰을 발급받기 위해선, 필수 파라미터를 포함해 POST 요청을 보내야한다고 나와있다.
Step 2. 토큰 받기
토큰을 받기 위해선, 위에 주어진 주소로 POST 요청을 보내야 한다.
여기서도 역시 쿼리와 함께 요청을 보내야 한다.
요청을 보낼 때 필요한 헤더와 쿼리 스트링값이다.
인가 코드 받기를 할 때 해봤으므로, 쉽게 넘어가보자.
우리가 위 파라미터들 중 사용한적 없는 값이 있다.
code 인데, 이는 인가코드를 요청했을 때, 성공 시 응답으로 제공된다.
Step1을 종료하고 우리에게 주어진 URL은
http://localhost:3000/kakao/callback?code={AUTHORIZE_CODE}
위와 같다.
code는 req.query.code에 있는 값을 의미한다.
router.get("/kakao/callback", async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/token";
const config = {
grant_type: "authorization_code",
client_id: process.env.KAKAO_CLIENT_ID,
redirect_uri: process.env.KAKAO_REDIRECT_URI,
code: req.query.code
};
const configURL = new URLSearchParams(config).toString();
const targetURL = `${baseURL}?${configURL}`;
const tokenRequest = await (
await fetch(targetURL, {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
})
).json();
...
위처럼 targetURL을 만든 뒤 , 필요 정보들과 함께 POST 요청을 보낸 뒤 응답을 받는다.
tokenRequest의 값에 어떤 값이 들어가는지 확인해보기 위해 console을 찍어보았다.
access token이 있는 것을 볼 수 있다.
로그인을 위해선 "access_token"이 필요하다.
이렇게 되면 로그인이 되었다고 볼 수 있지만, 우리가 원하는 추가 값들(email, nickname)을 얻기 위해선 추가적인 로직을 작성해줘야 한다.(추가 값들은 카카오 로그인 사이트에서 선택할 수 있다.)
사용자 정보를 가져오기 위해
위에 적혀진 URL로 GET/POST 요청을 보내야 한다.
필수로 지정해줘야 하는 헤더에 존재하므로 필수 값을 지정한 뒤 요청을 보낸다.
router.get("/kakao/callback", async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/token";
const config = {
grant_type: "authorization_code",
client_id: process.env.KAKAO_CLIENT_ID,
redirect_uri: process.env.KAKAO_REDIRECT_URI,
code: req.query.code
};
const configURL = new URLSearchParams(config).toString();
const finalURL = `${baseURL}?${configURL}`;
const tokenRequest = await (
await fetch(finalURL, {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
})
).json();
if ("access_token" in tokenRequest) {
const { access_token } = tokenRequest;
const userInfoObj = await (
await fetch("https://kapi.kakao.com/v2/user/me", {
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
})
).json();
...
위와 같이 로직을 작성하면 된다.
userInfoObj에 어떤 값이 있는지 확인하기 위해 console을 찍어보자.
이렇게 우리에게 필요한 email정보와, 이름 정보가 존재한다.
이제부터 우리가 구현해야 하는 것은 로그인 로직이다.
수도코드를 작성해보자.
1. email valid한가? && email이 verifited 됐는가?
-> 안된 경우 로그인 X
2. 우리 DB에 이미 해당 email로 가입된 유저가 있는가?
2.1 있는 경우, 해당 아이디로 로그인
2.2 없는 경우, DB에 해당 email 값을 갖는 유저를 생성한 뒤 로그인
const userData = userInfoObj.kakao_account;
const email =
userData.is_email_valid === true && userData.is_email_verified === true
? userData.email
: undefined;
if (!email) {
return res.redirect("/login");
}
let exsistingUser = await prisma.users.findFirst({ where: { email } });
if (exsistingUser) {
req.session.loggedIn = true;
req.session.loggedInUser = exsistingUser;
return res.redirect("/");
} else {
const user = await prisma.users.create({
data: {
email,
name: userData.profile.nickname,
password: "",
social: true
}
});
req.session.loggedIn = true;
req.session.loggedInUser = user;
return res.redirect("/");
}
우리가 작성한 수도코드에 따라 실제 코드를 작성했다.
(orm으로 prisma를 사용했다. 다른 orm을 사용한다면 해당 부분을 자신이 사용하고 있는 orm에 맞게 수정하면 된다.)
이로써 passport 없이 카카오톡 로그인하기를 구현해보았다.
전체 코드
router.get("/kakao", async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/authorize";
const config = {
client_id: process.env.KAKAO_CLIENT_ID,
redirect_uri: process.env.KAKAO_REDIRECT_URI,
response_type: "code"
};
const configURL = new URLSearchParams(config).toString();
const finalURL = `${baseURL}?${configURL}`;
return res.redirect(finalURL);
});
router.get("/kakao/callback", async (req, res) => {
const baseURL = "https://kauth.kakao.com/oauth/token";
const config = {
grant_type: "authorization_code",
client_id: process.env.KAKAO_CLIENT_ID,
redirect_uri: process.env.KAKAO_REDIRECT_URI,
code: req.query.code
};
const configURL = new URLSearchParams(config).toString();
const finalURL = `${baseURL}?${configURL}`;
const tokenRequest = await (
await fetch(finalURL, {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
})
).json();
if ("access_token" in tokenRequest) {
const { access_token } = tokenRequest;
const userInfoObj = await (
await fetch("https://kapi.kakao.com/v2/user/me", {
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
})
).json();
const userData = userInfoObj.kakao_account;
const email =
userData.is_email_valid === true && userData.is_email_verified === true
? userData.email
: undefined;
if (!email) {
return res.redirect("/login");
}
let exsistingUser = await prisma.users.findFirst({ where: { email } });
if (exsistingUser) {
req.session.loggedIn = true;
req.session.loggedInUser = exsistingUser;
return res.redirect("/");
} else {
const user = await prisma.users.create({
data: {
email,
name: userData.profile.nickname,
password: "",
social: true
}
});
req.session.loggedIn = true;
req.session.loggedInUser = user;
return res.redirect("/");
}
} else {
return res.redirect("/login");
}
});