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()
}
}
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()