Firebase Authentication in SwiftUI

Sign in with Apple

Learn how to authenticate users with Sign in with Apple and Firebase Authentication

Marwa Diab
Firebase Developers
6 min readDec 3, 2023

--

Photo by Sebastian Bednarek on Unsplash

In the previous parts of this series, we implemented Firebase Anonymous Authentication and Google Sign-In Authentication in Firebase. This tutorial is based on the source code we built in the previous parts, so if you want to follow along, check out the code from the repository.

Note: You need to be enrolled to the Apple Developer Program to use Sign in with Apple.

Set up Sign in with Apple

First, you need to enable Sign in with Apple in your Firebase project, and then add the Sign in with Apple capability to your SwiftUI project.

  • Enable Apple Sign-In for your Firebase project.
Enable Apple provider in Firebase console.
Enable Apple provider and click Save.

In Xcode, select your project’s root node → TargetsSigning & Capabilities+ Capability and select the Sign in with Apple capability.

Add Sign in with Apple capability in Xcode.
Sign in with Apple capability

Implement Sign in with Apple

  1. Inside AuthManager create a function named appleAuth(appleIDCredential:nonce:) that takes an ASAuthorizationAppleIDCredential and anonce string.
  2. Define AuthCredential using the AppleID identityToken from the given ASAuthorizationAppleIDCredential, with the given un-hashed nonce.
  3. Then call the previously created authenticateUser(credentials:) and pass the credentials we just defined to link or sign in with Firebase.
// 1. 
func appleAuth(
_ appleIDCredential: ASAuthorizationAppleIDCredential,
nonce: String?
) async throws -> AuthDataResult? {
guard let nonce = nonce else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}

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

guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return nil
}

// 2.
let credentials = OAuthProvider.appleCredential(withIDToken: idTokenString,
rawNonce: nonce,
fullName: appleIDCredential.fullName)

do { // 3.
return try await authenticateUser(credentials: credentials)
}
catch {
print("FirebaseAuthError: appleAuth(appleIDCredential:nonce:) failed. \(error)")
throw error
}
}

For every sign-in request, generate a random string — a “nonce” — which you will use to make sure the ID token you get was granted specifically in response to your app’s authentication request. This step is important to prevent replay attacks.
~ Firebase Documentation

Create a new Swift file named AppleSignInManager.swift in the Model folder, and add the following code:

import AuthenticationServices
import CryptoKit

// 1.
class AppleSignInManager {

static let shared = AppleSignInManager()

fileprivate static var currentNonce: String?

static var nonce: String? {
currentNonce ?? nil
}

private init() {}

// TODO: Request Apple Authorization.
}

extension AppleSignInManager {
// 2.
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
var randomBytes = [UInt8](repeating: 0, count: length)
let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
if errorCode != errSecSuccess {
fatalError(
"Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
)
}

let charset: [Character] =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")

let nonce = randomBytes.map { byte in
// Pick a random character from the set, wrapping around if needed.
charset[Int(byte) % charset.count]
}

return String(nonce)
}

// 3.
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
}
}

Here is what happens in this code snippet:

  1. The AppleSignInManger will be responsible for the authorization of the Sign in with Apple flow in your app, as well as for generating the nonce.
  2. Generate a random string — a cryptographically secure “nonce” — which will be used to make sure the ID token was granted specifically in response to your app’s authentication request.
  3. Secure hashing the original nonce with a 256-bit digest, this SHA256 hash value of the nonce will be sent to your sign-in request with Apple.

Next, replace // TODO: Request Apple Authorization. with the following function that takes an ASAuthorizationAppleIDRequest:

func requestAppleAuthorization(_ request: ASAuthorizationAppleIDRequest) {
AppleSignInManager.currentNonce = randomNonceString()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(AppleSignInManager.currentNonce!)
}

Inside LoginView, add the following code right after the body:

func handleAppleID(_ result: Result<ASAuthorization, Error>) {
if case let .success(auth) = result {
guard let appleIDCredentials = auth.credential as? ASAuthorizationAppleIDCredential else {
print("AppleAuthorization failed: AppleID credential not available")
return
}

Task {
do {
let result = try await authManager.appleAuth(
appleIDCredentials,
nonce: AppleSignInManager.nonce
)
if let result = result {
dismiss()
}
} catch {
print("AppleAuthorization failed: \(error)")
// Here you can show error message to user.
}
}
}
else if case let .failure(error) = result {
print("AppleAuthorization failed: \(error)")
// Here you can show error message to user.
}
}

To use Sign in with Apple in your SwiftUI view, replace // TODO: Request Apple Authorization inside theonRequest closure callback with the following code:

AppleSignInManager.shared.requestAppleAuthorization(request)

Then replace // TODO: Handle AppleID Completion inside theonCompletion closure callback with the following code:

handleAppleID(result)

Take it for a spin

Run the app on your phone or on the iOS Simulator. You should now be able to sign in and link with Apple ID.

Sign in with Apple flow on iPhone.
Sign in with Apple ID.
Firebase user with Apple provider.

Note: If you authenticated anonymously and then linked with Apple credentials, Firebase will not create a new user, but will update identifier and providers on the anonymous user.

Check for AppleID Credentials

Before finishing Sign in with Apple, you need to check for AppleID credentials upon app launch, using thecredentialState(forUserID:) function that retrieves the state of the user identifier saved in provider data. If the user granted authorization for the app, then the app continues executing. If the user revoked authorization for the app, or the user’s credential state is not found, the app should sign out and show the LoginView.

Inside AuthManager, add the following code to check for AppleID credentials, and then call verifySignInWithAppleID() in init() right after configureAuthStateChanges():

func verifySignInWithAppleID() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let providerData = Auth.auth().currentUser?.providerData
if let appleProviderData = providerData?.first(where: { $0.providerID == "apple.com" }) {
Task {
let credentialState = try await appleIDProvider.credentialState(forUserID: appleProviderData.uid)
switch credentialState {
case .authorized:
break // The Apple ID credential is valid.
case .revoked, .notFound:
// The Apple ID credential is either revoked or was not found, so show the sign-in UI.
do {
try await self.signOut()
}
catch {
print("FirebaseAuthError: signOut() failed. \(error)")
}
default:
break
}
}
}
}

To test this scenario, sign in with Apple, then go to Settings Your Apple IDSign-In & SecuritySign in with Apple → Your app, and Stop using Apple ID, then re-launch the app.

Your app name might look like this👇🏻

Apps using Apple ID

Next Steps

Now that user can authenticate using Apple, let’s look at how to handle Firebase auth linking errors next.

--

--