Firebase User Authentication:Email and Google Sign in on Android with Kotlin
Loveth Nwokike
Posted on January 11, 2021
Firebase authentication is a fast and easy way of providing secure user verification process. There are several platforms made available with firebase to simplify the process including Facebook, phone auth, GitHub, google, email, twitter and more. We will be discussing google and email implementation in this post.
By the end of this post you will learn how to :
Create and setup a firebase account
Add your app to firebase
Enable google and email authentication with firebase
Implement google authentication
Implement Email Authentication
Validate user details and check for errors
Verify users and recover passwords
🎁 Save user to cloud firestore
Fetch signed in user
What you should already know
familiarity with writing android apps with Kotlin
dependency injection(di) with hilt but not very necessary
di is the short for dependency injection and will be used through out the post.
A sample simplifying firebase authentication with email and gmail in android with Kotlin
FirebaseAndroidAuthSample
A sample simplifying firebase authentication with email and google in android
Libraries used
Firebase
Hilt-Android
Navigation Components
ViewModel
LiveData
ViewBinding
Timber
MIT License
Copyright (c) 2021 Loveth Nwokike
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
To create a firebase account you must have a google account which you will be using to sign in. Go to firebase and click on sign at the top right corner of the page then proceed to use your preferred google account.
Next is to register/add your app to it. There are two ways you can do that either by using the firebase console following these steps or the easier way using the firebase assistant in android studio
To add your app to firebase using the firebase assistant in android studio:
Go to tools > firebase > authentication > email and password >connect to firebase
You will be taken to the browser where you can
either choose an existing app or create a new one. If everything is successful you get a popup like this:
Then you click on connect and finally you get this
confirming that your app has been connected.
back in android studio, In the firebase assistant click on
Authentication > email and password authentication >Add firebase authentication to your app
Firestore > Read and write documents with Cloud Firestore > Add Cloud Firestore to your app
then allow the app to sync
At this point a googleservices.json has been added to your app directory and some dependencies added as well. When you check your app level build.gradle you will find out that the following dependency have been added for you implementation 'com.google.firebase:firebase-auth:20.0.1' implementation 'com.google.firebase:firebase-firestore:22.0.1'
and google service plugin added as well id 'com.google.gms.google-services'
also in project level build.gradle google service class path has been added classpath 'com.google.gms:google-services:4.3.4'
Create a di class to initalize firebaseAuth and firestore
FirebaseModule.kt
@Module
@InstallIn(ApplicationComponent::class)
class FireBaseModule {
@Provides
@Singleton
fun provideFireBaseAuth(): FirebaseAuth {
return FirebaseAuth.getInstance()
}
@Provides
@Singleton
fun provideFirestore()= FirebaseFirestore.getInstance()
Create a class for firebaseSource
FirebaseSource.kt
class FireBaseSource @Inject constructor(private val firebaseAuth: FirebaseAuth,private val firestore: FirebaseFirestore) {
fun signUpUser(email:String,password:String,fullName:String) = firebaseAuth.createUserWithEmailAndPassword(email,password)
fun signInUser(email: String,password: String) = firebaseAuth.signInWithEmailAndPassword(email,password)
fun saveUser(email: String,name:String)=firestore.collection("users").document(email).set(User(email = email,fullName = name))
}
here we have methods to sign up, sign and save a user
firebaseAuth.createUserWithEmailAndPassword(email,password)
signs up a user with an email and password
firebaseAuth.signInWithEmailAndPassword(email,password)
signs in user with an existing email and password
firestore.collection("users").document(email).set(User(email = email,fullName = name)) creates users document and save a user to it
Repository.kt
class Repository @Inject constructor(private val fireBaseSource: FireBaseSource) {
fun signUpUser(email: String, password: String, fullName: String) = fireBaseSource.signUpUser(email, password, fullName)
fun signInUser(email: String, password: String) = fireBaseSource.signInUser(email, password)
fun saveUser(email: String, name: String) = fireBaseSource.saveUser(email, name)
}
SignUpViewModel.kt
class SignUpViewModel @ViewModelInject constructor(
private val repository: Repository,
private val networkControl: NetworkControl,
private val firebaseAuth: FirebaseAuth
) : ViewModel() {
private val userLiveData = MutableLiveData<Resource<User>>()
fun signUpUser(email: String, password: String, fullName: String): LiveData<Resource<User>> {
when {
TextUtils.isEmpty(email) && TextUtils.isEmpty(password) && TextUtils.isEmpty( fullName ) -> {
userLiveData.postValue(Resource.error(null, "field must not be empty"))
}
password.length < 8 -> {
userLiveData.postValue( Resource.error(null, "password must not be less than 8"))
}
networkControl.isConnected() -> {
userLiveData.postValue(Resource.loading(null))
firebaseAuth.fetchSignInMethodsForEmail(email).addOnCompleteListener {
if (it.result?.signInMethods?.size == 0) {
repository.signUpUser(email, password, fullName).addOnCompleteListener { task ->
if (task.isSuccessful) {
firebaseAuth.currentUser?.sendEmailVerification()
userLiveData.postValue( Resource.success( User( email = email, fullName = fullName )))
} else {
userLiveData.postValue( Resource.error(null, it.exception?.message.toString()))
} }
} else {
userLiveData.postValue(Resource.error(null, "email already exist"))
} }
} else -> {
userLiveData.postValue(Resource.error(null, "No internet connection"))
} }
return userLiveData
}
fun saveUser(email: String, name: String) {
repository.saveUser(email, name).addOnCompleteListener {
if (it.isSuccessful) {
_saveUserLiveData.postValue(Resource.success(User(email,name)))
}else{
_saveUserLiveData.postValue(Resource.error(null,it.exception?.message.toString()))
}
}
}
}
In the SignupViewModel class we check for errors and validate user details
checks if the fields are empty
checks if the password field is less than 8
checks if the email already exists
then throws up suitable errors for all cases otherwise the user is successfully signed up with their details and email verification is sent to them firebaseAuth.currentUser?.sendEmailVerification()
a method to save to the user detail to firestore if successfully signed up
In the signup fragment we observe the signup user method, save user if successfully signed up and display message with the snackBar for changes that occur with the success, error and loading cases
Next we configure googlesigninoptions(gso) in the di class calling the requestidToken and requestEmail then create the googleClient object with gso configured
FirebaseModule.kt
@Provides
@Singleton
fun provideGso(@ApplicationContext context: Context) = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(context.getString(R.string.default_web_client_id))
.requestEmail()
.build()
@Provides
@Singleton
fun provideGoogleClient(@ApplicationContext context: Context, gso:GoogleSignInOptions)= GoogleSignIn.getClient(context, gso)
FirebaseSource.kt
fun signInWithGoogle(acct: GoogleSignInAccount) = firebaseAuth.signInWithCredential(GoogleAuthProvider.getCredential(acct.idToken,null))
fun fetchUser()=firestore.collection("users").get()
Repository.kt
fun signInWithGoogle(acct: GoogleSignInAccount) = fireBaseSource.signInWithGoogle(acct)
fun fetchUser() = fireBaseSource.fetchUser()
SignIn User with email or gmail
LoginViewModel.kt
fun signInUser(email: String, password: String): LiveData<Resource<User>> {
when {
TextUtils.isEmpty(email) && TextUtils.isEmpty(password) -> {
userLiveData.postValue(Resource.error(null, "Enter email and password"))
}
networkControl.isConnected() -> {
userLiveData.postValue(Resource.loading(null))
firebaseAuth.fetchSignInMethodsForEmail(email).addOnCompleteListener {
if (it.result?.signInMethods?.size == 0) {
userLiveData.postValue(Resource.error(null, "Email does not exist"))
} else {
repository.signInUser(email, password).addOnCompleteListener { task ->
if (task.isSuccessful) {
firebaseAuth.currentUser?.isEmailVerified?.let { verified ->
if (verified) {
repository.fetchUser().addOnCompleteListener { userTask ->
if (userTask.isSuccessful) {
userTask.result?.documents?.forEach {
if (it.data!!["email"] == email) {
val name = it.data?.getValue("fullName")
userLiveData.postValue(Resource.success(User(firebaseAuth.currentUser?.email!!, name?.toString()!!)
)) } }
} else {
userLiveData.postValue(Resource.error(null, userTask.exception?.message.toString()))
}
}
} else {
userLiveData.postValue(Resource.error(null, "Email is not verified, check your email"))
} }
} else {
userLiveData.postValue(Resource.error(null, task.exception?.message.toString()))
Timber.e(task.exception.toString())
} } } }
}
else -> {
userLiveData.postValue(Resource.error(null, "No internet connection"))
}
}
return userLiveData
}
fun signInWithGoogle(acct: GoogleSignInAccount): LiveData<Resource<User>> {
repository.signInWithGoogle(acct).addOnCompleteListener { task ->
if (task.isSuccessful) {
gMailUserLiveData.postValue(
Resource.success(
User(
firebaseAuth.currentUser?.email!!,
firebaseAuth.currentUser?.displayName!!
)
)
)
} else {
gMailUserLiveData.postValue(Resource.error(null, "couldn't sign in user"))
}
}
return gMailUserLiveData
}
In LoginViewModel we have signin method for both email and gmail
For sign in with email
We check if email exist
check if email has been verified
then check the email in firestore to get the full name associated with it during registration and post it through LiveData for the view to observe on sign in
For sign in with google
We get the selected google account
then get the display name and email associated with it and post via LiveData
LoginFragment.kt
@AndroidEntryPoint
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels()
private var binding: FragmentLoginBinding? = null
@Inject
lateinit var googleSignInClient: GoogleSignInClient
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentLoginBinding.inflate(layoutInflater)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.signUpTv?.setOnClickListener {
if (findNavController().currentDestination?.id == R.id.loginFragment) {
NavHostFragment.findNavController(this)
.navigate(LoginFragmentDirections.actionLoginFragmentToSignUpFragment())
}
}
binding?.signInBtn?.setOnClickListener {
val emailText = binding?.emailEt?.text?.toString()
val passwordText = binding?.passwordEt?.text.toString()
viewModel.signInUser(emailText!!, passwordText).observe(viewLifecycleOwner, {
when (it.status) {
Status.LOADING -> {
view.showsnackBar("...")
}
Status.SUCCESS -> {
view.showsnackBar("Login successful")
if (findNavController().currentDestination?.id == R.id.loginFragment) {
NavHostFragment.findNavController(this)
.navigate(
LoginFragmentDirections.actionLoginFragmentToDashBoardFragment(
it.data?.fullName!!
)
)
}
}
Status.ERROR -> {
view.showsnackBar(it.message!!)
}
}
})
}
binding?.googleSignIn?.setOnClickListener {
signIn()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
val account = task.getResult(ApiException::class.java)
viewModel.signInWithGoogle(account!!).observe(viewLifecycleOwner, {
if (it.status == Status.SUCCESS) {
if (findNavController().currentDestination?.id == R.id.loginFragment) {
NavHostFragment.findNavController(this).navigate(
LoginFragmentDirections.actionLoginFragmentToDashBoardFragment(it?.data?.fullName!!)
)
// Timber.d("display ${fAuth.currentUser?.displayName} ")
}
} else if (it.status == Status.ERROR) {
requireView().showsnackBar(it.message!!)
}
})
} catch (e: ApiException) {
Toast.makeText(requireContext(), e.message, Toast.LENGTH_SHORT).show()
}
}
}
private fun signIn() {
val signInIntent: Intent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}
}
In the LoginFragment
We observe the sign in method and get the user details
We receive the data from the google account in the onActivityResult
call startActivityForResult for the request code received
If successful we navigate the user to dashboard activity and display their name
Forgot password
In the FirebaseSource.kt we create the reset password method fun sendForgotPassword(email: String) = firebaseAuth.sendPasswordResetEmail(email)
In the Repository.kt we call the method fun sendForgotPassword(email: String)=fireBaseSource.sendForgotPassword(email)
In the LoginViewModel we create a LiveData to observe for when the email is sent
val dialog = AlertDialog.Builder(requireContext())
val inflater = (requireActivity()).layoutInflater
val v = inflater.inflate(R.layout.forgot_password, null)
dialog.setView(v)
.setCancelable(false)
val d = dialog.create()
val emailEt = v.findViewById<TextInputEditText>(R.id.emailEt)
val sendBtn = v.findViewById<MaterialButton>(R.id.sendEmailBtn)
val dismissBtn = v.findViewById<MaterialButton>(R.id.dismissBtn)
sendBtn.setOnClickListener {
viewModel.sendResetPassword(emailEt.text.toString()).observeForever {
if (it.status == Status.SUCCESS){
view.showsnackBar("reset email sent")
}else{
view.showsnackBar(it.message.toString())
}
}
}
dismissBtn.setOnClickListener {
d.dismiss()
}
binding?.forgotPasswordTv?.setOnClickListener {
d.show()
}
We create a dialog that is displayed when forgot password is clicked on so the user can enter a registered email and received a link to reset their password
Finally we have a working user authentication to verify and validate user with email/password and google. You can checkout this repository for a complete implementation.
A sample simplifying firebase authentication with email and gmail in android with Kotlin
FirebaseAndroidAuthSample
A sample simplifying firebase authentication with email and google in android
Libraries used
Firebase
Hilt-Android
Navigation Components
ViewModel
LiveData
ViewBinding
Timber
MIT License
Copyright (c) 2021 Loveth Nwokike
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE