OAuth2.0 소셜 로그인이 성공했을 때 토큰 전달하기

junto·2024년 6월 16일
0

spring

목록 보기
15/30
post-thumbnail

서버에서 발행한 토큰들은 http로 응답하면 되는 거 아닌가? 전달하는 과정에서 무슨 문제가 있지? 먼저 rfc6749에 나와있는 Authorization Code 인증 방식을 살펴본다.

OAuth 2.0 인증 처리 과정

1. 구글 인증 서버에 인증 요청

  • {도메인}/oauth2/authorization/google, 302 Found
  • 소셜 로그인 버튼을 클릭하면 백엔드가 등록한 구글 인증 서버로 리다이렉트 된다. 이 과정에서 302 FOUND 상태 코드가 반환된다.

2. 구글 OAuth2.0 로그인 URL 접근

  • 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

3. 구글 Oauth2.0 인증 코드 발급

  • {도메인}/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

4. 구글 인증 코드로 인증 서버에 접근하여 Access Token 발급 및 Access Token으로 리소스 획득

  • Spring으로 이미 구현된 Oauth2 client api를 이용하면 Access token 발급 받는 과정이 브라우저에 노출되지 않는다.
  • Debug Mode로 살펴보면 시큐리티 필터 과정이 진행되면서 Access Token을 발급받고, 이를 가지고 리소스 서버에서 사용자 정보를 가져오는 것을 확인할 수 있다.
  • 전체적인 인증 흐름은 아래와 같다.
  • Spring을 사용할 경우 아래와 같은 필터들을 거친다.

OAuth2.0 로그인 구현하는 방법

1. 프론트가 인증 책임을 일부 지는 방법

  • 구글링을 해보면, 프론트에서 인가 코드를 받고 프론트에서 백엔드로 인가 코드를 전달하면서 백엔드로부터 서버가 발행한 jwt토큰과 사용자 정보를 받는 코드를 쉽게 찾아볼 수 있다. 하지만 이는 권장되지 않는 방법이다.

2. 백엔드에서 모든 인증 책임을 지는 방법

  • 인가 코드, 액세스 토큰 모두 백엔드에서 발행받는 것이다. 그림으로 나타내면 아래와 같다.
  • 인증을 완료하고, http 응답에 토큰을 작성하면 프론트앤드가 토큰을 받을 수가 없다. 아래는 body에 토큰을 작성했을 때 보이는 브라우저 화면이다. 클라이언트가 볼 수 없다.

백엔드가 모든 인증 책임을 질 때 토큰 전달 방법

  • 백엔드가 모든 인증 책임을 질 때 클라이언트에게 토큰을 전달하는 방법을 살펴본다.

1. URL 파라미터

  • 가장 쉬운 방법은 인증이 완료되고, 이를 로그인이 완료된 페이지로 redirect하면서 URL에 토큰을 넘기는 방법이다. 클라이언트는 Refresh Token을 URL로 받아서 해당 Refresh Token으로 Access Token과 Refresh Token을 새로 발급받는 방법이다. 물론, 기존에 발행한 Refresh Token은 서버 측에서 무효화시켜야 한다.
  • RFC 문서에 나와있듯이 URL 파라미터로 절대 토큰을 넘기지 말라고 나와있다. (MUST NOT)

2. JavaScript

  • 서버에서 JavaScript를 이용해 클라이언트에게 토큰을 전달할 수도 있다. 클라이언트가 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]);

3. 쿠키나 세션

  • 백엔드는 OAuth2.0 로그인이 성공했을 때 쿠키로 안전하게 토큰들을 발행하고, 로그인 완료된 화면으로 redirect하면 된다.

그런데, 일반 일반 로그인을 완료하면 Http 응답 메시지로 토큰을 발급한다. 위와 형식을 맞추기 위해 소셜 로그인을 완료했을 때 임시 Refresh Token을 쿠키로 발급해 주었다.

  • httponly, samesite, Secure 옵션으로 발행한 뒤 redirect url로 클라이언트가 해당 토큰으로 액세스 토큰과 리프레시 토큰을 재발행하는 API를 호출하게끔 handler를 설정한다. 클라이언트는 해당 핸들러로 특정 API에서 쿠키가 유효하다면 새로 토큰들을 http 응답 메시지로 발행받고, 서버는 발급한 쿠키를 삭제한다.
  • 세션 방식도 위와 크게 다르지 않다.

참고자료

profile
꾸준하게

0개의 댓글