Using Firebase Auth claims with Swift, Kotlin and JavaScript

Firebase Authentication supports built-in and custom user claims that allow us to conditionally expose functionality to users based on authorization groups. This post demonstrates the syntax to add custom claims in a Firebase backend, and read claims from mobile apps.

What are Claims?

In identity systems, a "Claim" is is a piece of metadata about a user's identity.  They are a lot like a "Tag", in that claims are usually an array of strings attached to a user's account.  What they mean is up to implementation--they could signal anything.

Usually, though, a claim is used to authorize a user to some specific functionality within the application.  For example, a user claim could be "administrator", and an application could check if a user has the "administrator" claim before allowing that user to access an admin section of a navigation system.

Firebase Claims

Like many identity systems, Firebase has some claims built-in, and allows application developers to add custom claims to the user's identity (i.e. profile). Firebase has a limit of 1000 bytes for custom claim storage, so custom claims isn't intended as a repository for a full user profile -- Firebase Cloud Firestore is a better place to store extensive user metadata.

Claims can be added via the Firebase admin SDK, and can be read by any of the client SDKs. Below are code examples for Swift, Kotlin and JavaScript.

Adding a Custom Claim

How can you add a custom claim that applications or cloud functions can read?  I typically use a Firebase cloud function, which encapsulates this security operation on the backend side, allowing the function to use the Firebase Admin SDK.

// Call this function POST function with payload { email: 'user@company.com'} while signed in to a Firebase Auth account
exports.addSysAdmin = functions.https.onCall(async (data, context) => {
  // Only current administrators can create new administrators
  if (context.auth.token.administrator == false) {
    throw new functions.https.HttpsError('permission-denied', 'Must be an admin to use this endpoint')
  }

  const email = data.email;
  const user = await admin.auth().getUserByEmail(email)

  if (user == null) {
    throw new functions.https.HttpsError('invalid-argument', `No user associated with email ${email}`);
  }

  const customClaims = user.customClaims || {}
  customClaims.administrator = true
  admin.auth().setCustomUserClaims(user.uid, customClaims)

  return {
    success: true,
    claims: customClaims,
    message: null
  } 
})

Using Custom Claims in Applications

Once custom claims have been added to user accounts, applications can easily read claims to check whether users have access to some set of functionality.  Below are examples in Swift, Kotlin and JavaScript. In these examples we use the claim "administrator" to authorize users to some administrator functionality within an application.

Swift

To fetch a user's claims on the client in Swift, the user must first be authenticated.  Once authenticated the claims can be fetched asynchronously.

// Service function
func isUserAdministrator() async -> Result<Bool, String> {
    // Check if the user is authenticated.
    guard let user = Auth.auth().currentUser else {
        return .failure("Not signed in")
    }

    do {
        let tokenResult = try await user.getIDTokenResult()
        let claims = tokenResult.claims
        let isAdmin = claims["administrator"] as? Bool ?? false
        return .success(isAdmin) 
    } catch (let error) {
      return .failure(error.localizedDescription)
    }
}

To call the service function:

switch result {
  let result = await isUserAdministrator()
  
  switch result {
    case .success(let isAdmin):
      print("User is an administrator? \(isAdmin)")
    case .failure(let error):
      print("Error checking claims", error)
  }
}

Kotlin

Kotlin syntax to check the claim is similar:

sealed class Result<out Success, out Failure>
data class Success<out Success>(val value: Success) : Result<Success, Nothing>()
data class Failure<out Failure>(val reason: Failure) : Result<Nothing, Failure>()

suspend fun isUserAdministrator() : Result<Boolean, Exception> {
  return try {
    val currentUser = auth.currentUser ?: throw Exception("Not signed in")

    val tokenResult = currentUser.getIdToken(false).await()
    val claims = tokenResult.claims
    val isAdmin = claims["administrator"] as Boolean? ?: false
    Success(isAdmin)
  } catch (e: Exception) {
    Failure(e)
  }
}

To call the service function:

when (val result = isUserAdministrator()) {
    is Success -> print("User is an administrator? ${result.value}")
    is Failure -> print(result.reason.localizedMessage)
}

JavaScript

To evaluate whether a user calling a Firebase cloud function is an administrator, use the context object. Since this is a backend function, we'll throw an HTTP error if the user isn't authorized.

exports.somePriviligedFunction = functions.https.onCall(async (data, context) => {
  if (context.auth.token.administrator == false) {
    	throw new functions.https.HttpsError('permission-denied', 'Must be an admin to use this endpoint)
  }
  // Continue to execute the functionality protected by the "administrator" claim
}