Protecting Apps and APIs: A Deep Dive into Firebase App Check and Play Integrity at Chingari

Maxim Petlyuk
Firebase Developers
4 min readJun 5, 2023

Let’s deep dive into how to integrate an Android app with Firebase App Check to protect your backend from unknown and malicious sources.

Nowadays many apps offer benefits that can significantly enhance the user experience and help businesses promote their products more effectively.

Simply you are giving some virtual money or unlocking some paid features for doing some activity inside your app.

You have to ensure that the requests to your app’s backend resources (e.g. Realtime Database or Cloud Storage) originate from your authentic app, and not from some unknown or malicious source.

Notice: we assume that you have already some knowledge about what is App Check, there are a lot of articles to give you some understanding. Here we want to look into it through the prism of the Android app, so if you don’t know what is App Check — we suggest you read first:

Firebase App Check: Play Integrity

Play Integrity is an attestation provider for the Android platform. How does it work in simple words?

  1. The device gets an attestation token from Play Integrity;
  2. Then exchange the attestation token with the Firebase SDK to get an App Check token;
  3. Now the application can append that token to headers for backend requests;
  4. Backend service is getting verdict for the client token — again through the Firebase SDK on the backend side;

Sounds simple, correct?

But what if you have:

  • A big code base;
  • Hundreds of network requests;
  • You want to build a flexible solution to send tokens only for some specific API requests, and not for all;

Plan for Integration & Requirements

1. Synchronized firebase function call

Android Firebase App Check Play Integrity SDK has a quota limit for ordering tokens. It also supports a time cache for tokens under the hood. If there is no token in the cache — the SDK does a network request to the Firebase backend service, which is equivalent to 1 quota unit.

We found that if we execute fetch token from multiple threads in parallel it will cause multiplied quota usage. We don’t want to rely on SDK code and we want to be sure that our code for fetching tokens is thread-safe code.

2. Network interceptor to put the token into headers

Imagine that your code base is extremely big. You don’t want to copy-paste boilerplate code and you are going to attach tokens into headers for every request. Most Android applications use OkHttp library for network communication. We found that we can reuse the OkHttp Interceptor pattern to attach tokens on the fly.

3. Send App Check Token only for specific API requests

We discussed that Firebase App Check has a quota usage limit. Assume that you have hundred of requests inside the app and you don’t want to protect all of them. You want to have a flexible way to choose which request to choose for “signing”.

Implementation

  1. Look into the code of Android Firebase App Check SDK. The logic is simple: if a token cache is available and you are not doing forceRefresh — get it from the cache; otherwise — make a network request.

If we wrap the function call with a simple synchronized block — we will prevent concurrency from executing multiple unnecessary network requests into the Firebase backend service.

class FirebaseAppCheckTokenExecutor(
private val strategy: FirebaseRequestTokenStrategy
) : AppCheckTokenExecutor {

private val firebaseAppCheck = FirebaseAppCheck.getInstance()
private val mutex = Mutex()

override suspend fun getToken(): Result<String> {
return mutex.withLock {
executeTokenRequest(getAppCheckTokenTask(strategy))
}
}

private fun getAppCheckTokenTask(strategy: FirebaseRequestTokenStrategy): Task<AppCheckToken> {
return when (strategy) {
is FirebaseRequestTokenStrategy.Basic -> {
firebaseAppCheck.getAppCheckToken(strategy.refresh)
}

is FirebaseRequestTokenStrategy.Limited -> {
firebaseAppCheck.limitedUseAppCheckToken
}
}
}

private suspend fun executeTokenRequest(tokenTask: Task<AppCheckToken>): Result<String> {
return suspendCancellableCoroutine { continuation ->
tokenTask.addOnSuccessListener { appCheckResult ->
val token = appCheckResult.token

if (!continuation.isCancelled) {
continuation.resume(Result.success(token))
}
}.addOnFailureListener { exception ->
if (!continuation.isCancelled) {
continuation.resume(Result.failure(exception))
}
}
}
}
}

2. Create an OkHttp Interceptor to put token into headers for every request

class FirebaseAppCheckInterceptor(
private val appCheckTokenExecutor: AppCheckTokenExecutor
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val decorateRequest = chain.request()
.newBuilder()

val appCheckTokenResult = runBlocking {
appCheckTokenExecutor.getToken()
}

val appCheckToken = appCheckTokenResult.getOrNull() ?: ""

decorateRequest.addHeader("X-Firebase-AppCheck", appCheckToken)

return chain.proceed(decorateRequest.build())
}
}

3. Put token only for specific API requests

  • We decided to mock the header “AppCheckToken: true”, which will be a marker for requests which are required an App Check token. This example uses retrofit service:
interface ChatApiService {

@GET(Api.Url.GET_CHATS)
@Headers("AppCheckToken: true")
suspend fun getChats(@QueryMap params: Map<String, @JvmSuppressWildcards Any>): Response<ApiChatPage>

}
  • Then our interceptor will extract our mock header. If it exists, we have to put a token, otherwise we can proceed with the request without changes:
class FirebaseAppCheckInterceptor(
private val appCheckTokenExecutor: AppCheckTokenExecutor
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

val appCheckTokenHeader = request.header("AppCheckToken")
val requiresAppCheckToken = appCheckTokenHeader != null && appCheckTokenHeader.isNotEmpty()

if (!requiresAppCheckToken) {
return chain.proceed(request)
}

val appCheckTokenResult = runBlocking {
appCheckTokenExecutor.getToken()
}

val appCheckToken = appCheckTokenResult.getOrNull() ?: ""

val decoratedRequest = request.newBuilder()
.addHeader("X-Firebase-AppCheck", appCheckToken)

return chain.proceed(decoratedRequest.build())
}
}

Conclusion

After evaluating the features and benefits of Firebase App Check, it can be concluded that it is a reliable and effective tool for enhancing the security of applications.

This was our experience integrating App Check into a huge code base with minimal effort.

You can also check GitHub repository to get code samples: https://github.com/maxim-petlyuk/firebase-appcheck-interceptor

--

--