As the title suggests I am having trouble deleting a Firebase user. I have 2 sign in types enabled in Firebase console :
Anonymous
Google
These providers types are mirrored in the application and signing in is not an issue using firebase-ui-auth
listOf(
IdpConfig.AnonymousBuilder().build(),
IdpConfig.GoogleBuilder().build())
I want the user to be able to delete their account, this works fine for Anonymous users but fails for Users that signed in with a google account using a GoogleAuthCredential. In order to do this the documentation states you need to "reauthenticate" : FirebaseUser::reauthenticate. This is where I am having trouble and re-authentication always fails with :
FirebaseAuthInvalidCredentialsException
ERROR_INVALID_CREDENTIAL
The supplied auth credential is malformed or has expired. [ Invalid id_token in IdP response: <token provided in request>, error: id token is not issued by Google. ]
I have checked the token is within the UTC expiry time, and my device clock is set correctly.
Current Code (using coroutines):
class UserActions internal constructor(
private val context: Context,
private val authUI: AuthUI,
private val auth: FirebaseAuth) {
suspend fun signOut(): Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.signOut(context)) }
suspend fun delete(): Boolean {
auth.currentUser
?.takeIf { user -> !user.isAnonymous }
?.let { user ->
val tokenResult: GetTokenResult = suspendCoroutine { cont -> cont.suspendTask(user.getIdToken(true)) }
val credential : AuthCredential = GoogleAuthProvider.getCredential(tokenResult.token, null)
// Point of failure - always returns false with above error.
val success: Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(user.reauthenticate(credential)) }
if (!success) return false
}
return suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.delete(context)) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun Continuation<Boolean>.suspendCompletableTask(task: Task<Void>) {
task.addOnSuccessListener { this.success() }
.addOnFailureListener { this.failure() }
}
private fun Continuation<Boolean>.success() = resume(true)
private fun Continuation<Boolean>.failure() = resume(false)
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)
}
I thought that maybe I had the token incorrectly added as a parameter arguemnt for :
GoogleAuthProvider.getCredential(tokenResult.token, null)
So swapped to :
GoogleAuthProvider.getCredential(null, tokenResult.token)
But said I had an invalid value in the error description so I have the argument correct for the AuthCredential and a "valid" id token as far as I can see.
What am I doing wrong here?
Solved : Simple answer to this one in the end.
Both FirebaseUser and GoogleSigInAccount refer to having a idToken. I was using the former for the token here :
val tokenResult: GetTokenResult = suspendCoroutine { cont -> cont.suspendTask(user.getIdToken(true)) }
val credential : AuthCredential = GoogleAuthProvider.getCredential(tokenResult.token, null)
The AuthCredential was now using the incorrect token. What I should have used was :
val token = GoogleSignIn.getLastSignedInAccount(context)?.idToken.orEmpty()
val credential : AuthCredential = GoogleAuthProvider.getCredential(token, null)
So the error was correct - I was using the FirebaseUser token when reauthenticating a GoogleAuthCredential which should be using the GoogleSigInAccount token.
Update
As tokens are short lived the above can still fail if the token becomes stale. the solution is to perform a "silent sign in" to refresh the token. This cannot be done through FirebaseAuth::silentSignin as this fails if a FirebaseUser is already signed in. It requires a call to GoogleSignInClient::silentSignIn.
Revised full code :
class UserActions internal constructor(
private val context: Context,
private val authUI: AuthUI,
private val auth: FirebaseAuth) {
suspend fun signOut(): Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.signOut(context)) }
suspend fun delete(): Boolean {
auth.currentUser
?.takeIf { user -> !user.isAnonymous }
?.let { user ->
val credential: AuthCredential = GoogleAuthProvider.getCredential(getFreshGoogleIdToken(), null)
val success: Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(user.reauthenticate(credential)) }
if (!success) return false
}
return suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.delete(context)) }
}
private suspend fun getFreshGoogleIdToken(): String = suspendCoroutine<GoogleSignInAccount> { cont ->
cont.suspendTask(
GoogleSignIn.getClient(
context,
GoogleSignInOptions.Builder()
.requestProfile()
.requestId()
.requestIdToken(context.getString(R.string.default_web_client_id))
.build())
.silentSignIn())
}.idToken.orEmpty()
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun Continuation<Boolean>.suspendCompletableTask(task: Task<Void>) {
task.addOnSuccessListener { this.success() }
.addOnFailureListener { this.failure() }
}
private fun Continuation<Boolean>.success() = resume(true)
private fun Continuation<Boolean>.failure() = resume(false)
private fun <R> Continuation<R>.success(r: R) = resume(r)
private fun <R> Continuation<R>.failure(t: Exception) = resumeWithException(t)
}
Related
I'm new to android and I'm developing a few applications for studying.
I've been trying to improve a code that I have but I got stuck in the following problem:
I'm creating a new user, validating it with Google Firebase. I managed to create a user normally but I'm not able to handle with one exception from the register moment which is the "FirebaseAuthUserCollisionException".
I created a class to handle a most of exceptions from email/password mistakes:
class AddUser(private val repository: UserRepository) {
#Throws(InvalidUserException::class)
suspend operator fun invoke(user: UserModel) {
if(user.email.isEmpty()) {
throw InvalidUserException("Email cannot be empty")
}
if(!Patterns.EMAIL_ADDRESS.matcher(user.email).matches()) {
throw InvalidUserException("Email is not valid")
}
if(user.password.length <= 5) {
throw InvalidUserException("Password should contain at least 6 characters")
}
if(user.password.isEmpty()) {
throw InvalidUserException("Password cannot be empty")
}
if(user.confirmPassword.isEmpty()) {
throw InvalidUserException("Confirm password cannot be empty")
}
if(user.password != user.confirmPassword) {
throw InvalidUserException("Passwords does not match")
}
repository.insert(user)
}
}
My repository:
class UserRepositoryImpl: UserRepository {
private var auth: FirebaseAuth = Firebase.auth
private var database: DatabaseReference = FirebaseDatabase.getInstance().getReference("users")
override suspend fun insert(user: UserModel) {
auth = FirebaseAuth.getInstance()
auth.createUserWithEmailAndPassword(user.email, user.password).addOnCompleteListener {
if(it.isSuccessful) {
database.child(user.id.toString()).setValue(user)
} else {
//exception here
}
}
}
}
When this function is triggered, it navigates to another fragment and toasts the successful message, which is incorrect because the exception happens.
Fragment:
private fun configEventFlow() = lifecycleScope.launch {
viewModel.eventFlow.collectLatest { event ->
when(event) {
is RegisterViewModel.UiEvent.ShowToast -> {
toast(event.message)
}
is RegisterViewModel.UiEvent.SaveUser -> {
val action = RegisterFragmentDirections.actionRegisterFragmentToMainFragment()
findNavController().navigate(action)
toast(getString(R.string.register_successfully))
}
}
}
}
private fun configUserRegistration() = with(binding) {
fabRegister.setOnClickListener {
val email = editRegisterEmail.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredEmail(email))
val password = editRegisterPassword.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredPassword(password))
val confirmPassword = editRegisterPasswordConfirm.text.toString()
viewModel.onEvent(RegisterUserEvents.EnteredConfirmPassword(confirmPassword))
viewModel.onEvent(RegisterUserEvents.SaveUser)
}
}
ViewModel:
#HiltViewModel
class RegisterViewModel #Inject constructor(private val useCases: UserUseCases): ViewModel() {
private val _email = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val email: StateFlow<ResourceState<String>> = _email
private val _password = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val password: StateFlow<ResourceState<String>> = _password
private val _confirmPassword = MutableStateFlow<ResourceState<String>>(ResourceState.Empty())
private val confirmPassword: StateFlow<ResourceState<String>> = _confirmPassword
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
fun onEvent(event: RegisterUserEvents) {
when(event) {
is RegisterUserEvents.EnteredEmail -> {
_email.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.EnteredPassword -> {
_password.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.EnteredConfirmPassword -> {
_confirmPassword.value = ResourceState.Success(event.value)
}
is RegisterUserEvents.SaveUser -> {
viewModelScope.launch {
try {
useCases.addUser(
UserModel(
id = System.currentTimeMillis().toInt(),
email = email.value.data!!,
password = password.value.data!!,
confirmPassword = confirmPassword.value.data!!
)
)
_eventFlow.emit(UiEvent.SaveUser)
} catch(e: InvalidUserException) {
_eventFlow.emit(UiEvent.ShowToast(message = e.message!!))
}
}
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String): UiEvent()
object SaveUser: UiEvent()
}
}
Is there a way that I can manage this specific exception in this pattern? Even if I catch the exception there, the action is completed and my application follows at it was registered but in the database it does not occur because of the exception. Im sure that I'll have to face it again when login to handle specific exceptions from Firebase, which I cannot create this way but I have to receive them and display to the user.
Any suggestions??
Sorry if it's missing any content, tell me and I update asap.
Thanks in advance.
I'm working on an android app which require Firebase phone authentication, When I released my app, a recapture still required as in debug mode, I enabled Android Device Verification in google cloud console, also I added google play console SHA-256, SHA-1 to Firebase project setting.
Bellow my Kotlin code of Firebase authentication:
fun initFirebase() {
mAuth = FirebaseAuth.getInstance()
}
fun sendOtp(mobile: String, resend: Boolean) {
if (resend) resend(mobile)
else send(mobile)
}
private fun send(mobile: String) {
val options = PhoneAuthOptions.newBuilder(mAuth)
.setPhoneNumber(mobile)
.setTimeout(5L, TimeUnit.SECONDS)
.setActivity(requireActivity())
.setCallbacks(mCallbacks)
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
Also at many time I get VerificationFailed without getting reason of this failure
private val mCallbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
override fun onVerificationCompleted(authCredential: PhoneAuthCredential) {}
override fun onVerificationFailed(e: FirebaseException) {
events.value = Constant.EVENT_SEND_FAILED
Log.v("TAG","VerificationFailed")
}
override fun onCodeSent(
verificationId: String,
forceResendingToken: PhoneAuthProvider.ForceResendingToken
) {
Log.v("TAG","Success")
}
and in verification fragment:
private lateinit var mAuth: FirebaseAuth
var mVerificationId: String = ""
fun verifyOtp(otp: String) {
val credential = PhoneAuthProvider.getCredential(
mVerificationId,
otp
)
mAuth.signInWithCredential(credential).addOnCompleteListener(requireActivity()) {
if (it.isSuccessful) {
mAuth.currentUser?.getIdToken(true)?.addOnCompleteListener { task ->
if (task.isSuccessful && task.result != null) {
val phoneVerificationToken = task.result.token
if (phoneVerificationToken != null) {
Log.v("TAG","Success")
}
} else {
Log.v("TAG","Error")
}
}
} else {
Log.v("TAG","Invalid code")
}
}
}
I'm writing a unit test for my app. I need to test a function that handles the login inside my app. This login handler uses a Callback that notifies when the login is complete. This is my Login Handler
class LoginManager {
private var auth: FirebaseAuth = Firebase.auth
fun loginWithEmail(
email: String,
password: String,
loginCallback: LoginCallback
) {
auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { loginTask ->
if (loginTask.isSuccessful) {
auth.currentUser?.let { user ->
loginCallback.onLoginSuccess(user)
return#addOnCompleteListener
}
}
loginCallback.onLoginFailure(loginTask.exception)
}
} }
LoginCallback is a simple interface
interface LoginCallback {
fun onLoginSuccess(user: FirebaseUser)
fun onLoginFailure(error: Exception?) }
I need to test the loginWithEmail function. In my test I try this code
Handler(getMainLooper()).post {
var firebaseUser: FirebaseUser? = null
val loginCallback = object : LoginCallback {
override fun onLoginSuccess(user: FirebaseUser) {
firebaseUser = user
}
override fun onLoginFailure(error: Exception?) {
//No action needed
}
}
val loginManager = LoginManager(loginCallback)
loginManager.loginWithEmail(EXISTING_EMAIL_ADDRESS, CORRECT_PASSWORD)
Thread.sleep(5000)
assert(firebaseUser != null)
}
shadowOf(getMainLooper()).idle()
But the callback is never being called. I also try with an ArgumentCaptor
Handler(getMainLooper()).post {
val captor = ArgumentCaptor.forClass(LoginCallback::class.java)
val loginManager = LoginManager()
loginManager.loginWithEmail(EXISTING_EMAIL_ADDRESS, CORRECT_PASSWORD, captor.capture())
}
shadowOf(getMainLooper()).idle()
But with this code, I'm getting this error java.lang.NullPointerException: captor.capture() must not be null
Is there a way to test my function?
I use FirebaseAuth for registration new user
class FirebaseAuthenticationServiceImpl(): FirebaseAuthenticationService {
override fun registerWithEmailAndPassword(email: String, password: String): Boolean {
val registration = FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
println(it.additionalUserInfo?.providerId.toString())
}.addOnFailureListener {
println(it.message.toString())
}
return registration.isSuccessful
}
}
I call function above and every time I get false. After some time I get true
coroutineScope {
try {
if (firebaseService.registerWithEmailAndPassword(email, password)) {
openHomeActivity.offer(Unit)
} else {}
} catch (e: Exception) {}
}
How can I wait for uth result (success/failure) and afer that get that value?
Where is FirebaseAuthenticationService from? Do you need it? The official getting started guide just uses Firebase.auth. With this, you can authenticate using the await() suspend function instead of using the callback approach.
// In a coroutine:
val authResult = Firebase.auth.registerWithEmailAndPassword(email, password).await()
val user: FirebaseUser = authResult.user
if (user != null) {
openHomeActivity.offer(Unit)
} else {
// authentication failed
}
If you are using coroutines you can use suspendCoroutine which is perfect bridge between traditional callbacks and coroutines as it gives you access to the Continuation<T> object, example with a convenience extension function for Task<R> objects :
scope.launch {
val registrationResult = suspendCoroutine { cont -> cont.suspendTask(FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)
hi this is my user repository
class UserRepository(private val appAuth: FirebaseAuth) : SafeAuthRequest(){
suspend fun userLogin(email: String,password: String) : AuthResult{
return authRequest { appAuth.signInWithEmailAndPassword(email,password)}
}
}
this is the SafeAuthRequest class
open class SafeAuthRequest {
suspend fun<T: Any> authRequest(call : suspend () -> Task<T>) : T{
val task = call.invoke()
if(task.isSuccessful){
return task.result!!
}
else{
val error = task.exception?.message
throw AuthExceptions("$error\nInvalid email or password")
}
}
}
calling above things like that
/** Method to perform login operation with custom */
fun onClickCustomLogin(view: View){
authListener?.onStarted()
Coroutines.main {
try {
val authResult = repository.userLogin(email!!,password!!)
authListener?.onSuccess()
}catch (e : AuthExceptions){
authListener?.onFailure(e.message!!)
}
}
}
and my authListener like this
interface AuthListener {
fun onStarted()
fun onSuccess()
fun onFailure(message: String)
}
I am getting an error as the task is not completed
is the correct way to implement the task
I'm using MVVM architectural pattern, so the example I'm going to provide is called from my ViewModel class, that means I have access to viewModelScope. If you want to run a similar code on Activity class, you have to use the Coroutines scope available for your Activity, for example:
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {...}
Answering your question, what I've done to retrieve login from user repository is this:
//UserRepository.kt
class UserRepository(private val appAuth: FirebaseAuth) {
suspend fun userLogin(email: String, password: String) : LoginResult{
val firebaseUser = appAuth.signInWithEmailAndPassword(email, password).await() // Do not forget .await()
return LoginResult(firebaseUser)
}
}
LoginResult is a wrapper class of firebase auth response.
//ClassViewModel.kt
class LoginFirebaseViewModel(): ViewModel(){
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login() {
viewModelScope.launch {
try {
repository.userLogin(email!!,password!!).let {
_loginResult.value = it
}
} catch (e: FirebaseAuthException) {
// Do something on firebase exception
}
}
}
}
The code on Activity class would be like this:
// Function inside Activity
fun onClickCustomLogin(view: View){
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {
try {
repository.userLogin(email!!,password!!).let {
authResult = it
}
}catch (e : FirebaseAuthException){
// Do something on firebase exception
}
}
}
One of the main benefits of using Coroutines is that you convert asynchronous code in sequential one. That means you don't need listeners or callbacks.
I hope this help you