I am currently developing a android app which uses FirebaseAuth to control users.
The users can sign-in either using email or Google.
Considering this, i am catching some exceptions in order to handle any problems with the authentication system.
Most of these work fine. This is my code:
when(task.exception!!){
is FirebaseAuthEmailException -> {
errorDialogBuilder.setTitle(R.string.exception_email_title)
errorDialogBuilder.setMessage(getString(R.string.exception_email_msg, task.exception!!.localizedMessage))
}
is FirebaseAuthInvalidCredentialsException -> {
errorDialogBuilder.setTitle(R.string.exception_invalid_credentials_title)
errorDialogBuilder.setMessage(getString(R.string.exception_invalid_credentials_msg, task.exception!!.localizedMessage))
}
is FirebaseAuthInvalidUserException -> {
val invalidUserException = task.exception!! as FirebaseAuthInvalidUserException
when (invalidUserException.errorCode) {
"ERROR_USER_DISABLED" -> {
errorDialogBuilder.setTitle(R.string.exception_user_disabled_title)
errorDialogBuilder.setMessage(R.string.exception_user_disabled_msg)
errorDialogBuilder.setNeutralButton(R.string.more, null)
}
"ERROR_USER_NOT_FOUND" -> {
errorDialogBuilder.setTitle(R.string.exception_user_not_found_title)
errorDialogBuilder.setMessage(getString(R.string.exception_user_not_found_msg, fragmentView.email_input_signin.text.toString()))
errorDialogBuilder.setNeutralButton(R.string.action_createnew_account) { _, _ ->
viewModel.createUser(fragmentView.email_input_signin.text.toString(), fragmentView.email_password.text.toString())
}
}
else -> {
errorDialogBuilder.setTitle(invalidUserException.errorCode)
errorDialogBuilder.setMessage(invalidUserException.localizedMessage)
}
}
}
}
I had no problems when using this code for development purposes. But I found out something very weird:
I am playing around with disabling users; This issue ocurred: Whenever I disable a google account, not a FirebaseAuthInvalidUserException is thrown with code ERROR_USER_DISABLED as when trying to sign in with a disabled email-based account, but a FirebaseUserCollissionException which has the exact same message as a FirebaseAuthInvalidUserException with code ERROR_USER_DISABLED:
com.google.firebase.auth.FirebaseAuthUserCollisionException: The user account has been disabled by an administrator.
at com.google.firebase.auth.api.internal.zzdx.zza(com.google.firebase:firebase-auth##19.1.0:42)
at com.google.firebase.auth.api.internal.zzfa.zza(com.google.firebase:firebase-auth##19.1.0:19)
at com.google.firebase.auth.api.internal.zzet.zza(com.google.firebase:firebase-auth##19.1.0:34)
at com.google.firebase.auth.api.internal.zzev.zza(com.google.firebase:firebase-auth##19.1.0:98)
at com.google.firebase.auth.api.internal.zzev.zza(com.google.firebase:firebase-auth##19.1.0:85)
at com.google.firebase.auth.api.internal.zzed.zza(com.google.firebase:firebase-auth##19.1.0:43)
at com.google.android.gms.internal.firebase_auth.zza.onTransact(com.google.firebase:firebase-auth##19.1.0:13)
at android.os.Binder.execTransactInternal(Binder.java:1021)
at android.os.Binder.execTransact(Binder.java:994)
This is how it looks normally, when trying to use an disabled email-based account(this is handled&this works):
com.google.firebase.auth.FirebaseAuthInvalidUserException: The user account has been disabled by an administrator.
at com.google.firebase.auth.api.internal.zzdx.zza(com.google.firebase:firebase-auth##19.1.0:6)
at com.google.firebase.auth.api.internal.zzfa.zza(com.google.firebase:firebase-auth##19.1.0:21)
at com.google.firebase.auth.api.internal.zzet.zza(com.google.firebase:firebase-auth##19.1.0:34)
at com.google.firebase.auth.api.internal.zzev.zza(com.google.firebase:firebase-auth##19.1.0:74)
at com.google.firebase.auth.api.internal.zzed.zza(com.google.firebase:firebase-auth##19.1.0:18)
at com.google.android.gms.internal.firebase_auth.zza.onTransact(com.google.firebase:firebase-auth##19.1.0:13)
at android.os.Binder.execTransactInternal(Binder.java:1021)
at android.os.Binder.execTransact(Binder.java:994)
Why are there two different exceptions being used here? And why would firebase throw a FirebaseAuthUserCollisionException when the users account is disabled, as, from what i know from the docs, the FirebaseAuthUserCollisionException is only thrown when there are user accounts conflicting?
In addition:
Brief extract from the firebase docs:
FirebaseAuthUserCollisionException
public final class FirebaseAuthUserCollisionException extends FirebaseAuthException
Thrown when an operation on a FirebaseUser instance couldn't be
completed due to a conflict with another existing user.
That's weird. I read the Google API for Android docs - and what you said was correct - FirebaseAuthUserCollisionException should only be thrown when there is a conflict between Firebase users, particularly if both share the same credential accidentally, but I think Firebase Auth does not allow that and it can detect if the credential between two users or more are similar right from the start.
I think that Firebase Auth tries to re-register/re-add the disabled Google account into the system when you use it to sign in. Since the Google account is disabled, Firebase Auth may consider the account invalid - yet the account still remain and recorded in the auth system - hence if you are trying to sign in with the previously disabled Google account, Firebase Auth tries to re-add that and collide with the previous record of disabled Google account.
There is also a chance that this may be a bug/glitch/error in Firebase Auth side.
If you are asking about difference, I think it's more to the circumstances/conditions that caused either of those exceptions to occur; most exceptions are similar, it is the purpose and cause of error that makes the difference.
Hope this helps.
Related
I have the possibility that the user can choose if they want to log in with Google, Facebook, email/password, etc.
After testing my app, the following happened:
I sign up with my name, email, and password
Handle the get started logic
Verify my auth users on Firebase (grey email icon)
Sign out of the account
Now, I want to log in with Google (same email used on the sign-up with email and password)
The Google sign-in worked
Verify my auth users on Firebase (the grey email icon changed into the Google one)
Sign out of the account
Can't log in with email and password anymore but the google sign in worked
After some research, I end up with the Link Multiple Auth Providers to an Account on Android documentation
I realized I have to refactor my code to not use the FirebaseAuth.signInWith methods
This is a little except of my loginEmailAndPassword:
val credential = EmailAuthProvider.getCredential(email, password)
firebaseAuth.currentUser!!.linkWithCredential(credential).addOnCompleteListener{ authTask: Task<AuthResult> ->
if (authTask.isSuccessful) {
I have an 'else' meaning the (authTask.isSuccessful) did not happened and another 'if' with the FirebaseAuthUserCollisionException
val exception: java.lang.Exception? = authTask.exception
if (exception is FirebaseAuthUserCollisionException) {
linkAndMerge(credential)
My goal is to link and merge, and I do not know how to link the accounts (both email grey and Google on Firebase)
private fun linkAndMerge(credential: AuthCredential) {
val authenticatedUserMutableLiveData: MutableLiveData<ResponseState<UserModel>> =
MutableLiveData()
val prevUser = firebaseAuth.currentUser
firebaseAuth.signInWithCredential(credential)
.addOnSuccessListener { result ->
val currentUser = result.user
// Merge prevUser and currentUser accounts and data
// ...
}
.addOnFailureListener {
authenticatedUserMutableLiveData.value = ResponseState.Error("Error")
}
}
My questions:
Can I call something to merge prevUser and currentUser accounts. I just want to the user have the possibility of using different authentications.
I am not worried about the data because if it's the same User UID does not matter if the authentication provider
Can I still use 'createUserWithEmailAndPassword'?
Steps 1 to 9 provide the expected behavior. If you create a user with email and password and right after that you sign in with Google, the account will only be accessible with Google. Why? Because behind the scenes Firebase converts the account that was created with email and password into an account with the Google provider. Unfortunately, you cannot reverse that change.
The link in your question, is referring to the possibility to link an existing account to a specific provider. For example, if you implement anonymous authentication, then you can link that account with Google, for example. This means that the UID remains the same.
If you want to stop that mechanism from happening, then you should consider allowing the creation of different accounts for different providers. You can find this option which is called "Create multiple accounts for each identity provider" right inside the Firebase Console, in the Settings tab inside the Authentication.
So, I've implemented Firebase Auth & AuthUI and successfully used the Google auth provider to log in a user.
However, now I'm trying to access the Google APIs but I'm getting a login error when I try to access them. Code below is attempting to connect to Google Fit.
val account = GoogleSignIn.getAccountForExtension(requireContext(), fitnessOptions)
Fitness.getHistoryClient(requireContext(), account)
.readDailyTotal(DataType.TYPE_STEP_COUNT_DELTA)
.addOnSuccessListener { r ->
val first = r.dataPoints.first()
...
}
.addOnFailureListener { e ->
...
}
at which point I'm getting an error message of:
com.google.android.gms.common.api.ApiException: 4: The user must be signed in to make this API call.
I've debugged the Firebase Auth process and can see that there are two providers on the current FirebaseUser, one Google & one Firebase which looks like rather than authing my app with Google, Firebase is authing itself, then authing my app with Firebase.
I'm pretty sure I can remove AuthUI and manually do the Google SSO implementation (per Google's docs) but I really don't want to as I plan on having pretty much all the Auth providers enabled and I don't want to have to code them all manually (not to mention doing all the layout etc)
So, if anyone knows how to access the Google APIs/signed in Google Account from the Firebase authed user, I'd love to hear it!
Thanks in advance for any help you can render!
Ok, I figured it out. Firebase doesn't pass any default scopes to the Google sign in provider. Fixed it by doing
...
AuthUI.IdpConfig.GoogleBuilder()
.setScopes(listOf(Scopes.FITNESS_ACTIVITY_READ)
...
to access the standard steps read information. You can find out which scopes you need here and reverse-engineer it's name in the Scopes class from there.
Hope that helps!
I'm not using FirebaseUI. With FirebaseUI I managed to store the email and password into Smart Lock after creating a new account for an user of the app.
I had to remove FirebaseUI, because it doesn't allow a precise control over which account gets signed in with silent sign-in, and I rely on that to switch between the multiple accounts one user can have on a device.
Using the CredentialsClient I had no problems retrieving those passwords; the user got presented a Dialog where he could choose the account and the password would get handed over to the app via onActivityResult. I followed Google's guide Retrieve a user's stored credentials in order to do this.
But I am unable to save the new email and password as a new credential. I always get the error com.google.android.gms.common.api.CommonStatusCodes.SIGN_IN_REQUIRED with the message Passphrase required in form of an com.google.android.gms.common.api.ApiException.
This is my code:
CredentialsOptions options = new CredentialsOptions.Builder()
.forceEnableSaveDialog()
.build();
CredentialsClient mCredentialsClient = Credentials.getClient(MainActivity.activity, options);
Credential credential = new Credential.Builder("email#example.com")
.setPassword("dummy-password")
.build();
mCredentialsClient.save(credential).addOnCompleteListener(new OnCompleteListener<Void>() {
#Override public void onComplete(#NonNull Task<Void> task) {
if (task.isSuccessful()) {
LogWrapper.i(TAG, "Credentials saved");
}
else if (task.getException() instanceof ResolvableApiException) {
ResolvableApiException rae = (ResolvableApiException) task.getException();
LogWrapper.e(TAG, "RAE EXCEPTION WHEN SAVING PASSWORD:", rae);
}
else {
Exception e = task.getException();
LogWrapper.e(TAG, "EXCEPTION WHEN SAVING PASSWORD:", e);
}
}
});
I have three Google Accounts on that device, one of which has configured a passphrase for syncing, the other two don't.
Keep your info private
With a passphrase, you can use Google's cloud to store and sync your
Chrome data without letting Google read it. Your payment methods and
addresses from Google Pay aren't encrypted by a passphrase.
Passphrases are optional. Your synced data is always protected by
encryption when it's in transit.
I am not sure if this error message is being caused because of this, that this is the passphrase which is required. But then again I wonder why FirebaseUI had no issues with storing the Credentials, even into the correct account without asking, which is one which doesn't use a passphrase.
I also signed into the account via FirebaseUI's Google Sign-In where I want to save the credentials to, and then executed the code above in order to see if that signed-in user would set the app into a state where it is signed into that Google Account as the error messages seems to expect me to, and willing to save some email and password credentials into that account. That didn't make any difference.
I read Firebase's approach to using Smart Lock, and I can't find any real differences to what I'm doing.
It is basically the same as shown in the CredentialsQuickstart app.
So the question is: What needs to be signed in? Why am I getting this message?
Update: I've downloaded and compiled https://github.com/android/identity-samples/tree/master/CredentialsQuickstart and there I'm getting the same error, so this seems to be some more general issue.
This is on Android 10. I've also tried it on another device with Android 9 and there I have the same problem.
Has this API been "deprecated" by this thing called "AutoFill service"? I found an option in "Languages & Input" under "Tools" where there is an AutoFill Service from Google where I can select one account into which the credentials get stored into. And this is configured to use the account which has no passphrase, the one where FirebaseUI correctly stored the Credentials into. I think on a Samsung device it also offers an additional AutoFill service.
That would be this then: https://developer.android.com/guide/topics/text/autofill
Under The Hood
I am using Firebase Authentication in my Android app to sign up/in users using Google, Facebook and Email/Password. So far, almost everything works fine except for a single scenario.
The Scenario
I need to disable or delete user accounts from the Firebase console sometimes to ban some users of my app.
In that case, when I disable or delete that particular user, the user must get logged out from the app instantly and should not be able to use it any further.
The Bug
I have used the AuthStateListener to listen for authentication state changes and log out the user automatically as soon as their account is disabled or deleted.
FirebaseAuth.getInstance().addAuthStateListener(firebaseAuth -> {
if (firebaseAuth.getCurrentUser() == null) {
Intent intent = AuthFlowActivity.getCallingIntent(AuthFlowActivity.FORCE_LOGOUT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
activityExitAnimation(BaseAppActivity.this);
}
});
But I have never seen the AuthStateListener fire any events for these actions. So I am unable to log out the user instantly and the user can still keep on using the app.
I would appreciate if anyone can help in resolving this issue.
Disabling or deleting a user account does not fire an auth state change. Nor should it, the user is still authenticated. In at most an hour, Firebase Authentication will try to refresh the access token for the user. That refresh will fail, at which point the user will become unauthenticated and the auth state change event will fire.
If you're looking to revoke the user's authorization immediately, you will have to do so in another part of your application logic. A common way to do this is by having a blacklist in your application, e.g. in the Firebase Database:
/bannedUsers
uidOfBannedUser: true
Now when you delete/disable a user's account in the Autentication panel, you also add their uid to the list of banned users in the database.
The database can then be secured against access from unauthorized users by adding a clause to your database security rules, e.g.
{
"rules": {
"bannedUsers": {
".read": true,
".write": false // only admins can write these
},
"messages": {
".read": "auth != null && !root.child('bannedUsers').child(auth.uid).exists()"
}
}
}
If you use a different back-end, the implementation will be different. But a blacklist like this is a common approach to ban users. You'll find that you may even care little enough about their authentication that you only ban them, instead of deleting their credentials (which they could simply recreate).
I have been using Firebase Authentication in my app, and have noticed an issue with a particular use case.
I have enabled account linking sign up flow for my app, and thus I can attach multiple providers associated with a single email address.
Scenario 1: (Works fine)
The user has signed up with Google initially and sometime later, signs in in with Facebook or registers with email and password.
The account linking works fine and Facebook and/or Email is added in the provider list.
So, I can have 2 or 3 providers for the email, Google (initially), Facebook and Password (after that).
Scenario 2: (The bug)
The user has signed up with Facebook and/or Email initially and later signs in with Google, now the account linking doesn't work. Google replaces the previous providers present.
Account linking fails, and I just have Google as the sole provider associated with the email address and the others are gone.
In the second scenario, while signing in with Google, it should fail and throw FirebaseAuthCollisionException but it doesn't and succeeds. This is the main issue.
I can't paste the whole code here, but just a snippet for sure.
firebaseAuth
.signInWithCredential(credential)
.addOnFailureListener(exception -> {
if (exception instanceof FirebaseAuthUserCollisionException) {
mCredentialToLinkWith = credential;
if (mProviderList.size() == 1) {
if (mProviderList.contains(EmailAuthProvider.PROVIDER_ID)) {
mRegisterProviderPresenter.linkWithEmailProvider(credential, email);
} else {
linkProviderAccounts(email, AuthenticationHelper.getProviderToLinkAccounts(mWeakActivity, mProviderList));
}
} else {
linkProviderAccounts(email, AuthenticationHelper.getProviderToLinkAccounts(mWeakActivity, mProviderList));
}
} else {
Timber.d("Failed in signInWithCredential and unexpected exception %s", exception.getLocalizedMessage());
mRegisterProviderPresenter.onRegistrationFailed(new ErrorBundle(ErrorBundle.FIREBASE_ERROR, exception.getLocalizedMessage()));
}
})
.addOnSuccessListener(authResult -> {
Timber.d("Success: signInCred");
FirebaseUser firebaseUser = authResult.getUser();
/**
* Store the user details only for first time registration
* and not while acc linking
*/
storeUserCredentials(firebaseUser);
AuthenticationHelper.logUserDetails(firebaseUser);
mRegisterProviderPresenter.onRegistrationSuccess(mAlreadyRegistered);
});
Hope someone can come up with some help.
Facebook is a social identity provider and it doesn't own the emails. If an email is hacked, Facebook can't detect it and disable the account registered by this email. While Google is an email provider, its accounts are considered to be more secure.
Based on this theory, scenario 2 is different from 1. In scenario 1, the user has proved the ownership of this email by signing with Google first. So the user is allowed to add Facebook account using the same email. In scenario 2, Facebook sign in happens first and this provider record is untrusted, so it's removed when user signs in with another trusted provider.
Your code behavior is correct in both scenarios.
I faced the same issue and this is a supplemental answer for the question in the comment i.e.
Why is that after initially registering with a email & password, and then with Google, Google still replaces it?
I did some more exploration and found the answer here.
Pasting the relevant snippet.
If there is an existing account with the same email address but created with non-trusted credentials (e.g. non-trusted provider or password), the previous credentials are removed for security reason. A phisher (who is not the email address owner) might create the initial account - removing the initial credential would prevent the phisher from accessing the account afterwards.
The solution to handle this, i.e. to prevent Google from replacing the existing provider with Google, is to verify the email of the user.
So, after the user creates the account with email & password, or logs in with Facebook (or any other provider), send an email verification link to the user.
After the user verifies his/her email, then the subsequent Sign-in with Google will NOT replace the existing providers.
just use the email and password auth for the moment or a 3rd party plugin no solution so far