Using Sign in with Apple with SwiftUI

Using Sign inn with Apple with SwiftUi, we'll add the final button to our sign in page by adding a Sign in with Apple button. We'll see how to use SwiftUI's UIViewRepresentable protocol to bring UIKit views into SwiftUI. We'll also see how to combine both the Apple Sign In provider with Firebase Auth to give our users a secure way to sign into our app.

Getting Started

Taking a look at our Figma design file, we can see that we have two more views to add to our sign in card. First, we'd like to add a Rectangle view to serve as a divider, and then the Sign In With Apple button. We'll add both of these views inside the if !signupToggle condition.

  • Inside the if condition, underneath the "Reset Password" button, add the Rectangle view we've been using all along.
if !signupToggle {
    Button(action: {
        print("Send reset password email")
    }, label: {
        ...
    })

    Rectangle()
        .frame(height: 1)
        .foregroundColor(.white.opacity(0.1))
}
  • Underneath our divider, we'll add the Sign In With Apple button. Unfortunately, SwiftUI doesn't provide a SignInWithApple view at the time of writing, but not to worry, we can easily fix this. For now, we'll use a simple Button view.
if !signupToggle {
    Button(action: {
        print("Send reset password email")
    }, label: {
        ...
    })

    Rectangle()
        .frame(height: 1)
        .foregroundColor(.white.opacity(0.1))

    Button(action: {
        print("Sign in with apple")
    }, label: {
        Text("Button")
            .frame(height: 50)
            .cornerRadius(16)
    })
}

Screen_Shot_2021-04-24_at_7.18.53_PM

Creating a SignInWithAppleButton View

As mentioned earlier, since SwiftUI doesn't come with a Sign In With Apple button view we can use, we'll need to make our own. We'll need to use the ASAuthorizationAppleIDButton that comes with the AuthenticationServices framework. Underneath the hood, this button is actually a subclass of UIControl from UIKit. In order to bring UIKit elements into SwiftUI, we need to use a protocol known as UIViewRepresentable.

  • Create a new Swift file for the Views folder from the Navigator sidebar and call this SignInWithAppleButton.swift.

Screen_Shot_2021-04-30_at_12.18.55_PM

  • At the top of the file, import the frameworks we'll be using. We'll also create the struct that will represent our ASAuthorizationAppleIDButton view. We'll call this struct SignInWithAppleButton and have it conform to UIViewRepresentable.
import SwiftUI
import AuthenticationServices

struct SignInWithAppleButton: UIViewRepresentable {

}
  • Every struct that conforms to UIViewRepresentable needs to define a typealias. A typealias allows us to provide a new name for an existing data type into our struct. Since we want our SignInWithAppleButton struct to behave like an ASAuthorizationAppleIDButton, this will be our typealias.
struct SignInWithAppleButton: UIViewRepresentable {
    typealias UIViewType = ASAuthorizationAppleIDButton    
}
  • Next, we need to add the two methods every UIViewRepresentable must have. The first method is the UIView to return. SwiftUI will read this and render this UIView as a SwiftUI view. The next method will be an update method to update the UIView. However, in our case, we will not need to update our button. So all I need to do is return an ASAuthorizationAppleIDButton view. As per our design, I'll add the parameters to make it look like the one in our design.
struct SignInWithAppleButton: UIViewRepresentable {
    typealias UIViewType = ASAuthorizationAppleIDButton

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        return ASAuthorizationAppleIDButton(type: .signIn, style: .black)
    }

    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {}
}
  • Finally, we can use this view inside our SignupView. Go back to the Button and remove the Text view we were using as a label and instead use our custom created SignInWithAppleButton view.
Button(action: {
    print("Sign in with apple")
}, label: {
    SignInWithAppleButton()
        .frame(height: 50)
        .cornerRadius(16)
})

Screen_Shot_2021-04-30_at_12.26.35_PM

Making a Request to Apple's Authentication Servers

The next step to get Sign In With Apple working is to make the authentication request to Apple's servers. We can't put all this code inside SignupView.swift so we'll need to create a separate NSObject for this.

Before we do that, we first need to enable the Sign in with Apple capability. In the Navigator, click on the project icon and then go to the Signing and Capabilities tab. Click on the + Capability button, scroll down, and select Sign in with Apple. This will let Apple know that our application with its Bundle ID is eligible to communicate with their authentication servers.

Screen_Shot_2021-04-30_at_12.50.02_PM

Now that we have the capability enabled, we can start by creating our object. Inside the Helpers folder, create a new Swift file and call it SignInWithAppleObject.swift.

  • Start by importing the frameworks we'll use and creating the SignInWithAppleObject class. Here, the new framework is CryptoKit. This framework is used to perform cryptographic operations and gives us access to cryptography APIs.
import AuthenticationServices
import CryptoKit
import FirebaseAuth

class SignInWithAppleObject: NSObject {

}
  • When we call this class from SignupView.swift, we'll need to call a function inside this class. It is inside this function that will trigger the sign in request. So I'll create a new function inside this class called signInWithApple.
class SignInWithAppleObject: NSObject {    
    func signInWithApple() {

    }
}
  • Next, I need to create the request inside this function. This request will include which information we are requesting from a user. We'll be asking for their email and full name.
func signInWithApple() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.email, .fullName]
}
  • Now comes the cryptographic section. We can't just send a bare request to Apple's server. We need to attach a nonce, which is a random and unique string that lets both Apple and our code know if the data we receive is what we requested and has not been tampered with. Most of the code to create this is provided by Apple through Firebase's documentation which you can find at https://firebase.google.com/docs/auth/ios/apple#signinwithappleandauthenticatewith_firebase. Be sure to copy the randomNonceString and sha256 method from below or from the documentation. Paste them underneath our signInWithApple function.
class SignInWithAppleObject: NSObject {    
    func signInWithApple() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.email, .fullName]
    }

    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    private func randomNonceString(length: Int = 32) -> String {
        precondition(length > 0)
        let charset: Array<Character> =
            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
    }

    private 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
    }
}
  • Now that we have these cryptographic operations in place, we can create our nonce using the randomNonceString method. I'd like to access this nonce from even outside the function so I'll create a class-level variable to store our nonce in called currentNonce. Finally, I can attache this nonce to our request variable. To further encrypt the data, I'll use the sha256 method to encrypt the string using an industry standard SHA-256 encryption algorithm.
class SignInWithAppleObject: NSObject {
    private var currentNonce: String?

    func signInWithApple() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.email, .fullName]

        let nonce = randomNonceString()
        currentNonce = nonce
        request.nonce = sha256(currentNonce!)
    }

    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    private func randomNonceString(length: Int = 32) -> String {...}
    private func sha256(_ input: String) -> String {...}
}
  • Now that our request has been created, we can call the ASAuthorizationController. This will be a popup controller that was provided to us in the AuthenticationServices framework. It allows users to authorize whether or not to sync their Apple ID with our application. We'll attach the request to our controller.
func signInWithApple() {
    let request = ASAuthorizationAppleIDProvider().createRequest()
    request.requestedScopes = [.email, .fullName]

    let nonce = randomNonceString()
    currentNonce = nonce
    request.nonce = sha256(currentNonce!)

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

And that's all it takes to create an ASAuthorizationAppleIDRequest. I know this section was a lot of copying code and functions but because we are dealing with login methods, it's important to make sure that everything is encrypted and secure, hence the need to copy industry practiced code.

Explaining the cryptographic operations is way outside the scope of this course but if you're interested, I encourage you to read the Firebase documentation linked above to gain a better understanding of what the two functions we added are doing.

Screen_Shot_2021-04-30_at_1.04.08_PM

Completing the Authorization with Firebase

Earlier in our code, we set the delegate of authorizationController to self. We should be getting an error since we haven't had our SignInWithAppleObject conform to ASAuthorizationControllerDelegate but we'll do that now.

  • At the bottom of the file, be sure to extend SignInWithAppleObject so that it can conform to ASAuthorizationControllerDelegate. We'll also add the didCompleteWithAuthorization method. This is the method that is executing once a user has successfully allowed their Apple ID to be used as a login provider. As you might expect, here we'll link our code to Firebase Auth.
extension SignInWithAppleObject: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        // Sign in using Firebase Auth
    }
}
  • Next, I need to first check if a credential exists as part of the successful authorization object returned. This credential will contain important information that we need to use Firebase Auth's API's so I make sure to check if it is not nil using an if condition and creating a new variable called appleIDCredential.
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    // Sign in using Firebase Auth
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {

    }
}
  • Then, inside this if condition, I'll add three guard statements. These statements check to see if the returned nonce matches the local currentNonce we have. If it does, then that means we got the data we requested. Then we check to see if our credential has an identity token and whether or not this token can be converted into a string. This identity token is a JSON Web Token (JWT) that securely communicates information about the user to the app and is unique for every user.
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    // Sign in using Firebase Auth
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
        guard let nonce = currentNonce else {
            print("Invalid state: A login callback was received, but no login request was sent.")
            return
        }

        guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
        }

        guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data")
            return
        }
    }
}
  • At this point, if all three guard statements have passed, we have all the data needed to sign in with Firebase Auth. I create a new credential variable and set the nonce and token based on the data we received from appleIDCredential. Then, I call Firebase's signIn method, this time signing in with the credential variable instead of an email and password.
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    // Sign in using Firebase Auth
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
        guard let nonce = currentNonce else {...}
        guard let appleIDToken = appleIDCredential.identityToken else {...}
        guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {...}

        let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
        Auth.auth().signIn(with: credential) { result, error in
            guard error == nil else {
                print(error!.localizedDescription)
                return
            }
        }
    }
}
  • Lastly, in order to make sure we can access this class and its methods from SignupView.swift, we need to mark the class and methods with the public keyword.
public class SignInWithAppleObject: NSObject {
    private var currentNonce: String?

    public func signInWithApple() {...}

    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    private func randomNonceString(length: Int = 32) -> String {...}

    private func sha256(_ input: String) -> String {...}
}

extension SignInWithAppleObject: ASAuthorizationControllerDelegate {
    public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {...}
}

And that's all we need. To recap, we created a class called SignInWithAppleObject that creates an ASAuthorizationAppleIDRequest using some cryptographic functions, requesting a user's email and name. Then, once the user has successfully authorized their Apple ID to be used by our app from the ASAuthorizationController, we use the delegate to verify if we have the proper nonce and identityToken. This data is unique to every user and used to check that our data has not been manipulated or modified. If that's the case, we create a credential object and sign into Firebase Auth using this credential.

Screen_Shot_2021-04-30_at_1.53.07_PM

Putting it all together

Now that we have some fairly advanced Swift code in our project, it's time to put it all together by calling this class and its methods from our SignupView.

  • At the top of SignupView.swift, create a new @State variable that is equal to the initialized SignInWithAppleObject.
@State private var signInWithAppleObject = SignInWithAppleObject()
  • Finally, scrolling down to the Button view with out SignInWithAppleButton custom view as a label, remove the print statement as the action and replace it with a call to the signInWithApple method from the above class.
Button(action: {
    signInWithAppleObject.signInWithApple()
}, label: {
    SignInWithAppleButton()
        .frame(height: 50)
        .cornerRadius(16)
})

That's all! Run the app on a physical device (Sign in with Apple does not work on the Preview and Simulator with Xcode 12 for some inexplicable Apple-related bug) and you should see that when we click on the Sign in with Apple button, we're presented with the controller where we can link our Apple ID with our application. If you're running this on a new Simulator and have the fullScreenCover view modifier uncommented, as soon as Firebase Auth logs in with your Apple ID, you can see yourself being navigated over to the ProfileView screen with our new user also appearing in the Firebase Authentication table of users.

Screen_Shot_2021-04-30_at_2.00.56_PM
Screen_Shot_2021-04-30_at_2.02.22_PM
Screen_Shot_2021-04-30_at_2.08.01_PM