iOS 앱에서 Apple 로그인과 탈퇴(OIDC + PKCE 기반) 완전 정복

호기성세균·2025년 4월 3일
0

Project

목록 보기
17/18

홍보부터 하겠습니다. 저희 서비스 많관부...
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와 PKCE란?

OIDC (OpenID Connect)
OAuth 2.0 기반의 인증 프로토콜로,
로그인한 사용자의 정보를 안전하게 확인하기 위해 id_token을 발급하는 방식입니다.

PKCE (Proof Key for Code Exchange)
OAuth 2.0 인가 코드 플로우에서 보안을 강화하기 위한 기법입니다.
비밀번호나 클라이언트 시크릿을 저장할 수 없는 앱 환경에서
code_verifiercode_challenge를 활용해 중간자 공격을 방지합니다.

🍏 왜 Apple 로그인은 OIDC + PKCE로 구현해야 할까?

  1. OAuth2만으로는 사용자 정보(id)가 없습니다
    Apple은 OAuth2만 제공하는 게 아니라, 여기에 OIDC를 얹어서
    id_token을 통해 사용자의 고유 ID (sub) 등 인증 정보를 제공합니다.

  2. 앱 환경은 보안상 PKCE가 필수입니다
    iOS/Android는 클라이언트 시크릿을 안전하게 숨길 수 없기 때문에
    PKCE를 통해 안전하게 인가 코드를 교환해야 합니다.


iOS 앱에서의 로그인 흐름 요약

  1. 앱이 Apple 로그인 화면 요청 (PKCE 포함)

  2. Apple이 사용자 로그인 처리 후 인가 코드 반환

  3. 앱이 인가 코드와 code_verifier를 서버에 전달

  4. 서버가 Apple에 토큰 요청 (access_token, id_token)

  5. 서버가 id_token을 검증하고 사용자 정보 파싱

  6. 서버에서 자체 JWT 발급 → 로그인 완료


🛠️ 백엔드(Spring)에서 Apple 로그인 처리

다음은 실제 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 신규 사용자 등록


🔐 클라이언트 시크릿은 직접 JWT로 생성해야 합니다

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 로그인은 단순히 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에서 사용자 탈퇴 처리하면 전체 탈퇴 절차가 완료됩니다.


📱 앱 심사를 위해 Apple 로그인은 필수입니다

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

profile
공부...열심히...

0개의 댓글