๐ŸŒ KO

๐Ÿ” Sign in with Apple

The most privacy-friendly login. Hide email, automatic 2FA, Face ID auth!

โญ Difficulty: โญโญ โฑ๏ธ Est. Time: 1-2h ๐Ÿ“‚ App Services

โœจ 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

SignInManager.swift
import AuthenticationServices

class SignInManager: NSObject, ASAuthorizationControllerDelegate {
    func signInWithApple() {
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()

        // Requestํ•  ์ •๋ณด (์ด๋ฆ„, ์ด๋ฉ”์ผ)
        request.requestedScopes = [.fullName, .email]

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }

    // Success ์‹œ
    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")
        }
    }

    // Failed ์‹œ
    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

SignInButton.swift
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: \(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 ?? \"์—†์Œ\")")
        }
    }
}

// Usage Example
struct LoginView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("๋กœ๊ทธ์ธ")
                .font(.largeTitle.bold())

            SignInWithAppleButton()
                .padding(.horizontal)
        }
    }
}

๐Ÿ”„ Auto Login Status Check

CredentialCheck.swift
import AuthenticationServices

func checkCredentialState() {
    guard let userID = UserDefaults.standard.string(forKey: "appleUserID") else {
        // Save๋œ 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("other ํŒ€์œผ๋กœ ์ด์ „๋จ")

        @unknown default:
            break
        }
    }
}

// App ์‹œ์ž‘ ์‹œ ์ฒดํฌ
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`).

EmailRelay.swift
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
    if let email = credential.email {
        // ์ฒซ ๋กœ๊ทธ์ธ ์‹œ๋งŒ ์ œ๊ณต๋จ
        print("์ด๋ฉ”์ผ: \(email)")

        // privaterelay@icloud.com ํ˜•ํƒœ๋ฉด ํ”„๋ก์‹œ ์ด๋ฉ”์ผ
        if email.contains("privaterelay") {
            print("์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ์„ ์ˆจ๊น€")
        }

        // โš ๏ธ Important: ์ฒซ ๋กœ๊ทธ์ธ ์‹œ๋งŒ ์ œ๊ณต๋˜๋ฏ€๋กœ ๋ฐ˜๋“œ์‹œ ์ €์žฅ!
        UserDefaults.standard.set(email, forKey: "userEmail")
    }
}

๐Ÿ”‘ Backend Token Verification

BackendVerification.swift
// 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 === ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ
  }
});
*/

๐Ÿ“ฑ Upgrading Existing Accounts

AccountUpgrade.swift
import AuthenticationServices

// existing ์ด๋ฉ”์ผ/Password ๋กœ๊ทธ์ธ์„ Apple ID๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ
func upgradeToAppleID(existingEmail: String) {
    let provider = ASAuthorizationAppleIDProvider()
    let request = provider.createRequest()
    request.requestedScopes = [.fullName]

    // existing ๊ณ„์ • ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ํžŒํŠธ
    request.user = existingEmail

    let controller = ASAuthorizationController(authorizationRequests: [request])
    // ... delegate ์„ค์ •
    controller.performRequests()
}

๐Ÿšจ Logout & Account Deletion

Revocation.swift
// ๋กœ๊ทธ์•„์›ƒ
func signOut() {
    UserDefaults.standard.removeObject(forKey: "appleUserID")
    UserDefaults.standard.removeObject(forKey: "userEmail")
    // UI๋ฅผ ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ
}

// ๊ณ„์ • ์‚ญ์ œ (๋ฐฑ์—”๋“œ์—์„œ Apple API ํ˜ธ์ถœ ํ•„์š”)
/*
์‚ฌ์šฉ์ž๊ฐ€ "์„ค์ • > Apple ID > ์•”ํ˜ธ ๋ฐ ๋ณด์•ˆ > Apple๋กœ ๋กœ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜๋Š” ์•ฑ"์—์„œ
์•ฑ์„ ์ œ๊ฑฐํ•˜๋ฉด ๋กœ๊ทธ์ธ์ด ์ทจ์†Œ happens.

๋ฐฑ์—”๋“œ์—์„œ๋Š” Apple์˜ Token Revocation API๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:
POST https://appleid.apple.com/auth/revoke

์ดํ›„ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐDeleteํ•˜๊ณ  ๊ณ„์ •์„ ๋น„ํ™œ์„ฑํ™” does.
*/

โš™๏ธ App Settings (Capabilities)

Xcode ์„ค์ •
1. Xcode์—์„œ ํ”„๋กœ์ ํŠธ ์„ ํƒ
2. Signing & Capabilities ํƒญ
3. "+ Capability" ํด๋ฆญ
4. "Sign in with Apple" ์ถ”๊ฐ€

5. Apple Developer ์ฝ˜์†”์—์„œ:
   - Identifier์— "Sign in with Apple" ํ™œ์„ฑํ™”
   - Services ID ์ƒ์„ฑ (์›น ๋กœ๊ทธ์ธ์šฉ, ์„ ํƒ)
   - Key ์ƒ์„ฑ (๋ฐฑ์—”๋“œ ๊ฒ€์ฆ์šฉ)

โœ… Requirements (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
โœ… other ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ๊ณผ ๋™์ผํ•œ ํฌ๊ธฐ/์œ„์น˜
โœ… Email/name is only provided on first login (must save)
โœ… ํ† ํฐ์€ ๋ฐฑ์—”๋“œ์—์„œ ๊ฒ€์ฆ
โœ… On app launch credential ์ƒํƒœ ์ฒดํฌ

๐Ÿ“ฆ Learning Resources

๐Ÿ’ป
GitHub Project
๐ŸŽ
Apple HIG Docs
๐Ÿ“–
Apple Official Docs

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions