포스트

Final (20)

Apple 로그인 개선

모델링

1
2
3
4
5
6
7
8
9
import Foundation

struct AppleTokenResponse: Codable {
    let access_token: String?
    let expires_in: Int?
    let id_token: String?
    let refresh_token: String?
    let token_type: String?
}

배포전 여러 자료를 찾아보다가, 애플 계정 관련해서는 토큰이 있어야 한다는것을 보고 구현을 해보려 한다. 참고글을 보면서 만들었다.

✅ refreshToken 저장

Apple 로그인 성공 후 authorizationCode를 이용해 Apple 서버로 POST 요청을 보내 refresh_token을 획득한다.

예시:

1
2
3
4
5
6
7
8
9
10
11
if let code = appleCredential.authorizationCode,
   let codeString = String(data: code, encoding: .utf8) {
    self.getAppleRefreshToken(code: codeString) { refreshToken in
        guard let refreshToken = refreshToken else {
            completion(.failure(...))
            return
        }
        UserDefaults.standard.set(refreshToken, forKey: "appleRefreshToken")
        completion(.success(result))
    }
}

✅ JWT 발급 (makeJWT)

Apple 서버로 토큰을 요청하거나 revoke할 때 필요한 client_secret은 JWT로 생성해야 한다. 내부적으로는 다음과 같은 필드로 구성된다:

  • iss: Apple Team ID
  • sub: App Bundle ID
  • aud: https://appleid.apple.com

예시:

1
2
3
4
5
6
7
8
9
10
11
let myClaims = MyClaims(
    iss: "LA95MXQ3R5",
    iat: iat,
    exp: exp,
    aud: "https://appleid.apple.com",
    sub: "com.TeamSwiftbreakers.TteoPpoKki4U"
)

let privateKey = try Data(contentsOf: url)
let jwtSigner = JWTSigner.es256(privateKey: privateKey)
let signedJWT = try myJWT.sign(using: jwtSigner)

Apple 탈퇴: refreshToken revoke 흐름

Apple 사용자는 refresh_token을 직접 revoke 해줘야 한다.

예시:

1
2
3
4
5
6
7
8
9
10
guard let refreshToken = UserDefaults.standard.string(forKey: "appleRefreshToken") else {
    completion(.failure(...))
    return
}

let clientSecret = self.makeJWT()

self.revokeAppleToken(clientSecret: clientSecret, token: refreshToken) {
    self.deleteUserFromDatabase(uid: user.uid, completion: completion)
}

내부 요청 URL은 다음과 같다:

https://appleid.apple.com/auth/revoke?client_id=com.TeamSwiftbreakers.TteoPpoKki4U&client_secret=…&token=…&token_type_hint=refresh_token


Google 로그인: 토큰 저장

Google 로그인 성공 후, 추후 탈퇴에 사용할 토큰을 저장한다:

1
2
3
4
5
6
7
8
googleSignIn(result: GIDSignInResult, completion: ...) {
    let idToken = result.user.idToken?.tokenString ?? ""
    let accessToken = result.user.accessToken.tokenString

    UserDefaults.standard.set(idToken, forKey: "googleIDToken")
    UserDefaults.standard.set(accessToken, forKey: "googleAccessToken")
    ...
}

Google 탈퇴: 재인증 후 삭제

Firebase는 user.delete() 호출 전에 최근 인증이 필요하다.
Google 사용자는 reauthenticate 과정을 반드시 거쳐야 탈퇴가 가능하다.

예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
guard let idToken = UserDefaults.standard.string(forKey: "googleIDToken"),
      let accessToken = UserDefaults.standard.string(forKey: "googleAccessToken") else {
    completion(.failure(...))
    return
}

let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: accessToken)

user.reauthenticate(with: credential) { _, error in
    if let error = error {
        completion(.failure(error))
    } else {
        self.performDelete(user: user, completion: completion)
    }
}

회원 탈퇴 전체 흐름 (deleteCurrentUser)

회원탈퇴는 다음과 같은 분기 흐름으로 구성됨:

1
2
3
4
5
6
7
8
9
10
11
12
if let user = Auth.auth().currentUser {
    for provider in user.providerData {
        switch provider.providerID {
        case "apple.com":
            // Apple refreshToken revoke 후 삭제
        case "google.com":
            // Google reauthenticate 후 삭제
        default:
            break
        }
    }
}

데이터베이스 및 사용자 삭제

Firebase Auth와 Realtime Database에서 사용자 정보를 함께 삭제한다.

1
2
3
4
5
6
7
ref.child("users").child(uid).removeValue { error, _ in
    if let user = Auth.auth().currentUser {
        user.delete { error in
            ...
        }
    }
}

변경 요약 표

기능리팩토링 전리팩토링 후
Apple refreshToken 저장❌ 없음✅ authorizationCode로 요청 후 저장
Apple 토큰 revoke❌ 없음✅ revoke API 직접 호출
Google 탈퇴 재인증❌ 없음✅ reauthenticate + delete
JWT 서명❌ 없음✅ .p8 키 기반 client_secret 생성
회원탈퇴 흐름단순 delete✅ provider별 분기 + 보안 처리

결론

이번 리팩토링을 통해 다음과 같은 실질적인 문제가 해결되었다:

  • Apple 인증 후 토큰 저장/관리 문제
  • Google 탈퇴 시 auth/requires-recent-login 에러
  • 사용자 탈퇴 전 인증 프로세스 통합 처리
  • 외부 OAuth 시스템과 Firebase의 연결을 보안적으로 안전하게 구성

당시 문제 해결 과정에서 공식 문서나 기존 라이브러리만으로는 흐름을 파악하기 어려웠고, GPT를 통한 시도도 명확하지 않아 시행착오가 많았다.
현재 구조는 이 경험을 바탕으로 앱 서비스 운영에 적합하도록 정리된 결과물이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.