Apple Sign-In Authentication

Implementation guide for our secure Apple Sign-In authentication system

Overview

Our RentManager API uses Apple Sign-In for secure, privacy-focused authentication. This implementation provides users with a seamless login experience while protecting their privacy with Apple's private relay email system.

Security Benefits

  • • Two-factor authentication built-in
  • • No password management required
  • • Apple's fraud detection
  • • Private relay email protection

User Experience

  • • One-tap sign in
  • • Face ID / Touch ID support
  • • Cross-device synchronization
  • • Seamless iOS integration

Implementation Note

Apple Sign-In is our primary authentication method, providing the best user experience and security for our iOS application.

Apple Developer Setup

Our Apple Sign-In configuration in the Apple Developer portal. This setup is already completed for our application.

1 Create App ID

Create an App ID in the Apple Developer portal with Sign In with Apple capability enabled.

  1. 1. Go to Apple Developer Console
  2. 2. Navigate to Certificates, Identifiers & ProfilesIdentifiers
  3. 3. Click the + button to create a new identifier
  4. 4. Select App IDs and continue
  5. 5. Enter your Bundle ID (e.g., com.yourcompany.rentmanager)
  6. 6. Enable Sign In with Apple capability
  7. 7. Register the App ID

2 Create Services ID (Optional)

Create a Services ID for web-based authentication callbacks (optional for iOS-only apps).

Service ID: com.yourcompany.rentmanager.service
Domain: rentmanager.io
Callback URL: https://rentmanager.io/auth/apple/callback

3 Create Key for Sign In with Apple

Generate a private key for server-to-server authentication with Apple.

  1. 1. Go to Keys section in Apple Developer Console
  2. 2. Click + to create a new key
  3. 3. Enter a key name (e.g., "RentManager Apple Sign-In Key")
  4. 4. Enable Sign In with Apple
  5. 5. Click Continue and Register
  6. 6. Download the .p8 key file (you can only download it once!)
  7. 7. Note the Key ID displayed in the console

Important

Store the .p8 key file securely. You can only download it once, and it's required for your backend authentication.

4 Get Your Team ID

Your Team ID is required for backend authentication.

Go to Membership in Apple Developer Console and copy your Team ID.

iOS Implementation

Implementation guide for Apple Sign-In in our iOS app using the AuthenticationServices framework.

1. Import AuthenticationServices

import AuthenticationServices
import UIKit

2. Create Sign In with Apple Button

class LoginViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupAppleSignInButton()
    }
    
    private func setupAppleSignInButton() {
        let appleSignInButton = ASAuthorizationAppleIDButton(
            authorizationButtonType: .signIn,
            authorizationButtonStyle: .black
        )
        
        appleSignInButton.translatesAutoresizingMaskIntoConstraints = false
        appleSignInButton.addTarget(
            self,
            action: #selector(handleAppleSignIn),
            for: .touchUpInside
        )
        
        view.addSubview(appleSignInButton)
        
        // Add constraints
        NSLayoutConstraint.activate([
            appleSignInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            appleSignInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            appleSignInButton.widthAnchor.constraint(equalToConstant: 280),
            appleSignInButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

3. Handle Sign-In Request

extension LoginViewController {
    
    @objc private func handleAppleSignIn() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
}

// MARK: - ASAuthorizationControllerDelegate
extension LoginViewController: ASAuthorizationControllerDelegate {
    
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            handleSuccessfulSignIn(credential: appleIDCredential)
        }
    }
    
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        print("Apple Sign-In failed: \(error.localizedDescription)")
        // Handle error appropriately
    }
}

// MARK: - ASAuthorizationControllerPresentationContextProviding
extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return view.window!
    }
}

4. Process Apple ID Credentials

private func handleSuccessfulSignIn(credential: ASAuthorizationAppleIDCredential) {
    guard let identityToken = credential.identityToken,
          let identityTokenString = String(data: identityToken, encoding: .utf8) else {
        print("Failed to get identity token")
        return
    }
    
    // Prepare user data for API call
    var userData: [String: Any] = [:]
    
    if let fullName = credential.fullName {
        userData["name"] = [
            "firstName": fullName.givenName ?? "",
            "lastName": fullName.familyName ?? ""
        ]
    }
    
    // Send to your backend
    authenticateWithBackend(
        identityToken: identityTokenString,
        user: userData
    )
}

private func authenticateWithBackend(identityToken: String, user: [String: Any]) {
    let url = URL(string: "https://rentmanager.io/api/auth/apple")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let requestBody: [String: Any] = [
        "identityToken": identityToken,
        "user": user
    ]
    
    do {
        request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                self.handleAuthenticationResponse(data: data, response: response, error: error)
            }
        }.resume()
        
    } catch {
        print("Failed to serialize request body: \(error)")
    }
}

API Integration

Handle the authentication response from our backend API.

Handle Authentication Response

private func handleAuthenticationResponse(data: Data?, response: URLResponse?, error: Error?) {
    if let error = error {
        print("Network error: \(error.localizedDescription)")
        showErrorAlert(message: "Network error occurred")
        return
    }
    
    guard let data = data else {
        print("No data received")
        showErrorAlert(message: "No response from server")
        return
    }
    
    do {
        let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
        
        if authResponse.success {
            // Store tokens securely
            TokenManager.shared.saveTokens(
                accessToken: authResponse.token,
                refreshToken: authResponse.refreshToken
            )
            
            // Store user information
            UserManager.shared.saveUser(authResponse.user)
            
            // Navigate to main app
            navigateToMainApp()
            
        } else {
            showErrorAlert(message: "Authentication failed")
        }
        
    } catch {
        print("Failed to decode response: \(error)")
        showErrorAlert(message: "Invalid response from server")
    }
}

// MARK: - Data Models
struct AuthResponse: Codable {
    let success: Bool
    let token: String
    let refreshToken: String
    let isNewUser: Bool
    let user: User
}

struct User: Codable {
    let id: Int
    let email: String
    let fullName: String
    let role: String
    let accessLevel: String
    
    enum CodingKeys: String, CodingKey {
        case id, email, role
        case fullName = "full_name"
        case accessLevel = "access_level"
    }
}

Error Handling

private func showErrorAlert(message: String) {
    let alert = UIAlertController(
        title: "Authentication Error",
        message: message,
        preferredStyle: .alert
    )
    
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}

private func navigateToMainApp() {
    // Navigate to your main app interface
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let mainViewController = storyboard.instantiateViewController(withIdentifier: "MainTabBarController")
    
    if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
       let window = windowScene.windows.first {
        window.rootViewController = mainViewController
        window.makeKeyAndVisible()
    }
}

JWT Token Handling

JWT token management implementation for secure API communication in our application.

Secure Token Storage

import Security

class TokenManager {
    static let shared = TokenManager()
    private init() {}
    
    private let accessTokenKey = "RentManager_AccessToken"
    private let refreshTokenKey = "RentManager_RefreshToken"
    
    func saveTokens(accessToken: String, refreshToken: String) {
        saveToKeychain(key: accessTokenKey, value: accessToken)
        saveToKeychain(key: refreshTokenKey, value: refreshToken)
    }
    
    func getAccessToken() -> String? {
        return getFromKeychain(key: accessTokenKey)
    }
    
    func getRefreshToken() -> String? {
        return getFromKeychain(key: refreshTokenKey)
    }
    
    func clearTokens() {
        deleteFromKeychain(key: accessTokenKey)
        deleteFromKeychain(key: refreshTokenKey)
    }
    
    private func saveToKeychain(key: String, value: String) {
        let data = value.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }
    
    private func getFromKeychain(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
        
        if status == errSecSuccess {
            if let data = dataTypeRef as? Data {
                return String(data: data, encoding: .utf8)
            }
        }
        
        return nil
    }
    
    private func deleteFromKeychain(key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        
        SecItemDelete(query as CFDictionary)
    }
}

API Client with Automatic Token Refresh

class APIClient {
    static let shared = APIClient()
    private let baseURL = "https://rentmanager.io/api"
    
    private init() {}
    
    func makeAuthenticatedRequest(
        endpoint: String,
        method: HTTPMethod = .GET,
        body: [String: Any]? = nil,
        responseType: T.Type,
        completion: @escaping (Result) -> Void
    ) {
        guard let url = URL(string: baseURL + endpoint) else {
            completion(.failure(APIError.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        // Add authorization header
        if let token = TokenManager.shared.getAccessToken() {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        
        // Add request body if provided
        if let body = body {
            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: body)
            } catch {
                completion(.failure(error))
                return
            }
        }
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            if let httpResponse = response as? HTTPURLResponse {
                if httpResponse.statusCode == 401 {
                    // Token expired, try to refresh
                    self.refreshTokenAndRetry(
                        originalRequest: request,
                        responseType: responseType,
                        completion: completion
                    )
                    return
                }
            }
            
            guard let data = data else {
                completion(.failure(APIError.noData))
                return
            }
            
            do {
                let decodedResponse = try JSONDecoder().decode(responseType, from: data)
                completion(.success(decodedResponse))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
    
    private func refreshTokenAndRetry(
        originalRequest: URLRequest,
        responseType: T.Type,
        completion: @escaping (Result) -> Void
    ) {
        guard let refreshToken = TokenManager.shared.getRefreshToken() else {
            completion(.failure(APIError.noRefreshToken))
            return
        }
        
        let refreshURL = URL(string: baseURL + "/auth/refresh")!
        var refreshRequest = URLRequest(url: refreshURL)
        refreshRequest.httpMethod = "POST"
        refreshRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let refreshBody = ["refreshToken": refreshToken]
        refreshRequest.httpBody = try? JSONSerialization.data(withJSONObject: refreshBody)
        
        URLSession.shared.dataTask(with: refreshRequest) { data, response, error in
            guard let data = data,
                  let refreshResponse = try? JSONDecoder().decode(RefreshResponse.self, from: data) else {
                completion(.failure(APIError.tokenRefreshFailed))
                return
            }
            
            // Save new token
            TokenManager.shared.saveTokens(
                accessToken: refreshResponse.token,
                refreshToken: refreshToken
            )
            
            // Retry original request with new token
            var retryRequest = originalRequest
            retryRequest.setValue("Bearer \(refreshResponse.token)", forHTTPHeaderField: "Authorization")
            
            URLSession.shared.dataTask(with: retryRequest) { data, response, error in
                // Handle response same as before
                if let error = error {
                    completion(.failure(error))
                    return
                }
                
                guard let data = data else {
                    completion(.failure(APIError.noData))
                    return
                }
                
                do {
                    let decodedResponse = try JSONDecoder().decode(responseType, from: data)
                    completion(.success(decodedResponse))
                } catch {
                    completion(.failure(error))
                }
            }.resume()
        }.resume()
    }
}

enum HTTPMethod: String {
    case GET = "GET"
    case POST = "POST"
    case PUT = "PUT"
    case DELETE = "DELETE"
}

enum APIError: Error {
    case invalidURL
    case noData
    case noRefreshToken
    case tokenRefreshFailed
}

struct RefreshResponse: Codable {
    let token: String
}

Security Best Practices

Do's

  • Store tokens in iOS Keychain
  • Validate identity tokens on your backend
  • Implement automatic token refresh
  • Handle token expiration gracefully
  • Use HTTPS for all API communications
  • Clear tokens on app logout

Don'ts

  • Store tokens in UserDefaults
  • Log sensitive token information
  • Trust client-side token validation only
  • Ignore SSL certificate errors
  • Cache sensitive user data insecurely
  • Hardcode API credentials in your app

Troubleshooting

Identity Token Validation Failed

Cause: Invalid Apple key ID or client ID mismatch

Solution: Verify your APPLE_CLIENT_ID matches your iOS app bundle ID and APPLE_KEY_ID matches the key in Apple Developer Console

Private Key Format Error

Cause: Incorrect private key format in environment variables

Solution: Ensure private key includes proper line breaks: \n

Sign-In Button Not Appearing

Cause: Sign In with Apple capability not enabled or incorrect bundle ID

Solution: Check App ID configuration in Apple Developer Console and ensure capability is enabled

User Name Not Available

Cause: User name is only provided on first sign-in

Solution: Store user name on first sign-in and handle subsequent sign-ins without name data