How to force logout firebase auth user from app remotely - firebase

I have a project which uses firebase auth with firebaseUI to authenticate users. I have enabled Google, Facebook and email providers. What I need is to remotely logout or disable some of the users.
I want the users to logout from the app on doing so. I tried disabling the user in the firebase console and also used the firebase admin SDK (https://firebase.google.com/docs/auth/admin/manage-sessions) to revoke the refresh tokens.
I waited for more than 2 days and still noticed that the user was logged in and could access the firestore data.
I have also gone through and tried
Firebase still retrieving authData after deletion
Can anyone point to what I am doing wrong ?

You also cannot remotely force a user to be signed out. Any sign out will have to happen from the device that the user is signed in on.
There is no way to revoke an access token once that is minted. This means that even if you disable the user's account, they may continue to have access for up to an hour.
If that is too long, the trick (as also mentioned in my answer to the question you linked) is to maintain a list of blocked users in your database (or elsewhere) and then check against that in your security rules (or other authorization layer).
For example in the realtime database, you could create a list of blocked user's UIDs:
banned_uids: {
"uid1": true
"uid2": true
}
And then check against that in your security rules with:
".read": "auth.uid !== null && !root.child('banned_uids').child(auth.uid).exists()"

You can send a message data with FCM to force to log out.
For example, if the users use android application.
Save the FCM token in a collection in firebase Realtime.
configure the Android client app, in the service. LINK You have to make when receive a message with especial string, force to log out.
make the trigger you need in cloud functions, to send the data LINK when you need the user log out.
SUCCESS!

As per your scenarios, i assume that you need to make user logout when user is disabled.
Use One global variable to store TokenNo (might be in shared preference or sqlite):
Add following code to your manifest:
<service android:name=".YourFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Add following code in your
public class LogoutOntokenchange extends FirebaseMessagingService{
#Override
public void onNewToken (String token){
if(TokenNo=>1){ //if tokenNo >=1 means he already logged in
TokenNo=0;
FirebaseAuth.getInstance().signOut(); //Then call signout method
}
else{
TokenNo=1; //store token no in db
}
}
}
What Happens here:
When user logged in first time onNewToken is called then It goes into else then TokenNo is updated to 1 from 0.
When You disable any user then automatically token is refreshed.Then OnNewToken is called then TokenNo>=1 so user will be logged out.
NOTE: When user log in for first time i.e if TokenNo variable is not stored then store it as 0.
For reference: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService

The only way I can think about is adding a if-else block in your starting activity.
Store the that status of user (verified/banned/deleted) in Firebase Real-time database. Then retrieve the status of user at start of application and add the code:
if (currentUserStatus.equals("banned"))
{
currentUser.logout();
}

What I've done is I created for each user upon registration a Firestore document with the UID as document ID. In this document I store an array which stores all fcm tokens the individual user receives when logging into a new device. That way I always keep track where the user is logged in. When the user logs out manually the fcm token will be deleted from the document in Firestore as well as on the device.
In order to be able to log out the user everywhere they are signed in I did the following. When starting the app and once the user is logged in I start a snapshot listener that listens to all changes in the users document. As soon as there is a change I retrieve the new array of fcm tokens, search inside the array for the local current device fcm token. If found, I do nothing. If the fcm token is no longer in the array I will call the local logout method and go back to the login screen.
Here are the methods I used in swift on iOS. The closures (passOnMethod) will just trigger an unwind segue to the login view controller.
import Foundation
import Firebase
class FB_Auth_Methods {
let db = Firestore.firestore()
var listener: ListenerRegistration?
func trackLoginStatus(passOnMethod: #escaping () -> () ) {
listener?.remove()
if let loggedInUserA_UID = Auth.auth().currentUser?.uid {
listener = db.collection(K.FStore.collectionOf_RegisteredUsers_Name)
.document(loggedInUserA_UID)
.addSnapshotListener { (snapshotDocument, error) in
if let error = error {
print(error)
} else {
if let document = snapshotDocument {
if let data = document.data() {
if let fcmTokens = data[K.FStore.Users.fcmTokens] as? [String] {
print("Found the following tokens: \(fcmTokens)")
self.compareTokensAgainstCurrentDeviceToken(fcmTokens: fcmTokens, passOnMethod: { () in
passOnMethod()
})
}
}
}
}
}
}
}
func compareTokensAgainstCurrentDeviceToken(fcmTokens: [String], passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
if fcmTokens.contains(result.token) {
print("Token found, doing nothing")
} else {
print("Token no longer found, logout user")
do {
try Auth.auth().signOut()
InstanceID.instanceID().deleteID { error in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
} catch let signOutError as NSError {
print (signOutError)
}
}
}
}
}
}
And here is the method I use when logging out the user everywhere but at the current device.
func deleteAllFcmTokensExceptCurrent(loggedInUserA: User, passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
let batch = self.db.batch()
let deleteAllFcmRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.delete()], forDocument: deleteAllFcmRef)
let updateFcmTokenRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.arrayUnion([result.token])], forDocument: updateFcmTokenRef)
batch.commit { (error) in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
}
}
}

Not tested yet, as our backend programmer, who is in charge of setting up Firestore rules was gone for the day, but in theory this should work: (and it's something I'll test tomorrow)
Having a FirebaseAuth.AuthStateListener in charge of serving UI based on the status of the user
This combined with rules in firestore
match /collection
allow read: if isAuth();
Where isAuth is:
function isAuth() {
return request.auth.uid != null;
}
If the user is then disabled, while being logged in, whenever the user tries to read data from the collection, he should be denied, and a signOut() call should be made.
The AuthStateListener will then detect it, and sign the user out.

Related

Is there a way to generate a firebase email verification link before a user is actually signed up?

I am currently implementing a MFA system with Firebase Authentication & Google Authenticator.
Since my users are not allowed to authenticate with a non-verified email address, I'd like to prevent them from signing-in if their Firebase Authentication email_verified is set to false. To do that, I am using Google Cloud Identity Provider blocking functions, this works perfectly.
However, when it comes to the registration beforeCreate blocking function hook, I can't find a way to generate an email verification link for the user currently being created, the documentation says:
Requiring email verification on registration The following example
shows how to require a user to verify their email after registering:
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
const locale = context.locale;
if (user.email && !user.emailVerified) {
// Send custom email verification on sign-up.
return admin.auth()
.generateEmailVerificationLink(user.email)
.then((link) => {
return sendCustomVerificationEmail(
user.email, link, locale
);
});
}
});
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
}
});
However, as far as I understand, generateEmailVerificationLink() can only be called to generate email verification link of an existing Firebase Authentication user. At this stage (while running beforeCreate blocking function), the user is not created yet.
Now I am wondering, I am missing something or is the Google documentation wrong?
No.
User data is created upon registration in the database.
Then, you may send an Email-Verification with a link automatically.
This Email-Verification just updates the field emaiVerified of said user data.
If you want to prevent users with unverified Emails from logging in, you need to adjust your Login page and check whether emaiVerified is true.
Important: Google will sign in a user right upon registration whether the email is verified or not, as this is the expected behavior from the perspective of a user. Email verification is ensured on the second, manual login.
(Also, please do not screenshot code.)
You can let a user sign in via email link at first, and call firebase.User.updatePassword() to set its password.
I am using Angular-Firebase, this is the logic code.
if (this.fireAuth.isSignInWithEmailLink(this.router.url)) {
const email = this.storage.get(SIGN_IN_EMAIL_KEY) as string;
this.storage.delete(SIGN_IN_EMAIL_KEY);
this.emailVerified = true;
this.accountCtrl.setValue(email);
from(this.fireAuth.signInWithEmailLink(email, this.router.url)).pipe(
catchError((error: FirebaseError) => {
const notification = this.notification;
notification.openError(notification.stripMessage(error.message));
this.emailVerified = false;
return of(null);
}),
filter((result) => !!result)
).subscribe((credential) => {
this.user = credential.user;
});
}
const notification = this.notification;
const info = form.value;
this.requesting = true;
form.control.disable();
(this.emailVerified ? from(this.user.updatePassword(info.password)) : from(this.fireAuth.signInWithEmailLink(info.account))).pipe(
catchError((error: FirebaseError) => {
switch (error.code) {
case AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY.POPUP_CLOSED_BY_USER:
break;
default:
console.log(error.code);
notification.openError(notification.stripMessage(error.message));
}
this.requesting = false;
form.control.enable();
return of(null);
}),
filter((result) => !!result)
).subscribe((result: firebase.auth.UserCredential) => {
if (this.emailVerified) {
if (result.user) {
notification.openError(`注册成功。`);
this.router.navigateByUrl(this.authService.redirectUrl || '');
} else {
notification.openError(`注册失败。`);
this.requesting = false;
form.control.enable();
}
} else {
this.storage.set(SIGN_IN_EMAIL_KEY, info.account);
}
});
Mate, if database won't create a new user using his email and password, and you send him email verification which will create his account, how the heck database will know his password? If it didn't create his account in the first step? Stop overthinking and just secure database using rules and routes in application if you don't want user to read some data while he didn't confirm email address.
It is that simple:
match /secretCollection/{docId} {
allow read, write: if isEmailVerified()
}
function isEmailVerified() {
return request.auth.token.email_verified
}
I think the blocking function documentation is wrong.
beforeCreate: "Triggers before a new user is saved to the Firebase Authentication database, and before a token is returned to your client app."
generateEmailVerificationLink: "To generate an email verification link, provide the existing user’s unverified email... The operation will resolve with the email action link. The email used must belong to an existing user."
Has anyone come up with a work around while still using blocking functions?
Using firebase rules to check for verification isn't helpful if the goal is to perform some action in the blocking function, such as setting custom claims.

Firebase Login and Login with Apple not linking to same user account

Using SwiftUI, Xcode12.5.1, Swift5.4.2, iOS14.7.1,
My Firebase-Login page shall be extended with other Login possibilities such as Apple-Login (eventually Google-login, Facebook-login etc).
I have an implementation of Firebase-Login that works well.
I extended the LoginView with the Sign in with Apple Button.
And this new Apple Login in its basic implementation also works.
Now the problem:
If I log in with Apple, I need to access the corresponding Firebase-user in order to query the correct user-data. Right now, login in with Apple works but the retrieved data is not the user-data of the corresponding Firebase-user.
What I want to achieve:
From a logout-state, I want to
a) Being able to log in with Firebase Email/Password and sometimes later want to log-out and log in again with Apple.
--> and for both cases, I would like to get the same user-data
b) Being able to log in with Apple and sometimes later want to log-out and log in again with Firebase Email/Password
--> and for both cases, I would like to get the same user-data
--- THE IDEA ----------
I learned from the Firebase documentation that there is a way to link two login-accounts that we are able to know that these two accounts are corresponding.
--- THE IMPLEMENTATION -----------
Below is my current implementation for the Apple login:
I learned that you can get userInformation of the corresponding other account in the error of the link-callback. But in my case, I get the wrong linkError:
My linkError:
The email address is already in use by another account.
Instead of:
AuthErrorCode.credentialAlreadyInUse
For me this doesn't make sense. Especially since I know that I already did log in before with Firebase-Email/Password. Then I logged out and now I tried to log in with Apple.
Shouldn't the link method recognise that I am allowed to have been logged in via Firebase-Email/Password before and shouldn't it be ok to have that email being used before ?? I don't understand this linkError.
Questions:
In the link-callback, why do I get the linkError The email address is already in use by another account. instead of AuthErrorCode.credentialAlreadyInUse ??
What do I need to change in order to make a) work ??
How does the implementation look for the b) workflow (i.e. if user logs in to Apple, then logs-out and logs in again with Firebase-Email/Password ??). How do I link the two accounts then ??
Here my code:
switch state {
case .signIn:
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("Error authenticating: \(error.localizedDescription)")
return
}
do {
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
if let user = authResult?.user {
user.link(with: credential) { (result, linkError) in
if let linkError = linkError, (linkError as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(user.uid)] tasks.")
if let updatedCredential = (linkError as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let user = result?.user {
// eventually do a data-migration
user.getIDToken { (token, error) in
if let _ = token {
// do data migration here with the token....
self.doSignIn(appleIDCredential: appleIDCredential, user: user)
}
}
}
}
}
}
else if let linkError = linkError {
// I END UP HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// WHY WHY WHY WHY WHY WHY WHY WHY ????????????????????????
print("Error trying to link user: \(linkError.localizedDescription)")
}
else {
if let user = result?.user {
self.doSignIn(appleIDCredential: appleIDCredential, user: user)
}
}
}
}
}
} catch {
print(error.localizedDescription)
}
if let user = authResult?.user {
if let onSignedInHandler = self.onSignedInHandler {
onSignedInHandler(user)
}
}
}
case .link:
// t.b.d.
case .reauth:
// t.b.d.
}

Facebook login in iOS (Swift 4) - Get permissions and store in Firebase

I've found some good resources on Stackoverflow and youtube helping getting around the fact that the Facebook iOS SDK descriptions are not up to date. I've now successfully managed to create the Facebook login feature and a new user is registered in Firebase. However my current issue is two fold.
1) I understand that Firebase do NOT store the email of the user under Authentication if the user log in with Facebook. My question is - how to get the email from Facebook so that I can store it under the users profile, and how do I ensure that a user that has signed in / logged in with Facebook one day and by email another day are the same user?
2a) The .userFriends info is a list of friends that also use the app - I'm struggling to understand what info that Facebook provide and how I can use this to suggest other friends the user can follow in the app.
I've read the Facebook SDK info! But can someone help translating this into what it means in terms of Swift 4...
2B) Not knowing the data structure - I'm thinking of storing .userFriends into a new node - but unsure if I should do it under the specific user profile, or denormalise it and put it under root with the user uid as the identifier... let me know your thoughts please..
Working code - issue is with the commented section
#objc func loginButtonClicked() {
let loginManager = LoginManager()
loginManager.logIn(readPermissions: [.publicProfile, .email, .userFriends], viewController: self) { loginResult in // request access to user's facebook details
switch loginResult {
case .failed(let error):
print(error)
case .cancelled:
print("User cancelled login.")
case .success(let grantedPermissions, let declinedPermissions, let accessToken):
print(grantedPermissions)
print(declinedPermissions)
// Check permissions granted and declined add do action depending... eg. get name and surname for profile
// if FacebookAccessToken.grantedPermissions = {
// // TODO: publish content.
// }
// else {
// var loginManager = LoginManager()
// loginManager.logIn(readPermissions: [.publicProfile], viewController: self) { loginResult in
// //TODO: process error or result.
// }
// }
FacebookAccessToken = accessToken
let credential = FacebookAuthProvider.credential(withAccessToken: (FacebookAccessToken?.authenticationToken)!)
Auth.auth().signIn(with: credential) {( user, error) in
if let error = error {
print(error)
return
}
let currentUser = Auth.auth().currentUser
// Navigates back
self.dismiss(animated: true, completion: {});
self.navigationController?.popViewController(animated: true);
print("Successfully logged in user with Facebook")
}
}
}
}

How to ensure Anonymous login with Firebase?

I'm working on a React Native application which use Firebase Auth uid to identify individual user instances.
My initial implementation called signInAnonymously on every starting up and it returned non-persistent uid.
Referring to Anonymous user in Firebase, my code became like this. it works as expected but still unclear why this code is correct.
static ensureLogin() {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
resolve(user)
} else {
firebase.auth().signInAnonymously()
.catch(function(error) {
reject()
})
}
})
}
To answer this question, let's check details in this official document.
https://firebase.google.com/docs/auth/web/manage-users
It has this example code:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
The latter comment line says "No user is signed in." instead of "User is signed out.".
It implies that it is triggered not only by user's sign in/out actions.
It also says below.
By using an observer, you ensure that the Auth object isn't in an
intermediate state—such as initialization—when you get the current
user.
This means that 2 facts.
Auth object has internal states
onAuthStateChanged observes the state of Auth object
So, we should notice that onAuthStateChanged can be triggered by any status change of Auth object including initialization! And calling signInAnonymously after init is nothing strange.
Once signInAnonymously in else block is called, it re-triggers onAuthStateChanged after signing in.
That's why the code is correct.

Firebase setValue on offline

I would to save user data into firebase when he signup, But there is a problem which is if he created Auth account and then lost connection before saving his info, That's mean he will get Email\Password without any profile info.
So the question is how to setValue again if he lost connection or exit the app?
FIRAuth.auth()?.createUser(withEmail: self.emailTxt.text!, password: self.passTxt.text!, completion: {
(user, error) in
if error != nil {
if let isError = error?.localizedDescription
{
print(isError)
} else {
print("Error: 33")
}
}
else if user != nil
{
// Email and password created
let userInfo: [String : Any] = ["timestamp": FIRServerValue.timestamp(),
"uid" : user!.uid,
"email" : self.emailTxt.text!,
// This info must be send again if its failed.
]
self.ref.child("users").child("profile").child((user?.uid)!).setValue(userInfo) {
(error, ref) in
if error != nil {
print("error set profile info")
// What should i do if its failed?
} else {
print("done creating user's profile")
}
}
}
}// Completion end.
You need to enable the Disk Persistence :
Database.database().isPersistenceEnabled = true
Firebase apps automatically handle temporary network interruptions. Cached data is available while offline and Firebase resends any writes when network connectivity is restored.
Here the Firebase Offline Capabilities documentation.
You have two options here:
Use disk persistence, which means the data will be written when the user comes back online. This is what Pipiks' answer explains.
Don't rely on the client-side to create the registration information, but use a Cloud Function.
For the second option you're using Cloud Functions for Firebase to write JavaScript code that is executed on Google's servers when the user account is created. See this example of sending a welcome email (that you'll have to modify to write to the data instead).

Resources