Firebase Authentication - Sign In with Apple: nonce returns nil

577 views Asked by At

Strange. I'm clearly missing something. I'm setting currentNonce to the nonce I'm creating from the randomNonceString method.

The handleSignInWithAppleCompletion(_:) doesn't fail. It succeeds, but crashes with the fatal error, as I would like it to if I have an Invalid State, i.e. no login request was sent. My nonce is not even instantiated, so my currentNonce, of course, is nil.

Why?

Here's my code:

import SwiftUI
import LocalAuthentication
import FirebaseAuth
import CryptoKit
import _AuthenticationServices_SwiftUI

final class SignInManager: ObservableObject {
    @Published var errorMessage = ""
    
    private var currentNonce: String?
    
    // 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: [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
    }
    
    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
        let inputData = Data(input.utf8)
        let hashedData = SHA256.hash(data: inputData)
        let hashString = hashedData.compactMap {
            String(format: "%02x", $0)
        }.joined()
        
        return hashString
    }
}

extension SignInManager {
    func handleSignInWithAppleRequest(_ request: ASAuthorizationAppleIDRequest) {
        request.requestedScopes = [.fullName, .email]
        let nonce = randomNonceString()
        currentNonce = nonce
        request.nonce = sha256(nonce)
    }
    
    func handleSignInWithAppleCompletion(_ result: Result<ASAuthorization, Error>) {
        if case .failure(let failure) = result {
            errorMessage = failure.localizedDescription
        }
        else if case .success(let success) = result {
            if let appleIDCredential = success.credential as? ASAuthorizationAppleIDCredential {
                guard let nonce = currentNonce else {
                    fatalError("Invalid state: a login callback was received, but no login request was sent.")
                }
                guard let appleIDToken = appleIDCredential.identityToken else {
                    print("Unable to fetdch identify token.")
                    return
                }
                guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
                    print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
                    return
                }
                
                let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                          idToken: idTokenString,
                                                          rawNonce: nonce)
                Task {
                    do {
                        let result = try await Auth.auth().signIn(with: credential)
                        await updateDisplayName(for: result.user, with: appleIDCredential)
                    }
                    catch {
                        print("Error authenticating: \(error.localizedDescription)")
                    }
                }
            }
        }
    }
}

Fatal error: Invalid state: a login callback was received, but no login request was sent.

What did you try and what were you expecting?

• Rechecking my code • Debugging in console • Turning the music up

UPDATE

I attempted to deliberately create a request based on Firebase's documentation:

 func handleSignInWithAppleRequest(_ request: ASAuthorizationAppleIDRequest) {
          let nonce = randomNonceString()
          currentNonce = nonce
          let appleIDProvider = ASAuthorizationAppleIDProvider()
          let request = appleIDProvider.createRequest()
          request.requestedScopes = [.fullName, .email]
          request.nonce = sha256(nonce)
    }

The line let request = appleIDProvider.createRequest() changed nothing. Still working on solutions.

Adding SignInButtonView


import SwiftUI
import FirebaseAuth
import AuthenticationServices

struct SignInButtonView: View {
    @EnvironmentObject var signInManager: SignInManager
    @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        VStack {
            // MARK: - Sign In With Apple
            HStack {
                SignInWithAppleButton { request in
                    SignInManager().handleSignInWithAppleRequest(request)
                } onCompletion: { result in
                    SignInManager().handleSignInWithAppleCompletion(result)
                }
                .signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
                .frame(maxWidth: .infinity, maxHeight: 50)
                .cornerRadius(8)
            }
            .padding()
        }
        .frame(width: 400)
    }
}

struct SignInButtonView_Previews: PreviewProvider {
    static var previews: some View {
        SignInButtonView()
    }
}

2

There are 2 answers

1
somethingsomethingswift On

Well, this took forever.

The solution is obvious in hindsight, but I was following the docs too slavishly. Always learn what the docs are trying to get across about how the software works versus trusting the documentation.

I just needed to initialize the currentNonce

Before: fileprivate var currentNonce: ? After: fileprivate var currentNonce = String()

0
Peter Friese On

The code looks mostly fine (it seems like it is based on the sample app that goes along with the video about Getting started with sign in with Apple using Firebase Authentication on Apple platforms I recently published).

However, there is a small, but significant typo: instead of referring to the signInManager property inside the SignInWithAppleButton completion handlers, you instantiate a new SignInManager every time you receive a callback.

This results in currentNonce being nil whenever you receive the second callback.

Change this:

SignInWithAppleButton { request in
  SignInManager().handleSignInWithAppleRequest(request)
} onCompletion: { result in
  SignInManager().handleSignInWithAppleCompletion(result)
}

to this:

SignInWithAppleButton { request in
  signInManager.handleSignInWithAppleRequest(request)
} onCompletion: { result in
  signInManager.handleSignInWithAppleCompletion(result)
}