Firebase Authentication in SwiftUI

Anonymous Authentication

Implementing guest accounts with Firebase Anonymous Authentication

Marwa Diab
Firebase Developers

--

Single sign-on (SSO) image
Source: What Is Single Sign-On (SSO)?

Most apps now need to securely authenticate user’s identity, and save user’s data in the cloud and provide the same personalized experience across all of the user’s devices (across different platforms too).

Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. It supports authentication using passwords, phone numbers, popular federated identity providers like Google, Facebook and Twitter, and more. ~ Firebase Documentation

In this article you will learn how to implement single sign on using Firebase Authentication through different service providers (Apple, Google, and Anonymous), and link between them.

This tutorial contains 5 parts:

  1. Part 1: Setup and Anonymous Authentication.
  2. Part 2: Google Authentication.
  3. Part 3: Apple Authentication.
  4. Part 4: Handling Link Errors.
  5. Part 5: Deleting User Account & Revoke Access Token.

To get started, download starter project here.

Create a Firebase project, register an iOS app, & add the Firebase SDK

Before we start implementing Firebase authentication in our app, first we need to create a Firebase project, register our iOS app, and add the Firebase SDK to our project and select libraries.

If you do not know how to add Firebase to your SwiftUI project using SPM (Swift Package Manager), follow this tutorial to Add Firebase to SwiftUI Project.

Feel free to disable Google Analytics when creating Firebase project, as it is not essential for this tutorial. Only select the FirebaseAuth library when adding the SDK.

Initialize the Firebase SDK

Open AuthLoginApp or your @main struct and add the following code:

import SwiftUI
import FirebaseCore

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return true
}
}

@main
struct AuthLoginApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

init() {
// Use Firebase library to configure APIs
FirebaseApp.configure()
}

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

(To learn more about this code, check out the previously mentioned tutorial.)

Now that we configured Firebase, let’s implement authentication.

Add Authentication to your app

Create a new Swift file named AuthManager.swift, declare a class named AuthManager class and conform it toObservableObject. This class will be responsible for handling the Firebase authentication functionality in our app.

Add the following code:

import AuthenticationServices
import FirebaseAuth
import FirebaseCore

enum AuthState {
case authenticated // Anonymously authenticated in Firebase.
case signedIn // Authenticated in Firebase using one of service providers, and not anonymous.
case signedOut // Not authenticated in Firebase.
}

@MainActor
class AuthManager: ObservableObject {
@Published var user: User?
@Published var authState = AuthState.signedOut
}

Listen to the app’s authentication state

To listen to your app’s authentication state, use the addStateDidChangeListener(_:) method to attach a listener to the auth object. This listener will be called whenever the user’s sign-in state changes.

In AuthManager class, add the following:

  1. Define AuthStateDidChangeListenerHandle to handle add/remove listener.
  2. Create functions to add (attach) and remove this listener.
    Inside addStateDidChangeListener(_:) callback, make sure to update the @Published properties to reflect the change in your UI.
  3. Start listening to the authentication state upon AuthManager initialization.
  4. Create a function named updateState(user:) . This function will take aUser and runs on the MainActor to update the user and authState properties.
/// 1.
private var authStateHandle: AuthStateDidChangeListenerHandle!

init() {
// 3.
configureAuthStateChanges()
}

// 2.
func configureAuthStateChanges() {
authStateHandle = Auth.auth().addStateDidChangeListener { auth, user in
print("Auth changed: \(user != nil)")
self.updateState(user: user)
}
}

// 2.
func removeAuthStateListener() {
Auth.auth().removeStateDidChangeListener(authStateHandle)
}

// 4.
func updateState(user: User?) {
self.user = user
let isAuthenticatedUser = user != nil
let isAnonymous = user?.isAnonymous ?? false

if isAuthenticatedUser {
self.authState = isAnonymous ? .authenticated : .signedIn
} else {
self.authState = .signedOut
}
}

In order to access AuthManager in your UI, pass it through the SwiftUI environment using the .environmentObject(object:) view modifier. A good place to instantiate the AuthManager is in your app’s main application struct, AuthLoginApp:

@main
struct AuthLoginApp: App {
// 1. Add StateObject authManager.
@StateObject var authManager: AuthManager

init() {
FirebaseApp.configure()

// 2. Initialize authManager.
let authManager = AuthManager()
_authManager = StateObject(wrappedValue: authManager)
}

var body: some Scene {
WindowGroup {
ContentView()
// 3. Pass authManager to enviromentObject.
.environmentObject(authManager)
}
}
}

Make sure to capture the authManager environment object in your views as follows:

@EnvironmentObject var authManager: AuthManager

Start the sign in process

Anonymous authentication

“You can use Firebase Authentication to create and use temporary anonymous accounts to authenticate with Firebase. These temporary anonymous accounts can be used to allow users who haven’t yet signed up to your app to work with data protected by security rules. If an anonymous user decides to sign up to your app, you can link their sign-in credentials to the anonymous account so that they can continue to work with their protected data in future sessions.”
From Firebase documentation ~ Authenticate with Firebase Anonymously on Apple Platforms

It’s nice to have that option if you don’t want to force the user to create an account before they can explore the app (which is recommended for public facing apps).

Open the Firebase Console, navigate to the Authentication section, and under Sign-in methods, enable Anonymous.

Go back to Xcode, and add the following function named signInAnonymously() to the AuthManager class.

func signInAnonymously() async throws -> AuthDataResult? {
do {
let result = try await Auth.auth().signInAnonymously()
print("FirebaseAuthSuccess: Sign in anonymously, UID:(\(String(describing: result.user.uid)))")
return result
}
catch {
print("FirebaseAuthError: failed to sign in anonymously: \(error.localizedDescription)")
throw error
}
}

In LoginView add the following function after the body:

func signAnonymously() {
Task {
do {
let result = try await authManager.signInAnonymously()
}
catch {
print("SignInAnonymouslyError: \(error)")
}
}
}

Replace // TODO: Sign-in Anonymously inside the Skip button action with a call to signAnonymously().

If the user skipped Login (anonymously authenticated), then do not show LoginView upon launch.

In ContentView inside VStack, replace isLoggedIn with authManager.authState != .signedOut, as follows:

if authManager.authState != .signedOut {
HomeView()
} else {
LoginView()
}

Run the code 📲 and tap the Skip button.

  • In Authentication section of Firebase Console, under users tab, you will now find authenticated anonymous user..
Anonymous user record in Firebase Authentication.
Anonymous user in Firebase.

If you run the app again, you will still be signed in with the same anonymous user, as Firebase keeps track of the currently signed in user.

However, keep in mind that if you sign out and then sign in again, a new anonymous user will be created, so keep that in mind when implementing Anonymous Authentication.

Sign out functionality

As mentioned before, temporary anonymous authenticated accounts will allow users who haven’t yet signed up to your app to work with data protected by security rules.

And so sign out should not be available with anonymous authentication, but we should present sign-in to the user.

  • First, add signOut() function in AuthManager.
func signOut() async throws {
if let user = Auth.auth().currentUser {
do {
// TODO: Sign out from signed-in Provider.
try Auth.auth().signOut()
}
catch let error as NSError {
print("FirebaseAuthError: failed to sign out from Firebase, \(error)")
throw error
}
}
}

Show the user’s name and email in HomeView only when user is signed in (i.e. authenticated and not anonymous).

Replace the inner VStack with the following:

VStack(alignment: .leading) {
if authManager.authState == .signedIn {
Text(authManager.user?.displayName ?? "Name placeholder")
.font(.headline)
Text(authManager.user?.email ?? "Email placeholder")
.font(.subheadline)
}
else {
Text("Sign-in to view data!")
.font(.headline)
}
}

then add the following signOut()function after body:

func signOut() {
Task {
do {
try await authManager.signOut()
}
catch {
print("Error: \(error)")
}
}
}

Add the following @State variable, and replace isLoggedIn inside Sign out button with authManager.authState != .signedIn. (We don’t need isLoggedIn anymore).

@State private var showLoginSheet = false

Inside the Logout button action replace // TODO: Log out with the following:

if authManager.authState != .signedIn {
showLoginSheet = true
} else {
signOut()
}

The sign in/out button should look like the following:

// Show `Sign out` iff user is not anonymous,
// otherwise show `Sign-in` to present LoginView() when tapped.
Button {
if authManager.authState != .signedIn {
showLoginSheet = true
} else {
signOut()
}
} label: {
Text(authManager.authState != .signedIn ? "Sign-in" :"Sign out")
.font(.body.bold())
.frame(width: 120, height: 45, alignment: .center)
.foregroundStyle(Color(.loginYellow))
.background(Color(.loginBlue))
.cornerRadius(10)
}

Lastly, add the following at the end of VStack:

.sheet(isPresented: $showLoginSheet) {
LoginView()
}

We don’t want to show the skip button when the user is already anonymously authenticated. So, in LoginView, wrap theskip button with the following condition:

if authManager.authState == .signedOut {}

Now, if you run the code 📲 you will see the sign-in option visible to the user, and when you tap on Sign-in it will present LoginView, but without showing the skip button.

Anonymous authentication flow.
Sign-in anonymously.

Link sign-in credentials to the anonymous account

When an anonymous user signs up to your app, you might want to allow them to continue their work with their new account — for example, you might want to make the items the user added to their shopping cart before they signed up available in their new account’s shopping cart.
~ Convert an anonymous account to a permanent account

Before we continue to any service provider authentication, I want to add generic methods in AuthManager that will be used with any service provider (except Email of course).

There are two ways to authenticate a user

  1. signIn(with:) on auth object, to authenticate user for the first time, or after the user signed out.
  2. link(with:) to link an already authenticated user with new credentials.

You can allow users to sign in to your app using multiple authentication providers by linking auth provider credentials to an existing user account. Users are identifiable by the same Firebase user ID regardless of the authentication provider they used to sign in.
~ Link Multiple Auth Providers to an Account on Apple Platforms

Let’s create these three generic functions in AuthManager:

  1. authenticateUser(credentials:) takes AuthCredential, and checks if we have an authenticated user. If so, it will call authLink(credentials:), otherwise, it will call authSignIn(credentials:).
  2. authSignIn(credentials:) takes anAuthCredential, and authenticates a user using the given credentials.
  3. authLink(credentials:) takes an AuthCredential, and links the given credentials to the currently authenticated user.

We will keep editing these methods along the way through this tutorial.

// 1. 
private func authenticateUser(credentials: AuthCredential) async throws -> AuthDataResult? {
if Auth.auth().currentUser != nil {
return try await authLink(credentials: credentials)
} else {
return try await authSignIn(credentials: credentials)
}
}

// 2.
private func authSignIn(credentials: AuthCredential) async throws -> AuthDataResult? {
do {
let result = try await Auth.auth().signIn(with: credentials)
updateState(user: result.user)
return result
}
catch {
print("FirebaseAuthError: signIn(with:) failed. \(error)")
throw error
}
}

// 3.
private func authLink(credentials: AuthCredential) async throws -> AuthDataResult? {
do {
guard let user = Auth.auth().currentUser else { return nil }
let result = try await user.link(with: credentials)
// TODO: Update user's displayName
updateState(user: result.user)
return result
}
catch {
print("FirebaseAuthError: link(with:) failed, \(error)")
throw error
}
}

One more step

When linking provider credentials to an existing user account, the user won’t be updated with provider’s displayName, and the reason for that is explained in the link below:

So to update user’s displayName, we will get the displayName from providerData and update user’s profile after linking.

Create the following updateDisplayName(for:) function:

  1. Check if currentUser contains displayName, and that it is not empty, we don’t want to overwrite it if it already contains displayName.
  2. Get displayName from providerData.
  3. Call createProfileChangeRequest() on user object, to creates UserProfileChangeRequest object which used to change user’s profile data, then set displayName.
  4. Finally, commit changes.
private func updateDisplayName(for user: User) async {
// 1.
if let currentDisplayName = Auth.auth().currentUser?.displayName, !currentDisplayName.isEmpty {
// current user is non-empty, don't overwrite it
} else {
// 2.
let displayName = user.providerData.first?.displayName
// 3.
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = displayName
do {
// 4.
try await changeRequest.commitChanges()
}
catch {
print("FirebaseAuthError: Failed to update the user's displayName. \(error.localizedDescription)")
}
}
}

Replace // TODO: Update user’s displayName in authLink(credentials:) with await updateDisplayName(for: result.user).

Next Steps

Now that Firebase Authentication is set up, and users can authenticate anonymously, let’s add Google Sign-In next.

--

--