홍보부터 하겠습니다. 저희 서비스 많관부...
https://climb-log.my/app/download
안녕하세요. 디프만 클로그 서비스 백엔드 개발자 최혜미입니다.
iOS 앱에서 소셜 로그인을 구현할 때, Apple 로그인의 구조는 유독 복잡해서 많은 개발자들이 처음에 어려움을 겪습니다.
특히 OIDC, PKCE, 클라이언트 시크릿, revoke
요청 등 Apple만의 규칙이 있기 때문입니다.
이 글에서는 iOS 앱과 Spring 백엔드 기준으로 Apple 로그인과 탈퇴 흐름을 실제 코드 예시와 함께 정리해보겠습니다.
iOS 앱에서는 공식 SDK 사용이 기본입니다.
iOS 앱에서 소셜 로그인을 구현할 때는 각 플랫폼이 제공하는 공식 SDK (예: Apple SDK)를 사용하는 것이 기본입니다.
- 네이티브 환경에서는 브라우저/앱 간 쿠키와 세션이 공유되지 않기 때문에, SDK를 사용하지 않으면 앱에서 로그인했는데 웹에서는 로그인되지 않는 문제가 자주 발생합니다.
- SDK를 사용하면 로그인 상태 유지, 토큰 전달, 리디렉션 관리 등을 앱에 맞게 처리할 수 있습니다.
또한, 모든 SDK가 OIDC 기반 인증을 따르지만, 제공하는 인증 정보(idToken, accessToken 등)는 서비스마다 다르기 때문에
❗️클라이언트 개발자와 사전에 협의가 필요합니다.❗️
(예를 들어, 카카오 SDK는 로그인 시 idToken을 바로 내려줍니다.)
OIDC (OpenID Connect)
OAuth 2.0 기반의 인증 프로토콜로,
로그인한 사용자의 정보를 안전하게 확인하기 위해 id_token을 발급하는 방식입니다.
PKCE (Proof Key for Code Exchange)
OAuth 2.0 인가 코드 플로우에서 보안을 강화하기 위한 기법입니다.
비밀번호나 클라이언트 시크릿을 저장할 수 없는 앱 환경에서
code_verifier
와 code_challenge
를 활용해 중간자 공격을 방지합니다.
OAuth2만으로는 사용자 정보(id)가 없습니다
Apple은 OAuth2만 제공하는 게 아니라, 여기에 OIDC를 얹어서
id_token
을 통해 사용자의 고유 ID (sub
) 등 인증 정보를 제공합니다.
앱 환경은 보안상 PKCE가 필수입니다
iOS/Android는 클라이언트 시크릿을 안전하게 숨길 수 없기 때문에
PKCE를 통해 안전하게 인가 코드를 교환해야 합니다.
앱이 Apple 로그인 화면 요청 (PKCE 포함)
Apple이 사용자 로그인 처리 후 인가 코드 반환
앱이 인가 코드와 code_verifier
를 서버에 전달
서버가 Apple에 토큰 요청 (access_token
, id_token
)
서버가 id_token
을 검증하고 사용자 정보 파싱
서버에서 자체 JWT 발급 → 로그인 완료
다음은 실제 Spring에서 Apple 로그인을 처리하는 코드입니다.
핵심은 id_token
을 검증하고 사용자 정보를 안전하게 추출하는 부분입니다.
override fun login(request: AppleLoginRequest): AuthResponseDto {
val tokenResponse = requestAppleAccessToken(request.code, request.codeVerifier)
val idToken = tokenResponse["id_token"] as? String
?: throw AuthException(ErrorCode.ID_TOKEN_MISSING)
val appleUser = validateAndParseAppleIdToken(idToken)
val user = userRepository.findByLoginIdAndProviderAndIsDeletedFalse(appleUser.id, Provider.APPLE)
?: registerNewAppleUser(appleUser)
return tokenService.generateTokens(user)
}
Apple에서 받은 id_token
은 다음과 같은 절차로 검증합니다:
공개키를 Apple JWKS(https://appleid.apple.com/auth/keys) 에서 가져와
JWT 서명 확인
iss
, aud
, exp
등의 클레임 검증
사용자 정보를 추출하여 로그인 처리 or 신규 사용자 등록
Apple은 일반 OAuth2와 달리, 클라이언트 시크릿을 JWT 형식으로 직접 생성해야 합니다.
서명 방식은 ES256(ECDSA) 이며, kid
, iss
, sub
, aud
등의 값이 포함되어야 합니다.
private fun generateAppleClientSecret(): String {
// 비밀키 디코딩 및 서명
return Jwts.builder()
.setHeaderParam("kid", appleKeyId)
.setIssuer(appleTeamId)
.setIssuedAt(Date(nowMillis))
.setExpiration(Date(expMillis))
.setAudience("https://appleid.apple.com")
.setSubject(appleClientId)
.signWith(privateKey, SignatureAlgorithm.ES256)
.compact()
}
이 부분은 많은 개발자가 처음에 헷갈리는 부분이니 주의해야 합니다.
Apple 로그인은 단순히 DB에서 계정만 삭제해서 끝나는 방식이 아닙니다.
사용자와 Apple 간의 연결 자체를 끊기 위해
Apple의 revoke
엔드포인트에 refresh_token
을 보내야 합니다.
fun revokeAppleAccount(authorizationCode: String) {
val clientSecret = generateAppleClientSecret()
val refreshToken = handler.requestAppleAccessToken(
authorizationCode,
UUID.randomUUID().toString()
)["refresh_token"] as String
val body = LinkedMultiValueMap<String, String>().apply {
add("client_id", appleClientId)
add("client_secret", clientSecret)
add("token", refreshToken)
add("token_type_hint", "refresh_token")
}
try {
restTemplate.postForEntity("https://appleid.apple.com/auth/revoke", HttpEntity(body, headers), String::class.java)
} catch (e: Exception) {
throw AppleRevokeException("애플 계정 탈퇴 처리 중 revoke 요청 실패", e)
}
}
탈퇴 시 반드시 refresh_token
이 필요합니다.
이 토큰은 로그인 후 바로 받아와 저장하거나, 탈퇴 요청 시 다시 받아야 합니다.
클라이언트 시크릿은 이때도 새로 생성해서 사용해야 합니다.
Apple에 정상적으로 revoke
요청을 보내고, 그 후 내부 DB에서 사용자 탈퇴 처리하면 전체 탈퇴 절차가 완료됩니다.
iOS 앱에서 다른 소셜 로그인(Google, Kakao 등)을 제공하는 경우, Apple 로그인도 반드시 함께 제공해야 앱 심사를 통과할 수 있습니다. 사실상 선택권이 없다는 뜻이죠 ㅎ
“Facebook, Google 등 외부 로그인 서비스를 사용하는 앱은 반드시 'Apple로 로그인'도 지원해야 합니다.”
(App Store Review Guidelines 4.8)
예외는 거의 없으며, 실무에서는 SDK 기반 Apple 로그인 구현은 필수입니다.
웹 기반 소셜 로그인에는 자신 있었던 저에게, iOS 앱 환경의 Apple 로그인은 생각보다 큰 벽이었습니다.
특히 세션 기반 인증을 그대로 가져가려다 앱에서는 쿠키가 공유되지 않아 로그인 연동이 꼬이는 경험을 여러 번 했습니다.
하지만 Apple 로그인 구조를 정확히 이해하고,OIDC + PKCE 기반으로 클라이언트와 서버의 역할을 명확히 나눠 구현하니 생각보다 깔끔하고 안정적으로 구성할 수 있었습니다.
이 글이 Apple 로그인을 처음 붙이는 분들께
불필요한 삽질을 줄이고, 빠르게 구현하는 데 도움이 되었으면 합니다. 🙌
📎 실제 구현 코드는 GitHub에서 확인하실 수 있습니다!
https://github.com/depromeet/clog-server