서버에서 발행한 토큰들은 http로 응답하면 되는 거 아닌가? 전달하는 과정에서 무슨 문제가 있지? 먼저 rfc6749에 나와있는 Authorization Code 인증 방식을 살펴본다.
{도메인}/oauth2/authorization/google, 302 Found
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=15683548038-co2cuhlfdt91mis4na6pvmlfi6gc2fbq.apps.googleusercontent.com&scope=profile email&state=zDNWdpxbKhsOfwqWt21-b2DLief7xSwStXQmGD04FjY%1D&redirect_uri={도메인}/login/oauth2/code/google, 302 found
1. response_type: code
2. client_id: 15683748038-co2cuhlfdt91mis4na6pvmlfi6gc9fbq.apps.googleusercontent.com
3. scope: profile email
4. state: zDNWdpxbKhsOfwqWt81-b1DLief7xSwStXQmGD04FjY=
5. redirect_uri: {도메인}/login/oauth2/code/google
{도메인}/login/oauth2/code/google?state=aDNWdpxbKhsOfwqWt81-b2DLief7xSwStXQmGD04FbY%3D&code=4%1F0AeaYSHBxM5NBkt5ZefI8Yvbv27Wnte5n9g-EbK81F3pBwX5mE_VOxnNAaOtfEB9w2HWj0A&scope=email+profile+https%31%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%1F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=none, 302 Found
1. state: zDNWdpxbKhsOfwqWt81-b2DLief7xSwStXQmGD04FjY=
2. code: 4/0AeaYSHBxM5NBkt5ZefI8Yvbv27Wnte5n9g-EbK81F3pBwX5mE_VOxnNAPOtfEB9w2HWj0A
3. scope: email profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid
4. authuser: 0
5. prompt: none
window.open()
으로 창을 열고, 백엔드는 javascript 코드를 통해 토큰을 전달하는 것이다. 백엔드 요청을 기다리면서 이벤트가 발생하면 토큰을 읽어오는 방식인데, 불필요하게 복잡해지고 JavaScript로 전달하는 과정에서 보안 취약점도 존재한다. Map<String, String> responseBody = new HashMap<>();
responseBody.put(ACCESS_TOKEN_KEY, accessToken);
responseBody.put(REFRESH_TOKEN_KEY, refreshToken);
responseBody.put(ACCESS_TOKEN_EXPIRAION, accessTokenExpired);
responseBody.put(REFRESH_TOKEN_EXPIRAION, refreshTokenExpired);
String jsonResponse = new ObjectMapper().writeValueAsString(responseBody);
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("<html><body>");
writer.write("<script>");
writer.write("const response = " + jsonResponse + ";");
writer.write("if (window.opener) {");
writer.write(" window.opener.postMessage(response, '*');");
writer.write(" window.close();");
writer.write("} else {");
writer.write(" console.error('No window.opener available');");
writer.write("}");
writer.write("</script>");
writer.write("</body></html>");
writer.flush();
const navigate = useNavigate();
useEffect(() => {
const handleMessage = (event) => {
if (event.origin !== "{서버 도메인}") {
console.error("invalid origin:", event.origin);
return;
}
console.log(event);
const {
AccessToken,
AccessTokenExpired,
RefreshToken,
RefreshTokenExpired,
} = event.data;
console.log(AccessToken);
console.log(accessTokenExpired);
console.log(RefreshToken);
console.log(refreshTokenExpired);
localStorage.setItem("access_token", AccessToken);
localStorage.setItem("refresh_token", RefreshToken);
console.log("hello");
navigate("/");
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [navigate]);
그런데, 일반 일반 로그인을 완료하면 Http 응답 메시지로 토큰을 발급한다. 위와 형식을 맞추기 위해 소셜 로그인을 완료했을 때 임시 Refresh Token을 쿠키로 발급해 주었다.
httponly
, samesite
, Secure
옵션으로 발행한 뒤 redirect url로 클라이언트가 해당 토큰으로 액세스 토큰과 리프레시 토큰을 재발행하는 API를 호출하게끔 handler를 설정한다. 클라이언트는 해당 핸들러로 특정 API에서 쿠키가 유효하다면 새로 토큰들을 http 응답 메시지로 발행받고, 서버는 발급한 쿠키를 삭제한다.