Apple 소셜 로그인 콜백 세션 유실
목차
Apple OAuth를 처음 붙일 때 구글이나 카카오랑 같은 방식으로 접근했다가 꽤 고생했다. 다른 provider는 콜백이 GET으로 오는 게 일반적인데 Apple만 response_mode=form_post를 강제한다. 이게 단순히 HTTP 메서드 하나 바꾸는 문제가 아니라, 연쇄적으로 CORS, 봇 필터, 세션 관리 세 군데를 동시에 건드린다.
form_post가 문제를 연쇄적으로 터뜨리는 이유
Apple이 GET 대신 POST를 선택한 건 나름의 근거가 있다. 인증 코드나 id_token 같은 민감한 파라미터를 URL 쿼리스트링에 노출하지 않기 위해서다. GET 방식이면 브라우저 히스토리, 서버 접근 로그, Referer 헤더에 토큰값이 그대로 남는다. Apple 입장에서는 보안상 올바른 선택이지만, 서버 쪽에서는 기존에 잘 동작하던 인프라들이 하나씩 오해를 시작한다.
첫 번째 벽은 봇 차단 필터다. Apple의 OAuth 서버 IP에서 POST 요청이 들어오면, 봇 차단 레이어가 브라우저가 아닌 자동화 요청으로 판단하고 403을 뱉는다. User-Agent도 일반 브라우저와 다르고, POST body에 form 데이터가 담겨 있으니 더욱 의심스럽게 보인다. 콜백 URL을 필터 예외 목록에 명시적으로 추가하지 않으면 인증 흐름 자체가 완전히 막힌다.
두 번째는 CORS다. 브라우저가 Apple 인가 서버로 리다이렉트되고, Apple 서버가 우리 서비스 콜백 URL로 직접 POST 요청을 보내는 흐름이다. 이때 origin이 appleid.apple.com이 되므로 CORS 허용 목록에 Apple 도메인이 없으면 preflight에서 400을 돌려준다.
세 번째가 세션 유실이다. 보통 OAuth 흐름에서 인증 전 세션에 state 파라미터를 저장해두고, 콜백에서 검증한 뒤 사용자 정보를 세션에 쓴다. POST 콜백에서는 이 과정에서 세션이 끊기는 케이스가 생긴다. SameSite 쿠키 정책이 Lax 또는 Strict로 걸려 있으면 cross-site POST 요청에서 세션 쿠키가 전달되지 않는다. 결국 세션 없는 새 요청처럼 처리되고, 로그인은 실패한다. 세 문제가 동시에 터지면 어디서 막힌 건지 파악하는 것 자체가 시간 잡아먹는다. 로컬에서 잘 됐던 게 운영에서 403으로 터진 이유가 여기 있었다. 운영 서버에는 봇 필터가 훨씬 촘촘하게 걸려 있으니.
수정한 흐름을 정리하면 이렇다.
BotBlockFilter → Apple 콜백 URL 예외 추가
WebMvcConfig → CORS 허용 URL 추가
OAuthController → 세션 재설정 로직 추가
| 버그 | 증상 | 해결책 |
|---|---|---|
| 봇 필터 차단 | 콜백 403 에러 | BotBlockFilter 예외 처리 |
| CORS 차단 | 콜백 400 에러 | WebMvcConfig에 Apple URL 허용 |
| 세션 유실 | 로그인 실패 | 콜백 후 세션 강제 재설정 |
세션 재설정 코드는 단순하다. 콜백 처리가 끝난 뒤 세션에 사용자 정보를 직접 박아 넣는다.
// 콜백 후 세션 재설정
session.setAttribute("loginUser", member);
session.setAttribute("sysId", member.getSysId());
Spring Security를 쓴다면 SecurityContextHolder까지 갱신해야 할 수도 있는데, 내부 세션 기반 인증이면 위 두 줄로 충분했다. 주의할 점은 session.invalidate() 후 새 세션을 얻거나, 기존 세션을 그대로 살리면서 값만 덮어쓰거나, 둘 중 하나를 일관되게 선택해야 한다는 거다. 중간에 세션 무효화 로직이 섞이면 오히려 더 꼬인다.
Apple만의 추가 함정 - 첫 인증에만 사용자 정보를 준다
이게 가장 놓치기 쉬운 부분이다. Apple은 최초 인증 때 딱 한 번만 사용자 이름과 이메일을 form body의 user 파라미터에 담아 보낸다. 두 번째 로그인부터는 sub(고유 식별자)만 온다. 구글이나 카카오는 userinfo 엔드포인트를 통해 매번 가져올 수 있는데, Apple은 그게 없다.
따라서 콜백에서 user 파라미터가 존재하면 즉시 DB에 저장해야 한다. 타이밍을 놓치면 복구 방법이 없다. 사용자가 Apple 계정에서 앱 연동을 해제하고 다시 연결하면 그때 또 한 번 보내주긴 하는데, 그걸 기대하고 설계하면 안 된다.
String userJson = request.getParameter("user");
if (userJson != null && !userJson.isEmpty()) {
// 최초 인증 시에만 name/email 파라미터가 존재함
// 이 시점에 반드시 DB에 저장
AppleUserInfo appleUser = objectMapper.readValue(userJson, AppleUserInfo.class);
member.setEmail(appleUser.getEmail());
member.setName(appleUser.getName().getFirstName());
memberService.save(member);
}
provider별 분기가 늘어나는 게 코드 복잡도 면에서 불편하긴 하다. 같은 OAuth 2.0 스펙 위에 있는데 동작이 이렇게 다르다. SNS 연동은 결국 분기 처리가 불가피하다는 걸 이번에 다시 확인했다.
이번 작업을 정리하면서 느낀 건, Apple OAuth를 제대로 붙이려면 세 레이어를 동시에 열어두고 고쳐야 한다는 거다. 어느 하나만 수정하면 또 다른 곳에서 터진다. 다음번에 Apple Sign In을 새로 붙인다면 처음부터 아래 네 가지를 체크리스트로 들고 들어갈 것 같다.
- BotBlockFilter 또는 WAF 레벨에서 Apple 콜백 경로 예외 등록
- CORS 허용 origin에
appleid.apple.com추가 - SameSite=None; Secure 쿠키 설정, 또는 콜백 후 세션 명시적 재설정
- 첫 콜백에서
user파라미터 존재 여부 체크 후 즉시 영속화
OAuth 스펙이 같다고 구현도 같을 거라 가정하면 꼭 어딘가에서 터진다. Apple은 보안 의도가 분명한 만큼 기존 흐름과 어긋나는 부분이 많고, 그 어긋남이 한꺼번에 세 군데서 나온다는 게 이번 디버깅의 핵심이었다.
댓글 0
첫 댓글 달아줘.