๐ Sign in with Apple
The most privacy-friendly login. Hide email, automatic 2FA, Face ID auth!
โจ Sign in with Apple is?
Simple sign-in with Apple ID. You can hide your email, and two-factor authentication is handled automatically. Required if your app has other social logins.
๐ฏ Basic Implementation
import AuthenticationServices class SignInManager: NSObject, ASAuthorizationControllerDelegate { func signInWithApple() { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() // ์์ฒญํ ์ ๋ณด (์ด๋ฆ, ์ด๋ฉ์ผ) request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } // ์ฑ๊ณต ์ func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { let userID = credential.user // ๊ณ ์ ID (๋ณํ์ง ์์) let email = credential.email // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต let fullName = credential.fullName // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต let identityToken = credential.identityToken // JWT ํ ํฐ // ๋ฐฑ์๋๋ก identityToken ์ ์กํ์ฌ ๊ฒ์ฆ if let tokenData = identityToken, let tokenString = String(data: tokenData, encoding: .utf8) { verifyTokenWithBackend(tokenString, userID: userID) } // UserDefaults์ userID ์ ์ฅ UserDefaults.standard.set(userID, forKey: "appleUserID") } } // ์คํจ ์ func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error ) { print("๋ก๊ทธ์ธ ์คํจ: \(error.localizedDescription)") } func verifyTokenWithBackend(_ token: String, userID: String) { // ๋ฐฑ์๋ API ํธ์ถ } } extension SignInManager: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return UIApplication.shared.windows.first! } }
๐จ Sign in with Apple Button in SwiftUI
import SwiftUI import AuthenticationServices struct SignInWithAppleButton: View { @Environment(\.colorScheme) var colorScheme var body: some View { SignInWithAppleButton( .signIn, onRequest: { request in request.requestedScopes = [.fullName, .email] }, onCompletion: { result in switch result { case .success(let authorization): handleAuthorization(authorization) case .failure(let error): print("์๋ฌ: \(error)") } } ) .signInWithAppleButtonStyle( colorScheme == .dark ? .white : .black ) .frame(height: 50) } func handleAuthorization(_ authorization: ASAuthorization) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { print("์ฌ์ฉ์ ID: \(credential.user)") print("์ด๋ฉ์ผ: \(credential.email ?? \"์์\")") } } } // ์ฌ์ฉ ์์ struct LoginView: View { var body: some View { VStack(spacing: 20) { Text("๋ก๊ทธ์ธ") .font(.largeTitle.bold()) SignInWithAppleButton() .padding(.horizontal) } } }
๐ ์๋ ๋ก๊ทธ์ธ ์ํ ์ฒดํฌ
import AuthenticationServices func checkCredentialState() { guard let userID = UserDefaults.standard.string(forKey: "appleUserID") else { // ์ ์ฅ๋ userID ์์ return } let provider = ASAuthorizationAppleIDProvider() provider.getCredentialState(forUserID: userID) { state, error in switch state { case .authorized: print("๋ก๊ทธ์ธ ์ ์ง ์ค") // ์๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ case .revoked: print("๋ก๊ทธ์ธ ์ทจ์๋จ") // ๋ก๊ทธ์์ ์ฒ๋ฆฌ UserDefaults.standard.removeObject(forKey: "appleUserID") case .notFound: print("์ฌ์ฉ์ ์์") case .transferred: print("๋ค๋ฅธ ํ์ผ๋ก ์ด์ ๋จ") @unknown default: break } } } // ์ฑ ์์ ์ ์ฒดํฌ func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { checkCredentialState() return true }
๐ง ์ด๋ฉ์ผ ๋ฆด๋ ์ด (Email Relay)
When users choose "Hide My Email", Apple provides a proxy email (`privaterelay@icloud.com`).
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) { if let email = credential.email { // ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต๋จ print("์ด๋ฉ์ผ: \(email)") // privaterelay@icloud.com ํํ๋ฉด ํ๋ก์ ์ด๋ฉ์ผ if email.contains("privaterelay") { print("์ฌ์ฉ์๊ฐ ์ด๋ฉ์ผ์ ์จ๊น") } // โ ๏ธ ์ค์: ์ฒซ ๋ก๊ทธ์ธ ์๋ง ์ ๊ณต๋๋ฏ๋ก ๋ฐ๋์ ์ ์ฅ! UserDefaults.standard.set(email, forKey: "userEmail") } }
๐ ๋ฐฑ์๋ ํ ํฐ ๊ฒ์ฆ
// identityToken์ JWT ํ์์ ๋๋ค // ๋ฐฑ์๋์์ Apple ๊ณต๊ฐํค๋ก ๊ฒ์ฆํด์ผ ํฉ๋๋ค func sendTokenToBackend(_ credential: ASAuthorizationAppleIDCredential) async { guard let tokenData = credential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { return } // authorizationCode๋ ๋ฐฑ์๋๋ก ์ ์ก (refresh token ๋ฐ๊ธ์ฉ) guard let codeData = credential.authorizationCode, let code = String(data: codeData, encoding: .utf8) else { return } // API ํธ์ถ let body: [String: Any] = [ "identityToken": token, "authorizationCode": code, "user": credential.user ] // POST to your backend } /* ๋ฐฑ์๋ ๊ฒ์ฆ (Node.js ์์) const jwt = require('jsonwebtoken'); const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://appleid.apple.com/auth/keys' }); function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { callback(null, key.publicKey || key.rsaPublicKey); }); } jwt.verify(identityToken, getKey, (err, decoded) => { if (err) { // ํ ํฐ ๊ฒ์ฆ ์คํจ } else { // decoded.sub === userID // decoded.email === ์ฌ์ฉ์ ์ด๋ฉ์ผ } }); */
๐ฑ ๊ธฐ์กด ๊ณ์ ์ ๊ทธ๋ ์ด๋
import AuthenticationServices // ๊ธฐ์กด ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ ๋ก๊ทธ์ธ์ Apple ID๋ก ์ ๊ทธ๋ ์ด๋ func upgradeToAppleID(existingEmail: String) { let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName] // ๊ธฐ์กด ๊ณ์ ์ฐ๊ฒฐ์ ์ํ ํํธ request.user = existingEmail let controller = ASAuthorizationController(authorizationRequests: [request]) // ... delegate ์ค์ controller.performRequests() }
๐จ ๋ก๊ทธ์์ & ๊ณ์ ์ญ์
// ๋ก๊ทธ์์ func signOut() { UserDefaults.standard.removeObject(forKey: "appleUserID") UserDefaults.standard.removeObject(forKey: "userEmail") // UI๋ฅผ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก } // ๊ณ์ ์ญ์ (๋ฐฑ์๋์์ Apple API ํธ์ถ ํ์) /* ์ฌ์ฉ์๊ฐ "์ค์ > Apple ID > ์ํธ ๋ฐ ๋ณด์ > Apple๋ก ๋ก๊ทธ์ธ์ ์ฌ์ฉํ๋ ์ฑ"์์ ์ฑ์ ์ ๊ฑฐํ๋ฉด ๋ก๊ทธ์ธ์ด ์ทจ์๋ฉ๋๋ค. ๋ฐฑ์๋์์๋ Apple์ Token Revocation API๋ฅผ ํธ์ถํด์ผ ํฉ๋๋ค: POST https://appleid.apple.com/auth/revoke ์ดํ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ณ ๊ณ์ ์ ๋นํ์ฑํํฉ๋๋ค. */
โ๏ธ ์ฑ ์ค์ (Capabilities)
1. Xcode์์ ํ๋ก์ ํธ ์ ํ
2. Signing & Capabilities ํญ
3. "+ Capability" ํด๋ฆญ
4. "Sign in with Apple" ์ถ๊ฐ
5. Apple Developer ์ฝ์์์:
- Identifier์ "Sign in with Apple" ํ์ฑํ
- Services ID ์์ฑ (์น ๋ก๊ทธ์ธ์ฉ, ์ ํ)
- Key ์์ฑ (๋ฐฑ์๋ ๊ฒ์ฆ์ฉ)โ ํ์ ์๊ตฌ์ฌํญ (App Review)
โ ๏ธ ํ์!
If your app has other social logins (Google, Facebook, etc.),
Sign in with Apple์ ๋ฐ๋์ ๊ตฌํํด์ผ .
Otherwise, it will be rejected in App Store review.
๐ก HIG Checklist
โ
Use the official Sign in with Apple button
โ
๋ค๋ฅธ ๋ก๊ทธ์ธ ๋ฒํผ๊ณผ ๋์ผํ ํฌ๊ธฐ/์์น
โ
Email/name is only provided on first login (must save)
โ
ํ ํฐ์ ๋ฐฑ์๋์์ ๊ฒ์ฆ
โ
์ฑ ์์ ์ credential ์ํ ์ฒดํฌ