개발 slecs

Apple OAuth form_post 콜백 세션 유실로 SNS 연동 실패

목차

Apple OAuth를 처음 붙여보는 개발자가 가장 많이 걸리는 함정이 바로 form_post 콜백이다. 구글이나 카카오는 OAuth 인증 완료 후 ?code=xxx&state=yyy 파라미터를 붙여서 GET 리다이렉트로 돌아온다. 근데 Apple은 다르다. 명세에서 response_mode=form_post를 강제하기 때문에 콜백이 POST 요청으로 들어온다. 이 차이 하나가 CORS, 봇 필터, 세션 전부를 건드린다.

왜 Apple이 이걸 강제하냐면, 보안 때문이다. GET 리다이렉트는 인가 코드(code)가 URL에 노출되는데, 브라우저 히스토리·서버 액세스 로그·리퍼러 헤더 어디서든 줄줄 샐 수 있다. Apple은 codeid_token을 요청 바디에 담아 POST로 때려 넣는 방식을 선택했다. 논리는 맞는데, 그 때문에 서버 입장에서 고려해야 할 게 확 늘어난다.

세 가지 차단 지점을 순서대로 뚫었다

실제 수정 흐름을 보면 한 방에 해결한 게 아니라 레이어별로 막히는 걸 하나씩 뚫는 구조였다.

BotBlockFilter → Apple 콜백 URL 예외 추가
WebMvcConfig   → CORS 허용 URL 추가
OAuthController → 세션 재설정 로직 추가

첫 번째 막힌 곳은 봇 차단 필터. Apple 인증 서버가 form_post 콜백을 날릴 때 요청 주체는 사용자 브라우저가 아니라 Apple 서버 자체다. 그러니까 요청 IP가 Apple 데이터센터 대역에서 들어온다. 봇 차단 필터가 이걸 크롤러·스캐너로 판단해서 403을 뱉어버린다. Apple 콜백 경로(/oauth/callback/apple 같은 엔드포인트)는 필터 예외 목록에 명시적으로 박아야 한다.

두 번째는 CORS. POST 요청이고 Content-Type: application/x-www-form-urlencoded인데, 오리진이 appleid.apple.com이다. CORS preflight가 붙는 경우 서버가 허용된 오리진 목록에 Apple URL이 없으면 400으로 떨어진다. WebMvcConfig나 Spring Security CORS 설정에 Apple 콜백 URL을 명시적으로 추가해야 한다.

세 번째가 세션 유실. 이게 가장 골치 아팠다. 일반적인 OAuth 플로우는 같은 브라우저 탭에서 리다이렉트가 연속으로 일어나기 때문에 세션 쿠키가 자연스럽게 유지된다. 근데 form_post는 Apple 서버에서 콜백을 POST로 던지는 구조라서 브라우저-서버 간 세션 컨텍스트가 끊길 수 있다. 콜백 처리 후 세션을 명시적으로 다시 심어줘야 한다.

// 콜백 후 세션 강제 재설정
session.setAttribute("loginUser", member);
session.setAttribute("sysId", member.getSysId());

이게 없으면 인증 자체는 성공했는데 로그인이 안 된 상태로 홈에 떨어지는, 사용자 입장에서 뭔지 알 수 없는 실패가 발생한다.

버그 증상 해결책
봇 필터 차단 콜백 403 에러 BotBlockFilter 예외 처리
CORS 차단 콜백 400 에러 WebMvcConfig에 Apple URL 허용
세션 유실 인증 성공인데 로그인 실패 콜백 후 세션 강제 재설정

Apple의 진짜 함정 - 이름·이메일은 딱 한 번만 온다

수정하면서 더 중요하게 챙겨야 했던 게 이거다. Apple은 사용자가 최초 인증할 때 한 번만 이름과 이메일을 id_token이나 콜백 파라미터로 넘겨준다. 두 번째 로그인부터는 안 넘긴다. Apple 계정 설정에서 앱 연동을 해제했다가 다시 연동해도 안 온다는 얘기도 있다.

구글이나 카카오는 매번 로그인할 때마다 이름·이메일을 넘겨주니까, 콜백 때마다 DB를 갱신해도 문제없다. Apple은 첫 콜백에서 저장 안 하면 영원히 못 가져온다. 그래서 로직을 이렇게 분기해야 한다.

  • 신규 가입: 콜백 파라미터에서 이름·이메일 추출 → DB 저장 → 세션 설정
  • 기존 회원: sub(Apple 사용자 식별자)로 DB 조회 → 세션 설정 (이름·이메일 갱신 시도 X)

Apple이 이름 파라미터를 넘길 때 형식도 특이한데, JSON 형태의 user 파라미터로 {"name":{"firstName":"길동","lastName":"홍"},"email":"[email protected]"} 이런 식으로 온다. 이걸 파싱하는 코드가 없으면 이름이 null로 저장된다.

그리고 Apple은 이메일 숨기기 옵션을 제공하기 때문에 실제 이메일이 아니라 @privaterelay.appleid.com 도메인의 릴레이 주소가 올 수 있다. 이메일 기준으로 계정 매칭·중복 체크 로직을 짜뒀다면 이 부분에서 예상치 못한 동작이 나온다. Apple 연동은 sub 값(Apple 사용자 식별자)을 primary key로 쓰는 게 맞다.


provider별로 OAuth 동작 방식이 다 달라서 결국 분기 처리를 피할 수 없다. 공통 인터페이스로 추상화를 잘 해놨더라도 Apple만큼은 예외 케이스가 많아서 별도 처리 블록이 생기게 된다. 이번 수정에서 그걸 다시 확인했다. 다음에 신규 provider 붙일 때는 콜백 방식(GET/POST), 봇 필터 충돌 여부, 세션 처리, 사용자 정보 제공 시점 이 네 가지를 체크리스트로 먼저 보고 시작할 것 같다.

댓글 0

첫 댓글 달아줘.