Introduction
We often use social network login services like Google, Facebook and Twitter to sign in to apps or websites, because it is easy and convenient rather than creating and remembering multiple usernames and passwords, and Sign in with apple is a recent addition to them. I assume you are familiar with it, so I won’t cover what it is, and how it works under the hood. If you don’t know about it you can check these articles from MacRumors and Tech Crunch
This article is about its implementation.
Implement sign in with apple
I have divided implementation in three steps for convenience.
- Step 1: We start by adding “Sign in with apple” button in the view / screen
Import AuthenticationServices in the start of the file and add following code in viewDidLoadoverride func viewDidLoad() { super.viewDidLoad() if #available(iOS 13.0, *) { let btn = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: .black) btn.addTarget(self, action: #selector(handleAppleSignIn(_:)), for: .touchUpInside) view.addSubview(btn) btn.translatesAutoresizingMaskIntoConstraints = false btn.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true btn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true } @objc func handleAppleSignIn(_ sender: ASAuthorizationAppleIDButton) { } }
Let’s understand the above code step by step.
- “import AuthenticationServices” imports all the API related sign in with apple
- In viewDIdLoad we initialize ASAuthorizationAppleIDButton, add target to the button for user’s click.
- Set centerx and centery constraints relative to view’s centerx and centery to set button at the center of the view and add button as a subview of view.
- Lastly, implement a method to handle user clicks.
Notice in code we added OS version check statements( if #available(iOS 13.0, *), @available(iOS 13.0, *)), that is because ASAuthorizationService is only available in iOS 13 so if the app has support below iOS 13 then we have to add these statements wherever we use any API which is part of ASAuthorizationService.
- Step 2: Initialize authorization request, authorization controller and perform authorization request
Add the following code in method handleAppleSignIn which we defined in step 1@objc func handleAppleSignIn(_ sender: ASAuthorizationAppleIDButton) { let appleIDProvider = ASAuthorizationAppleIDProvider() let req = appleIDProvider.createRequest() req.requestedScopes = [.fullName, .email] let authorizationContr = ASAuthorizationController(authorizationRequests: [req]) authorizationContr.delegate = self authorizationContr.presentationContextProvider = self authorizationContr.performRequests() }
The above method do following whenever user clicks apple sign in button
- Initializes appleIdProvider which is a mechanism for generating requests to authenticate users based on their Apple ID.
- Creates a new Apple ID authorization request.
- Sets request’s scope to email and fullName because we want both these values after the user completes sign in process.
- Initializes controller with a request object that manages authorization requests.
- Sets controller’s delegate to self, which informs about the success or failure of an authorization attempt.
- Sets controller’s presentationContextProvider to self, which provides a display context in which the system can present an authorization interface to the user.
- Performs authorization requests.
- Step 3: Implement authorization delegate and context provider methods
In the second step we set authorizationController’s delegate and contextProvider to self, now it’s time to implement methods regarding that, so add the following extension to the view controller.// MARK: - Apple sign-in delegate @available(iOS 13.0, *) extension ViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return self.view.window! } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { print("*** Error in sign in with apple: \(error.localizedDescription) ***") } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { print("email: \(String(describing: credential.email))\n firstName: \(String(describing: credential.fullName?.givenName))\n lastName: \(String(describing: credential.fullName?.familyName))\n, userId: \(String(describing: credential.user))") if let identityTokenData = credential.identityToken, let identityTokenString = String(data: identityTokenData, encoding: .utf8) { print("Identity Token \(identityTokenString)") } } } }
In the third step, we add an extension which confirms to ASAuthorizationControllerDelegate and ASAuthorizationControllerPresentationContextProviding, and implement three methods. Let’s look at those one by one.
- PresentationAnchor :
This method is part of ASAuthorizationControllerPresentationContextProviding and indicates from which window it should present content to the user.
So we returned the window object of the current view. - AuthorizationController didComplete with error :
This method is part of ASAuthorizationControllerDelegate and called by delegate when any error occurs during authorization, so we just print error description in this method. - AuthorizationController didComplete with authorization :
This method is also part of ASAuthorizationControllerDelegate and called by delegate when authorization gets completed successfully, so we printed user information in this method.
The property credential.user is unique userId and identityToken is Json Web Token used to communicate with Apple server. Now just add “sign in with apple” capability in the section “Sign in & Capabilities” of your app’s target, and run your app. And I hope you are getting the user’s information successfully.
(I highly recommend to test this functionality in the device rather than simulator)The important thing to note here is we get the name and email only first time when the user performs sign in with the specific app, after that, we only get unique userId, so either you have to store that information somewhere when user do sign in the first time or you can integrate firebase which is subsequent part of this article.
- PresentationAnchor :
Firebase integration
To integrate firebase you need to have an apple developer account which cost you $99/year, again I have divided the whole process into five steps
- Step 1: Create / Update appId
Head over to the apple developer portal and create or update existing appId with “sign in with apple” capability, download updated provisioning profile, and install it. - Step 2: Register service key
This step is optional if you are integrating firebase with the only iOS app, if you are integrating this also in the web or android then in the apple developer portal generate a new service key with sign in with apple enabled and download this file somewhere because in fourth step we will require this key. - Step 3: Create and configure service id
In the Apple developer portal create a new service id with sign in with apple enabled, then click configure, in configure window select your appId, in domain and subdomain enter “YourFirebase-projectID.firebaseapp.com” and in return url enter
“https://YourFirebase-projectID.firebaseapp.com/_/auth/handler”, for reference see below image - Step 4: Setup firebase portal
- In the firebase portal head over to authentication tab, in authentication go to the Sign in methods tab and inside that enable apple.
- In Services ID enter identifier of serviceId which we created in the third step
- In Apple team ID enter team id from your apple developer account
- In Key ID enter the key id of service key which we generated in the second step
- In Private key enter key from the file which we downloaded in the second step And save.
- Step 5: Add pod and update code related to firebase authentication
Add pod “Firebase/Auth”(I have used version: 6.16.0) to your project, import “FirebaseAuth” and “CryptoKit” Add variable var currentNonce: String?Update method “handleAppleSignIn” with below code@objc func handleAppleSignIn(_ sender: ASAuthorizationAppleIDButton) { let appleIDProvider = ASAuthorizationAppleIDProvider() let req = appleIDProvider.createRequest() req.requestedScopes = [.fullName, .email] currentNonce = randomNonceString() req.nonce = sha256(currentNonce!) let authorizationContr = ASAuthorizationController(authorizationRequests: [req]) authorizationContr.delegate = self authorizationContr.presentationContextProvider = self authorizationContr.performRequests() }
update delegate method “AuthorizationController didComplete with authorization” with below code
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let credential = authorization.credential as? ASAuthorizationAppleIDCredential { performFirebaseAppleLogin(cred: credential) } }
And add the following extension to your view controller
// MARK: - Firebase auth related method(s) @available(iOS 13.0, *) extension LoginVC { func performFirebaseAppleLogin(cred: ASAuthorizationAppleIDCredential) { guard let nonce = currentNonce, let token = cred.identityToken, let tokenStr = String(data: token, encoding: .utf8) else { print("Error in firebase apple login"); return } let firebaseCred = OAuthProvider.credential(withProviderID: "apple.com", idToken: tokenStr, rawNonce: nonce) Auth.auth().signIn(with: firebaseCred) {[weak self] (authRes, error) in guard let weakSelf = self else { return } if let error = error { print(" *** Error in firebase apple login response: \(error.localizedDescription) *** ") } print("email: \(String(describing: authRes?.user.email)), displayName: \(String(describing: authRes?.user.displayName)), userId: \(String(describing: authRes?.user.providerData[0].uid))") // execute first time when user logging if let fName = cred.fullName?.givenName, let lName = cred.fullName?.familyName { let changeRequest = authRes?.user.createProfileChangeRequest() changeRequest?.displayName = "\(fName) \(lName)" changeRequest?.commitChanges(completion: { (error) in if let error = error { print(" *** Error in updating name in firebase: \(error.localizedDescription) *** ") } }) } var fullName = "" if let name = authRes?.user.displayName, !name.isEmpty { fullName = name } else if let fName = cred.fullName?.givenName, let lName = cred.fullName?.familyName { fullName = "\(fName) \(lName)" } guard let appleID = authRes?.user.providerData[0].uid, let email = authRes?.user.email, let cell = weakSelf.getTableViewCell(row: 3, section: 0) as? MFAAppleSocialTC else { print(" *** Not getting user info from Firebase *** ") return } } } func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset: Array = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remainingLength = length while remainingLength > 0 { let randoms: [UInt8] = (0 ..< 16).map { _ in var random: UInt8 = 0 let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random) if errorCode != errSecSuccess { fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)") } return random } randoms.forEach { random in if remainingLength == 0 { return } if random < charset.count { result.append(charset[Int(random)]) remainingLength -= 1 } } } return result } func sha256(_ input: String) -> String { let inputData = Data(input.utf8) let hashedData = SHA256.hash(data: inputData) let hashString = hashedData.compactMap { return String(format: "%02x", $0) }.joined() return hashString } }
At the time of creating authorization request we create nonce using two helper methods which we added in the last extension, then add that nonce to the request and in “authorization didComplete” we call method “performFirebaseAppleLogin” with user credentials.
In the method “performFirebaseAppleLogin ” we create firebase credentials with nonce and identity token, and perform sign in.
Enabling private email relay service
When the user selects the “Hide my email” option while performing signing, Apple generates a proxy email, and we can communicate to the user’s actual email using that proxy email.
To do that head over to Apple developer portal, then go to more tab and configure Sign in with apple for email communication, Add email sources in popup add your domain and subdomains and email address through which you want to communicate with users(You have to setup SMTP server for this functionality).