2023. 12. 5. 00:44ㆍ_Web/React
OAuth 2.0에서 프론트에서 리다이렉트 페이지를 어떻게 구현해야 하는지, 해당 페이지가 왜 필요한 건지 알아봤다. 결론적으로, 리다이렉션 페이지에서 로그인 성공/실패를 처리해야 한다.
사건의 발단
매번 간편하게 카카오톡 로그인 버튼을 사용하기 위해서 컴포넌트화하였다.
SVG라 이미지를 등록하지 않아도 코드를 복붙하면 바로 카카오 로그인 버튼이 생겨난다.
리다이렉션 페이지는 로그인 성공페이지로 작업하였다. 아래는 문제의 코드이다.
코드 보기
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { BASE_URL } from "../../utils/URL";
import { useDispatch } from "react-redux";
import { AuthActions } from "../../store/auth-slice";
import "./KakaoLoginBtn.css";
const REST_API_KEY = import.meta.env.VITE_APP_REST_API_KEY;
const KakaoLoginBtn = () => {
const REDIRECT_URI = "http://127.0.0.1:5173/auth/kakao/callback";
const KAKAO_AUTH_URI = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
const [code, setCode] = useState(null);
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
// 코드가 있을 때만 처리
if (code) {
console.log(code);
const handleKakaoLogin = async () => {
try {
const res = await axios.post(`${BASE_URL}/kakao/login`, { code });
const token = res.headers.authorization;
// Redux store에 로그인 상태 업데이트
dispatch(AuthActions.setSignIn({ status: "success", token }));
// 로그인 성공 시 페이지 이동
navigate("/auth/kakao/callback");
} catch (e) {
console.error(e);
// Redux store에 로그인 실패 상태 업데이트
dispatch(AuthActions.setSignIn({ status: "success", token: "test" }));
// dispatch(AuthActions.setSignIn({ status: "failed", token: null }));
// 로그인 실패 시 페이지 이동
navigate("/auth/kakao/callback");
}
};
// 코드가 있을 때만 비동기 함수 호출
handleKakaoLogin();
}
}, [code, navigate, dispatch]);
useEffect(() => {
// 코드 받아오기
const codeParam = new URL(window.location.href).searchParams.get("code");
setCode(codeParam || "");
}, [window.location.href]);
return (
<div className="KakaoLoginBtn-wrapper">
<a href={KAKAO_AUTH_URI} className="btn-kakao">
<div className="Balloon">
<div className="Circle">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<ellipse cx="8" cy="6.5" rx="8" ry="6.5" fill="#191900" />
<path
d="M2.35025 14.516C2.22081 14.9721 2.73969 15.3354 3.12405 15.0578L9.11076 10.7332C9.38956 10.5318 9.38649 10.1156 9.10476 9.91829L5.13427 7.13812C4.85253 6.94085 4.46037 7.08033 4.36647 7.4112L2.35025 14.516Z"
fill="#191900"
/>
</svg>
</div>
</div>
<span className="text"> 카카오 계정으로 로그인</span>
</a>
</div>
);
};
export default KakaoLoginBtn;
CSS
.KakaoLoginBtn-wrapper{
display: flex;
justify-content: center;
}
.btn-kakao {
width: 300px;
height: 46px;
background: #FEE500;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
transition: background-color 0.3s;
}
.text {
font-family: 'Noto Sans', sans-serif;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 24px;
text-align: center;
color: #000000;
}
.btn-kakao:hover {
background-color: #fff7ac;
}
백엔드에게 JWT 토큰을 받아오기 위해 연결하는 중에 PM님과 코드를 확인했다. 코드가 요상하게 작동하는거 같아서 로그인 이후에 보이는 잠깐의 흰 창을 본 적 있냐고 들었다. 그래서 음?? 그런게 있었나 싶었는데 이 창이 리디렉션(Redirect) 창이였고 다시 OAuth 2.0 이 어떻게 작용하는지를 꼼꼼하게 읽어보았다.
https://hudi.blog/oauth-2.0/
현재 코드에서는 카카오 로그인 버튼 자체에서 Param 추출, POST 요청까지 진행하고 있다. 이는 중간에 코드 탈취라는 치명적인 보안 문제를 일으킬 수 있다.
OAuth 2.0 의 개념, 왜 필요한가?
예전에는 직접 회원가입 및 로그인을 하여 서비스에 접근하고 필요한 정보를 서버로 받아올 수 있었다. 이는 같은 로그인/비밀번호를 사용하는 유저의 개인정보 유출에 대한 큰 부담이 되었다. 타사 서비스를 이용하기 위하여 구글이나 카카오, 네이버와 같은 플랫폼 계정 정보를 제공해야 했고 개인정보 유출의 부담을 줄일 방법이 필요했다.
OAuth 는 접근 권한을 위임받을 수 있는 방식으로 다양한 플랫폼의 특정한 사용자 데이터에 접근하기 위해 제 3자 클라이언트가 권한을 위임받는 것이다.
정확한 용어로는 3가지의 용어가 있다.
Resource Owner : 리소스 소유자
구글, 네이버, 카카오에서 리소스를 소유하는 사용자
Authorization Server & Resource Server: Resource Owner를 인증하고, Client에게 액세스 토큰을 발급해주는 서버, 리소스를 가지고 있는 서버
Client : Resource Server의 자원을 이용하고자 하는 서비스, 개발하려는 서비스이다. 이름의 명명은 외부 서비스를 클라이언트로 기준으로 작성하여서 이름이 이러하다.
공식문서 상에서 Resource Owner과 Authorization 이를 구분하는 것은 개발자의 편의에 맡긴다.
https://datatracker.ietf.org/doc/html/rfc6749#section-1.2
아무튼 우리의 서비스를 등록하려면 Client를 Resource Server에 등록을 해야 한다. 이때 Redirect URL을 등록해야하는데 사용자가 OAuth 2.0 서비스에서 인증을 마치고 사용자를 리디렉션시킬 위치이다.
Redirect URI
OAuth 2.0 서비스는 인증이 성공한 사용자를 등록된 Redirect URI로만 리디렉션한다. 승인되지 않은 URI로 리디렉션하면 Authorization Code를 중간에 탈취당할 위험성이 있기 때문이다. 따라서 여러 Redirect URI를 등록할 수 있다.
기본적으로 보안을 위해 https만 허용되며 루프백(localhost)는 예외적으로 http가 허용된다.
Client ID 와 Client Secret을 얻을 수 있으며 발급된 두 코드로 액세스 토큰을 획득한다. 이때 Client Secret은 절대 유출되서는 안 된다.
기존 코드가 어떤 문제가 있는지는 아래의 동작 메커니즘을 보면 이해가 편하다.
OAuth 2.0의 동작 메커니즘 (블로그 참조)
따라서 RedirectPage가 없이 카카오 버튼 컴포넌트에서 이를 모두 처리하여 성공시 성공페이지로 보내도 위 링크와 같이 Authorization Code 정보가 유출된다는 것.............. 다른 페이지로 사용자를 보내야한다.
아래처럼 리디렉션 경로를 나누고 리디렉션에서 code추출, POST요청, 로컬 저장까지 완료해서 사용자에게 로그인 성공페이지를 보여줘야한다.
아무튼 윗 동작 메커니즘으로 카카오 소셜 로그인 기능을 자세히 살펴보면
1 ~ 2. 로그인 요청
버튼을 클릭하면 OAuth를 사용하기 위해 Authorization on Server로 해당 정보를 보낸다.
<a href="https://kauth.kakao.com/oauth/authorize?client_id=3ab84610885da2d484d38c91444ad3e6&redirect_uri=http://127.0.0.1:5173/auth/kakao/callback&response_type=code" class="btn-kakao">
client_id : 애플리케이션 생성시 발급받은 ID, REST_API_KEY
redirect_uri : 애플리케이션 생성시 등록한 Redirect URI
response_type=code << 여기에 Authorization Code를 발급받을 수 있다.
scope : 클라이언트가 부여받은 리소스 접근 권한
스코프에 해당하는 권한을 제한적으로 획득할 수 있다. (ex 연락처 보기, 수정, 다운로드, 영구삭제)
3 ~ 4. 로그인 페이지 제공, ID/PW 제공
클라이언트가 만든 Authorization URL로 이동된 Resource Owner는 제공된 로그인 페이지에서 ID/PW를 입력하여 인증한다.
5 ~ 6. 로그인 요청
인증이 성공되었다면, Authorization Server은 제공된 Redirect URI로 사용자를 리디렉션시킨다. 이때 Redirct URI에 Authorization Code를 포함하여 사용자를 리디렉션 시킨다. 이는 Access Token을 획득하기 위해 사용하는 임시 코드로 수명이 매우 짧다. (1~10분)
7~ 8. Authorization Code 와 Access Token 교환
Client는 Authorization Server에 Authorization Code를 전달하고, Access Token을 응답받는다. Client(우리의 서비스)는 발급받은 Resource Owner의 Access Token을 저장하고, 매번 Resouce Server가 Resouce Owner에 접근하기 위해서 Access Token을 사용한다.
이때 Access Token은 유출되어서는 안되며, token 엔드페인트에서 교환이 이루어진다.
여기서 필수로 전달해야하는 매개변수는
grant_type=authorization_code
code=Authorization_code
redirect_uri=Redirect_URI
client_id=client_ID
client_secret=
9. 로그인 성공
위 과정을 성공적으로 마치면 Client는 Resource Owner에게 로그인이 성공하였음을 알리며 페이지를 이동한다.
10 ~ 13. Access Token으로 리소스 접근
Resource Owner가 Resource Server의 리소스가 필요한 기능을 Client에 요청한다. Client는 기존에 저장해둔 Resoucre Owner의 Access Token을 사용하여 제한된 리소스에 접근하고, Resoucre Owner에게 서비스를 제공한다.
그래서 Redirct URI을 통해 발급받는 과정이 생략된다면... << 현재 우리의 서비스다... 이 모호한 경계
Authorization Server가 Access Token을 Client에 전달하기 위해 Redirect URI를 통해야 한다. 이때, Redirect URI를 통해 데이터를 전달할 방법은 URL 자체에 데이터를 실어 전달하는 방법밖에 존재하지 않는다. 브라우저를 통해 데이터가 곧바로 노출되는 것 이다
하지만, Access Token은 민감한 데이터이다. 이렇게 쉽게 노출되어서는 안된다. 이런 보안 사고를 방지 Authorization Code를 사용하는 것 이다.
Redirect URI를 프론트엔드 주소로 설정하여, Authorization Code를 프론트엔드로 전달한다. 그리고 이 Authorization Code는 프론트엔드에서 백엔드로 전달된다. 코드를 전달받은 백엔드는 비로소 Authorization Server의 token 엔드포인트로 요청하여 Access Token을 발급한다.
이런 과정을 거치면 Access Token이 항상 우리의 어플리케이션과 OAuth 서비스의 백채널을 통해서만 전송되기 때문에 공격자가 Access Token을 가로챌 수 없게된다. (참고)
그러하다..