Implementing Firebase App Check on iOS (non-Firebase back-end)

Today we are exploring the security solution based on Apple’s App Attest attestation services via Firebase’s App Check framework. Let’s make it simple.

Liudas Baronas
12 min readJan 24, 2024
Image from Apple

Why we need it?

App Attest is part of Apple’s DeviceCheck services, aiming to help combat fraudulent use of services by verifying that requests to a server are coming from a legitimate mobile app instance.

Firebase’s App Check works alongside App Attest. Initially, App Attest confirms the authenticity of your app. Following this, App Check steps in to guarantee that all future requests to your backend services come exclusively from legitimate versions of your app. This ensures that only your app has access to the backend server data, maintaining a secure and streamlined communication channel. In short, it:

  • Prevents API Abuse: Helps in preventing unauthorized access to your backend server APIs. In other words, a back-end server not able to verify the token will reject the request. The solution prevents executing requests coming from any other source (a script, a different app, etc.) that does not include a valid App Check token.
  • Reduces Fraud: Useful in scenarios like preventing fake app instances from accessing the resources. Since the attestation process involves checking the app’s bundle identifier and its integrity, a modified (cloned) app with altered code or identifier would fail this verification process.
  • Enhances Security: Adds an additional layer of security for sensitive operations like financial transactions. It is designed to detect and prevent attestation from jailbroken devices. Since jailbreaking can compromise the security features of iOS, including the integrity of the Secure Enclave, App Attest’s checks would typically fail on such devices.

Apple’s App Attest relies on a combination of hardware-based security (Secure Enclave), cryptographic techniques, and software integrity checks to ensure that requests are coming from legitimate instances of your app on genuine, uncompromised Apple devices.

This multi-layered mobile application attestation solution is great for safeguarding against fraud and tampering in app-to-server communications.

Image from Apple
Image from techholding.co

Why Firebase App Check?

Firebase provides a variety of no-code server tools. And one of them is App Check that simplifies the process of implementing this multi-layered security solution.

  • Ease of Implementation: Compared to encryption-based solutions, which require complex setup, configuration, and ongoing management of cryptographic algorithms, Firebase App Check is straightforward and easy to integrate.
  • Seamless Integration with Firebase Services: Firebase App Check offers smooth integration with the Firebase ecosystem, enhancing consistency and unity across Firebase services (like authentication, real-time database, or cloud functions) for those already utilizing Firebase in their application.
  • Broad Platform Compatibility: With SDKs available for a range of platforms including Android, iOS, Flutter, Web, and others, Firebase App Check allows for easy incorporation into various application types.
  • Non-intrusive User Experience: Unlike traditional CAPTCHAs, which require user interaction, Firebase App Check operates in the background, offering protection without disrupting the user experience.
  • Designed for Application Security: Firebase App Check is tailored to safeguard applications from misuse and unauthorized access, presenting a more targeted solution than captcha-based options, which primarily aim to identify bots.
  • Automatic Updates: Regularly updated SDKs ensure your app is protected with the latest security measures without manual intervention. It saves developers time and effort in maintaining the latest security standards, thus ensuring continuous protection against emerging threats.
  • Scalability: As your application grows, Firebase App Check scales with it automatically, providing consistent security regardless of user base size.

Application attestation sounds quite complicated, but as a developer you don’t really have to do much to integrate Firebase App Check into your system stack. It means that a lot of the heavy logic is handled by the Firebase SDKs. All you have to do is configure a few things the Firebase console, and do a bit of coding for the app.

How does it work?

When you integrate Firebase App Check and configure it to use Apple’s App Attest, the app will request an attestation from the device. App Attest, running on the user’s iOS device, generates an attestation. This attestation includes information that proves the app’s integrity and that it’s running on a genuine Apple device.

So the role of Apple’s App Attest is to help in verifying that the device is genuine, uncompromised, and that your app has not been tampered with.

The iOS app then sends this attestation to Firebase as part of the App Check token request. Firebase, under the hood, uses the attestation provided by Apple’s App Attest to verify the legitimacy of the app and the device.

Once Firebase confirms that the attestation is valid, it generates a unique to each app instance App Check token. This token then can be included by your app in subsequent API requests to your back-end server (via X-Firebase-AppCheck header — we’ll discuss that later on).

Lastly, the server verifies the token with each incoming request with the help of Firebase Admin SDK. If it is a verifiable token from a legitimate source, the access to resources is granted, otherwise, a server can block the request.

Image from Apple

Implementing the solution

Now we understand what this is for and how it can be beneficial. Let’s work with the application attestation integration in our iOS app.

⚠️ This article will not describe back-end server integration with Firebase Admin SDK. Only iOS mobile app configuration is provided.

Firebase integration

One prerequisite is to have Firebase project created and Firebase integrated into the app via Swift Package Manager (SPM) or CocoaPods. If you haven’t done so, please follow this tutorial for the setup:

Registering the API

When you create a new Firebase project for an iOS app, a corresponding Google Cloud project is automatically created for you and is linked altogether. This is because Firebase relies on Google Cloud’s infrastructure and services.

When you access the Google Cloud console, navigate to your project, find APIs & Services, then Credentials. Click on existing API key that was auto-created by Firebase.

Do not delete this API key otherwise your whole Firebase project will be inaccessible by the app. It will be quite challenging to re-generate a new one :)

Here we’ll be able to secure our API key by adding restrictions and registering App Check API with the associated API key.

Scroll down to API restrictions section and check the box for Firebase App Check API.

Then save the changes and allow Firebase up to 5 minutes to apply those changes.

Attestation manager

It is time to code the logic. We will write a new independent App Check manager factory that would include logic for setting the app attestation provider and for generating the token (that we would later send to the back-end server).

For all the asynchronous operations, we will be relying on Apple’s Combine framework. Of course, you can use async and await , but we are sticking with Combine for now :)

Start by creating a new file AppCheckManager.swift and add the necessary imports:

import Combine
import Firebase
import FirebaseAppCheck

We will also need to add an enumeration that would help distinguishing the errors:

public enum TokenError: Error {
case error(_ with: String)
}

Next, let’s add our AppCheckManager factory. It will be an instance as a singleton, enabling a shared instance to be used in multiple areas of our app.

public protocol AppCheckManaging: AppCheckProviderFactory { }

public final class AppCheckManager: NSObject, AppCheckManaging {
// MARK: - Variables
public static let shared = AppCheckManager()

// MARK: - Methods
public func createProvider(with app: FirebaseApp) -> AppCheckProvider? {
return AppAttestProvider(app: app)
}

public func generateTokenAppCheck() -> Future<String, TokenError> {
Future<String, TokenError> { promise in
AppCheck.appCheck().token(forcingRefresh: false) { token, error in
if let error {
promise(.failure(.error(error.localizedDescription)))
return
}
guard let tokenref = token else { return }
promise(.success(tokenref.token))
}
}
}

public func setProviderFactory() {
AppCheck.setAppCheckProviderFactory(self)
}
}

Now we will analyse these three methods provided in the code snippet above.

When integrating Firebase’s App Check, you are required to specify which App Check provider to use. In our case, this would be the Firebase’s AppAttestProvider that verifies app integrity using the App Attest API.

First method createProvider(with:) is mandatory by protocol AppCheckProviderFactory . This enables us to set this AppCheckManager factory to be used as an attestation provider factory. More on this later.

We will not have to call this method anywhere in the code. It will be automatically handled by App Check by conforming the factory to AppCheckProviderFactory protocol and by setting our factory object as an app check provider factory with the setProviderFactory() method.

App Check, upon needing an App Check provider for its operations (like generating tokens for verifying app integrity), will invoke this method to obtain an instance of the specified provider.

Moving on.. generateTokenAppCheck() method will provide us with an App Check token that we can include in our requests to backend server for verification purposes. As per documentation, this method should only be used if you need to authorize requests to a non-Firebase backend (this is what our article is about). Requests to Firebase backend are authorized automatically if configured.

Setting forcingRefresh to true will force request Firebase App Check token ignoring the cached token. Otherwise, setting it to false will let App Check framework use cached token if it exists and has not expired yet. In most cases false is preferred. true should only be used if the server explicitly returns an error, indicating a revoked token.

Completion handler includes the app check token if the request succeeds, or an error if the request fails.

As per Firebase’s recommendation, if your non-Firebase backend exposes sensitive or expensive endpoints that have low traffic volume, consider protecting it with Replay Protection. In this case, use the limitedUseToken(completion:)instead to obtain a limited-use token.

Lastly, setProviderFactory() method allows us to set AppCheckManager instance itself as a provider factory for App Check framework.

App Delegate

Our app needs to set up the App Check with its factory on app launch.

Setting an instance of AppCheckManager as an App Check provider factory needs to be performed before configuring Firebase. This configuration is typically done in AppDelegate or equivalent initialization code.

Here we set our custom App Check provider factory using AppCheckManager.shared.setProviderFactory() method. We are essentially telling Firebase App Check to use this factory to create App Check providers.

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
AppCheckManager.shared.setProviderFactory()
FirebaseApp.configure()
return true
}

Capabilities

For our solution to work, we also need to configure the Xcode project so that the SDK can use Apple’s App Attest API to ensure that requests sent come from legitimate instances of our app.

For this we add App Attest capability to the main target.

A file yourApp.entitlements (obviously, with a different name) will appear in the root folder of your Xcode project after performing the previous step.

In the yourApp.entitlements file, change the value for the App Attest Environment key to production.

The reason is that, currently, Firebase App Check only supports the App Attest provider when it is in production mode. By default, when you run your app from Xcode, App Attest is running in sandbox or development mode, so make sure that you complete the steps above to switch the App Attest mode. Otherwise, Firebase App Check will fail to perform the device attestation.

Debug mode

The FirebaseAppCheckDebug provider makes it possible to test applications with Firebase App Check enforcement in untrusted environments, including the iOS Simulator, or debug build configuration during the development process. To make sure this works smoothly, we need to configure the debug provider.

Let’s modify createProvider(with:) method in AppCheckManager factory by including App Check Debug provider for debug builds:

public func createProvider(with app: FirebaseApp) -> AppCheckProvider? {
#if DEBUG
// App Attest is not available on debug builds and/or simulators.
return AppCheckDebugProvider(app: app)
#else
// Use App Attest provider on release builds and/or real devices.
return AppAttestProvider(app: app)
#endif
}

This is needed for logging the local debug secret (token) generated by the debug provider, so we can register this instance of the app in the Firebase console for debugging purposes.

Then set -FIRDebugEnabled as one of the arguments passed on launch.

And run the application. Debug console will output a secret. Copy it. Do not expose it to others.

The debug secret is generated for your simulator/device (e.g. debug configuration) on the first app launch and is stored in the user defaults. If you remove the app, reset the simulator or use another simulator / device, a new debug secret will be generated.

Next, make sure to register the new debug secret via the Firebase console. Go to App Check configuration and select Manage debug tokens.

Paste the token in the field provided and give it a name.

Sending the token

Last step is to send the token with each network request.

Code snippet below displays how to access the token from our AppCheckManager using Combine framework.

AppCheckManager.shared.generateTokenAppCheck()
.sink(
receiveCompletion: { completion in
guard case let .failure(error) = completion else { return }
print(error)
},
receiveValue: { tokenString in
print(tokenString)
// Continue building network request with the token and executing the request.
// See code snippet below
}
)
.store(in: &cancelBag)

And here is the mock network layer solution with token integration into HTTP headers:

let url = URL(string: "https://yourbackend.example.com/path")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
// Include the App Check token with requests to your server
request.setValue(tokenString, forHTTPHeaderField: "X-Firebase-AppCheck")

let task = URLSession.shared.dataTask(with: request) { data, response, error in
// Handle response from your backend
}
task.resume()

The token (as value) must be included in HTTP headers as X-Firebase-AppCheck key.

Congratulations

iOS app attestation is done from mobile app side. But it takes a little bit more to have this security solution implemented in the system.

🎉

🙃 What’s left is to contact your teammate responsible for back-end and ask them (or do it yourself if working alone) to implement Firebase Admin SDK App Check token validation with each incoming request, as described on the following tutorial:

It is so great that we have such tools for preventing unauthorized access and securing the resources against threat actors. I hope this tutorial helped you to understand how such a complex solution can be tackled easily.

Thank you for reading and good luck securing your resources! :)

More about me and my experience here.

--

--