Implementation guide for our secure Apple Sign-In authentication system
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.
Apple Sign-In is our primary authentication method, providing the best user experience and security for our iOS application.
Our Apple Sign-In configuration in the Apple Developer portal. This setup is already completed for our application.
Create an App ID in the Apple Developer portal with Sign In with Apple capability enabled.
com.yourcompany.rentmanager
)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
Generate a private key for server-to-server authentication with Apple.
Store the .p8 key file securely. You can only download it once, and it's required for your backend authentication.
Your Team ID is required for backend authentication.
Go to Membership in Apple Developer Console and copy your Team ID.
Implementation guide for Apple Sign-In in our iOS app using the AuthenticationServices framework.
import AuthenticationServices
import UIKit
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)
])
}
}
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!
}
}
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)")
}
}
Handle the authentication response from our backend API.
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"
}
}
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 management implementation for secure API communication in our application.
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)
}
}
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
}
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
Cause: Incorrect private key format in environment variables
Solution: Ensure private key includes proper line breaks: \n
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
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