Firebase Authentication in Jetpack Compose

Anonymous Authentication

Implementing guest accounts with Firebase Anonymous Authentication

Marwa Diab
11 min readDec 8, 2023

--

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 Google service provider, as well Anonymous Authentication, and link between them.

This tutorial contains 3 parts

  1. Part 1: Setup and Anonymous Authentication.
  2. Part 2: Google Authentication.
  3. Part 3: Handling Link Errors.

To get started, download the starter project here.

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

Before we start implementing Firebase authentication in our app, first we need to create a Firebase project, and register our Android app, then add the Firebase SDK in the gradle file then add the Firebase library dependencies.

If you do not know how to add Firebase to your Jetpack Compose project using BoM (Bill of Materials), follow this tutorial to Add Firebase to Jetpack Compose Project.

Feel free to disable Google Analytics when creating Firebase project, as it is not essential for this tutorial.

Setup Dagger-Hilt

This tutorial uses Dagger-Hilt for the Dependency Injection, if you’re not familiar with Dagger-Hilt, I highly recommend checking out this tutorial:

Or watch this YouTube tutorial video:

In the project-level build.gradle file, add the following dependencies:

buildscript {
dependencies {
// Add the dependency for the Google services Gradle plugin
classpath("com.google.gms:google-services:4.4.0")
classpath("com.google.dagger:hilt-android-gradle-plugin:2.44.2")
}
}

In the app-level build.gradle file, add kapt and dagger-hilt plugins, and hilt library dependencies:

plugins {
// ...

id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}

android {
// ...
}

dependencies {
implementation("com.google.dagger:hilt-android:2.44.2")
kapt("com.google.dagger:hilt-android-compiler:2.44.2")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")

// ...
}

All the apps that use Dagger-Hilt must have an Application class annotated with the @HiltDaggerApp, create a new kotlin file named AuthLoginApp and add the following code:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class AuthLoginApp: Application() {
// Application code
}

Enable the dependency injection by annotating MainActivity with the @AndroidEntryPoint:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// ...
}

In the AndroidManifest.xml, add the application class in the android:name attribute inside the application tag:

<application
android:name=".AuthLoginApp"
<!-- ... -->
</application>

Add the Firebase Auth dependency

Add Firebase Authentication library dependency in the app-level build.gradle file:

// Add the dependency for the Firebase Authentication library
implementation("com.google.firebase:firebase-auth")

Add Authentication to your app

Create a generic sealed class in the model package named Response that defines the three possible states an app process can be in as follows:

typealias FirebaseSignInResponse = Response<AuthResult>
typealias SignOutResponse = Response<Boolean>
typealias AuthStateResponse = StateFlow<FirebaseUser?>

sealed class Response<out T> {
object Loading: Response<Nothing>()
data class Success<out T>(val data: T?): Response<T>()
data class Failure(val e: Exception): Response<Nothing>()
}

Create a new kotlin singleton module class named AuthModule, and add the following code to provide a singleton binding to the FirebaseAuth instance:

This class will be installed at the application level using the SingletonComponent. So this class will be executed first and will continue to exist as long as the application is on.

@Module
@InstallIn(SingletonComponent::class)
class AuthModule {

@Provides
@Singleton
fun provideFirebaseAuth() = FirebaseAuth.getInstance()
}

Create an interface in the repository package and name it AuthRepository. Declare the following functions:

interface AuthRepository {

fun getAuthState(viewModelScope: CoroutineScope): AuthStateResponse

suspend fun signInAnonymously(): FirebaseSignInResponse

suspend fun signOut(): SignOutResponse
}

Create a subclass that implements AuthRepository, named AuthRepositoryImpl, inject a FirebaseAuth instance in the constructor, to access the firebase authentication functions.

class AuthRepositoryImpl @Inject constructor(
private val auth: FirebaseAuth
): AuthRepository {
// TODO: Implement members
}

Click on the error bulb and select Implement members, this will generate all the functions we defined in AuthRepository interface.

Implement members.

Listen to app’s authentication state

To listen to your app’s authentication state, use the addAuthStateListener to attach a listener to the auth object.
This listener will be called whenever the user’s sign in state changes.

Inside AuthManagerImpl class, define the overridden function getAuthState(viewModelScope:) as follows:

  1. Define AuthStateListener that will be called when there is a change in the authentication state.
  2. Register a listener to changes in the user authentication state.
  3. When the viewModelScope is closed, unregister the listener to authentication changes.
  4. Inside AuthStateListener callback, return the current authenticated user result — (the result will be null when the user signs out).
override fun getAuthState(viewModelScope: CoroutineScope) = callbackFlow {
// 1.
val authStateListener = AuthStateListener { auth ->
// 4.
trySend(auth.currentUser)
Log.i(TAG, "User: ${auth.currentUser?.uid ?: "Not authenticated"}")
}
// 2.
auth.addAuthStateListener(authStateListener)
// 3.
awaitClose {
auth.removeAuthStateListener(authStateListener)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), auth.currentUser)

Next add the following code in AuthModule to provide a binding for AuthRepository:

@Provides
fun provideAuthRepository(impl: AuthRepositoryImpl): AuthRepository = impl

Now that we prepared the Repository, let’s create the view model..

Create a new kotlin class named AuthViewModel that inherits ViewModel(), and inject AuthRepository inside the constructor:

  1. Create a private getAuthState() function that calls getAuthState(viewModelScope) on the repository instance.
  2. Start listening to the auth state changes upon AuthViewModel initialization.
  3. Create a public variable currentUser to be accessible and collected from MainActivity.
@HiltViewModel
class AuthViewModel @Inject constructor(
private val repository: AuthRepository
): ViewModel() {
// 3.
val currentUser = getAuthState()

init {
// 2.
getAuthState()
}

// 1.
private fun getAuthState() = repository.getAuthState(viewModelScope)
}

We will redirect the user to the corresponding screen according to the auth state value.

Create a new file in the model package named DataProvider, declare a singleton object DataProvider which will hold the data across the app, add the following code:

  1. anonymousSignInResponse, googleSignInResponse, & signOutResponse will be used to track the auth process responses.
  2. Create updateAuthState(user:) function that takes a FirebaseUser and updates the user and the authState properties.
enum class AuthState {
Authenticated, // Anonymously authenticated in Firebase.
SignedIn, // Authenticated in Firebase using one of service providers, and not anonymous.
SignedOut; // Not authenticated in Firebase.
}

object DataProvider {

// 1.
var anonymousSignInResponse by mutableStateOf<FirebaseSignInResponse>(Success(null))
var googleSignInResponse by mutableStateOf<FirebaseSignInResponse>(Success(null))
var signOutResponse by mutableStateOf<SignOutResponse>(Success(false))

var user by mutableStateOf<FirebaseUser?>(null)

var isAuthenticated by mutableStateOf(false)

var isAnonymous by mutableStateOf(false)

// 2.
fun updateAuthState(user: FirebaseUser?) {
this.user = user
isAuthenticated = user != null
isAnonymous = user?.isAnonymous ?: false

authState = if (isAuthenticated) {
if (isAnonymous) AuthState.Authenticated else AuthState.SignedIn
} else {
AuthState.SignedOut
}
}
}

Next, in MainActivity, add the following code:

  1. Create a AuthViewModel instance.
  2. Create a currentUser of type AuthStateResponse which is a StateFlow<FirebaseUser?>, so that the value can be collected by calling collectAsState().
  3. Call updateAuthState(user:) to update the user value.
  4. Redirect the user to the corresponding screen according to the DataProvider.isAuthenticated value.

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

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// 1.
private val authViewModel by viewModels<AuthViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AuthLoginTheme {
// 2.
val currentUser = authViewModel.currentUser.collectAsState().value
// 3.
DataProvider.updateCurrentUser(currentUser)
// 4.
if (DataProvider.isAuthenticated) {
HomeScreen()
} else {
LoginScreen()
}
}
}
}
}

Build and run code 📲 .. in the Logcat window, the following message will be logged:

Logcat Window

Start signing-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.
~ Authenticate with Firebase Anonymously on Android

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 Firebase Console, in Authentication section, and under Sign-in methods, enable Anonymous.

Go back to Android Studio, in AuthRepositoryImpl class add the following code in signInAnonymously() overridden function:

override suspend fun signInAnonymously(): FirebaseSignInResponse {
return try {
val authResult = auth.signInAnonymously().await()
authResult?.user?.let { user ->
Log.i(TAG, "FirebaseAuthSuccess: Anonymous UID: ${user.uid}")
}
Response.Success(authResult)
} catch (error: Exception) {
Log.e(TAG, "FirebaseAuthError: Failed to Sign in anonymously")
Response.Failure(error)
}
}

in AuthViewModel add the following function:

fun signInAnonymously() = CoroutineScope(Dispatchers.IO).launch {
DataProvider.anonymousSignInResponse = Response.Loading
DataProvider.anonymousSignInResponse = repository.signInAnonymously()
}

In LoginScreen add the authViewModel, and the loginState parameters, and the anonymousSignInResponse check as follows:
the loginState will be used later when linking accounts.

@Composable
fun LoginScreen(
authViewModel: AuthViewModel,
loginState: MutableState<Boolean>? = null
) {
// Scaffold content...

when (val anonymousResponse = DataProvider.anonymousSignInResponse) {
is Response.Loading -> {
Log.i("Login:AnonymousSignIn", "Loading")
AuthLoginProgressIndicator()
}
is Response.Success -> anonymousResponse.data?.let { authResult ->
Log.i("Login:AnonymousSignIn", "Success: $authResult")
}
is Response.Failure -> {
Log.e("Login:AnonymousSignIn", "${anonymousResponse.e}")
}
}
}

Then replace // TODO: Sign in Anonymously inside the onClick block of the Skip button with the authViewModel.signAnonymously() function call.

Code for the AuthLoginProgressIndicator() composable:

@Composable
fun AuthLoginProgressIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
CircularProgressIndicator(
color = MaterialTheme.colorScheme.tertiary,
strokeWidth = 5.dp
)
}
}

Run the code 📲 and tap on Skip button.

  • In Authentication section of Firebase Console, under users tab, you will find authenticated anonymous user..
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.

In AuthRepositoryImpl class add the following code in signOut() overriden function:

override suspend fun signOut(): SignOutResponse {
return try {
auth.signOut()
Response.Success(true)
}
catch (e: java.lang.Exception) {
Response.Failure(e)
}
}

Then, in AuthViewModel add the following:

fun signOut() = CoroutineScope(Dispatchers.IO).launch {
DataProvider.signOutResponse = Response.Loading
DataProvider.signOutResponse = repository.signOut()
}

in HomeScreen add an AuthViewModel parameter, and add the following properties:

@Composable
fun HomeScreen(
authViewModel: AuthViewModel
) {
val openLoginDialog = remember { mutableStateOf(false) }
val authState = DataProvider.authState
// ...
}

Next, show the user’s name and email in HomeScreen only when the user is signed in (i.e. authenticated and not anonymous).

Replace the Column content with the following:

if (authState == AuthState.SignedIn) {
Text(
DataProvider.user?.displayName ?: "Name Placeholder",
fontWeight = FontWeight.Bold
)
Text(DataProvider.user?.email ?: "Email Placeholder")
}
else {
Text(
"Sign-in to view data!"
)
}

Show the ‘Sign out’ iff user is not anonymous, otherwise show the ‘Sign-in’,
the sign in/out button should look like the following:

Button(
onClick = {
if (authState != AuthState.SignedIn)
openLoginDialog.value = true
else
authViewModel.signOut()
},
modifier = Modifier
.size(width = 200.dp, height = 50.dp)
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiary,
)
) {
Text(
text = if (authState != AuthState.SignedIn) "Sign-in" else "Sign out",
modifier = Modifier.padding(6.dp),
color = MaterialTheme.colorScheme.primary
)
}

Inside Scaffold composable, after the Column, add the following code to present the LoginScreen when the user taps on Sign-in:

AnimatedVisibility(visible = openLoginDialog.value) {
Dialog(
onDismissRequest = { openLoginDialog.value = false },
properties = DialogProperties(
usePlatformDefaultWidth = false
)
) {
Surface(modifier = Modifier.fillMaxSize()) {
LoginScreen(authViewModel, openLoginDialog)
}
}
}

In LoginScreen wrap skip button with the following condition:
We don’t want to show the skip button when the user is already anonymously authenticated.

if (DataProvider.authState == 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 skip button.

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 AuthRepositoryImpl that will be used with any service provider (except Email of course).

There are two ways to authenticate a user

  1. signInWithCredential(authCredential:) on auth object, to authenticate the user for the first time, or after the user signed out.
  2. linkWithCredential(authCredential:) to link an already authenticated user with a 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 Android

Let’s create these three generic functions in AuthRepositoryImpl class:

  1. authenticateUser(credential:) function, that takes an AuthCredential, and checks if we have an authenticated user, then call authLink(credential:), otherwise, call authSignIn(credential:).
  2. authSignIn(credential:) takes an AuthCredential, and authenticates a user using the given credentials.
  3. authLink(credential:) takes an AuthCredential, and links the given credentials to the current authenticated user.
// 1.
private suspend fun authenticateUser(credential: AuthCredential): FirebaseSignInResponse {
// If we have auth user, link accounts, otherwise sign in.
return if (auth.currentUser != null) {
authLink(credential)
} else {
authSignIn(credential)
}
}

// 2.
private suspend fun authSignIn(credential: AuthCredential): FirebaseSignInResponse {
return try {
val authResult = auth.signInWithCredential(credential).await()
Log.i(TAG, "User: ${authResult?.user?.uid}")
DataProvider.updateAuthState(authResult?.user)
Response.Success(authResult)
}
catch (error: Exception) {
Response.Failure(error)
}
}

// 3.
private suspend fun authLink(credential: AuthCredential): FirebaseSignInResponse {
return try {
val authResult = auth.currentUser?.linkWithCredential(credential)?.await()
Log.i(TAG, "User: ${authResult?.user?.uid}")
DataProvider.updateAuthState(authResult?.user)
Response.Success(authResult)
}
catch (error: Exception) {
Response.Failure(error)
}
}

Next Steps

Now that Firebase Authentication is setup, and user can authenticate anonymously, let’s add Google provider next.

--

--