Firebase Authentication in Jetpack Compose
Anonymous Authentication
Implementing guest accounts with Firebase Anonymous Authentication
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
- Part 1: Setup and Anonymous Authentication.
- Part 2: Google Authentication.
- 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.
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:
- Define
AuthStateListener
that will be called when there is a change in the authentication state. - Register a listener to changes in the user authentication state.
- When the
viewModelScope
is closed, unregister the listener to authentication changes. - Inside
AuthStateListener
callback, return the current authenticated user result — (the result will benull
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:
- Create a private
getAuthState()
function that callsgetAuthState(viewModelScope)
on therepository
instance. - Start listening to the auth state changes upon
AuthViewModel
initialization. - Create a public variable
currentUser
to be accessible and collected fromMainActivity
.
@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:
anonymousSignInResponse
,googleSignInResponse
, &signOutResponse
will be used to track the auth process responses.- Create
updateAuthState(user:)
function that takes aFirebaseUser
and updates theuser
and theauthState
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:
- Create a
AuthViewModel
instance. - Create a
currentUser
of typeAuthStateResponse
which is aStateFlow<FirebaseUser?>
, so that the value can be collected by callingcollectAsState()
. - Call
updateAuthState(user:)
to update the user value. - 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:
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..
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.
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
signInWithCredential(authCredential:)
onauth
object, to authenticate the user for the first time, or after the user signed out.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:
authenticateUser(credential:)
function, that takes anAuthCredential
, and checks if we have an authenticated user, then callauthLink(credential:)
, otherwise, callauthSignIn(credential:)
.authSignIn(credential:)
takes anAuthCredential
, and authenticates a user using the given credentials.authLink(credential:)
takes anAuthCredential
, 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.
Resources
“ Everyone has something to learn. Everyone has something to teach.” ~ Paul Hudson