소셜 로그인 기능을 구현하면서 가장 먼저 고민했던 부분은 발급한 JWT를 어디에 저장하고, 어떻게 다음 요청에 전달할 것인가였다.
처음에는 단순히 토큰만 발급하면 될 것이라고 생각했지만, OAuth 로그인 흐름과 리디렉션, 보안 이슈까지 얽히면서 생각보다 여러 번 방향을 바꾸게 되었다.
이 글에서는 JWT 저장 방식을 바꾸며 겪은 문제와 최종적으로 HTTP-Only 쿠키를 사용하게 된 과정을 정리해 보려고 한다.
구현하려던 로그인 흐름은 다음과 같다.
JWT 전달 및 저장 방식에 대해 다음과 같은 시도를 했다.
처음에는 로그인 성공 후 JWT를 URL에 포함시키는 방식을 사용했다.
?token=... 형태로 전달하지만 URL에 토큰이 그대로 노출되어 보안상 문제가 있었다.
JWT는 인증 정보이기 때문에 이 방식은 유지하기 어렵다고 판단했다.
다음으로는 토큰을 응답 헤더에 담아 전달하는 방식을 시도했다.
URL 노출 문제는 해결되었지만, OAuth 로그인에서 문제가 발생했다.
소셜 로그인은 리디렉션 기반으로 동작하기 때문에 중간 응답에 포함된 헤더를 프론트에서 확인할 수 없었다.
다음으로는 서버가 JSON으로 토큰을 내려주고, 프론트에서 fetch로 받아 처리하는 방식도 고려했다.
회원가입과 같은 요청은 프론트에서 직접 요청하고 응답을 받는 구조이기 때문에 fetch 기반으로 토큰을 처리할 수 있었다.
반면에 로그인은 OAuth 인증 이후 브라우저 이동을 통해 진행되는데, fetch는 페이지 이동 없이 응답만 처리하기 때문에 동일한 방식으로 처리할 수 없었다.
정리하면 다음 조건이 필요했다.
이 조건을 만족시키기 위해 JWT를 HTTP-Only 쿠키에 저장하는 방식으로 변경했다.
JWT는 발급 후 HTTP-Only 쿠키에 저장한다.
따라서 프론트에서 별도로 토큰을 저장하거나 관리할 필요가 없다.
요청이 들어오면 쿠키에서 JWT를 꺼내 인증을 수행한다.
즉, 인증은 요청 시마다 서버에서 판단한다.
OAuth 콜백 이후 서버에서 사용자 상태에 따라 분기한다.
소셜 인증이 완료된 후 기존 회원으로 확인되면,
최초 로그인 사용자라면,
회원가입 페이지에서는 세션에 저장된 소셜 정보를 활용해 기본 정보를 보여주고,
추가 입력을 완료하면 가입 처리 후
비로그인 상태에서 인증이 필요한 기능에 접근할 경우, 원래 요청한 URL을 세션에 저장하도록 했다.
로그인이나 회원가입 이후 해당 값을 기반으로 페이지를 이동하도록 처리했다.
차단된 사용자는 인증 단계에서 접근을 제한하도록 처리했다.
이미 로그인된 상태에서 차단된 경우에도, 인증이 필요한 요청이 발생하면 동일하게 차단된다.
탈퇴는 계정 삭제가 아닌 상태 변경으로 처리했다.
다른 소셜 계정으로 로그인할 경우 기존에 가입한 소셜로 로그인하도록 안내했다.
이미 로그인된 사용자가 로그인 페이지에 접근하면, 현재 로그인된 계정 정보를 안내하도록 했다.
회원가입 페이지는 소셜 로그인 과정에서 받은 정보가 세션에 있을 때만 진입할 수 있도록 제한했다.
로그인 관련 안내 화면은 하나의 페이지로 구성하고, 상황에 따라 다른 메시지를 출력하도록 했다.
구현 후 다음과 같은 흐름을 확인했다.
로그인된 상태에서 사용자를 차단한 뒤 동작을 확인했다.
차단되더라도 즉시 로그아웃되지는 않고, 인증이 필요한 페이지에 접근할 경우 쿠키를 삭제한 뒤 차단 안내 화면으로 이동하는 것을 확인했다.
토큰 만료 시 쿠키가 정상적으로 제거되는지 확인했다.
만료된 토큰으로 요청이 들어오면 인증 과정에서 JWT 쿠키가 삭제되는 것을 확인했다.
다만 JWT는 발급 시점에 만료 시간이 결정되기 때문에, 설정 값을 변경하더라도 이미 발급된 토큰에는 반영되지 않는다.
현재 프로젝트는 HTTP 환경으로 배포되어 있어 JWT 쿠키에 Secure=true를 적용하지 못했다.
HTTP-Only 기반으로 자바스크립트 접근은 막을 수 있었지만, 네트워크 구간까지 고려하면 HTTPS 환경에서 Secure 옵션을 적용하는 것이 더 적절하다.
로그인 후 복귀 URL은 현재 세션을 기준으로 처리하고 있지만, 일부 흐름에서는 URL 파라미터를 통해 전달한 뒤 다시 세션에 저장하는 방식이 함께 사용되고 있다.
기능적으로는 동작하지만, 리디렉션 처리 방식을 통일하면 인증 흐름을 더 단순하게 관리할 수 있다.
로그인 후 복귀 URL이 내부 경로인지 검증하는 로직을 별도로 두지 않았다.
외부 URL이 주입될 가능성을 고려하면 보안상 취약점이 될 수 있다.
따라서 /post/..., /chat/...처럼 애플리케이션 내부 경로만 허용하도록 검증할 필요가 있다.
JWT 인증은 필터에서 처리하도록 했지만, 일부 컨트롤러에서는 쿠키를 직접 읽어 토큰을 다시 해석하는 로직을 사용하고 있다.
이후에는 인증 정보를 SecurityContext 기반으로만 조회하도록 통일하여 인증 로직을 한 곳에서 관리할 수 있도록 개선할 수 있다.
현재는 Access Token만 사용하고 있어 토큰 만료 시 재로그인이 필요하다.
구조가 단순하고 서버 상태를 최소화할 수 있다는 장점이 있지만, 토큰을 갱신할 수 있는 방식이 없다.
이후에는 Refresh Token을 도입해 재로그인 없이 인증을 유지할 수 있도록 개선할 수 있다.