Firebase Phone Auth along with account linking - ios

I am having a problem with phone authentication credentials persistence/expiry.
My scenario is like this:
I have a guest user, that I want to link with a phone number. The flow works perfectly if an account is not registered with that phone number. But if it does exist, then I have to:
SignIn and Unlink account.
Link account.
Sign In.
This requires 3 different credentials. But credentials expire for phone authentication after it gets used once - as per my understanding from the error message :
The SMS code has expired. Please re-send the verification code to try
again.
I do not want to ask user 3 times in a row for verification code on his mobile so new credentials can be generated. Any way to make credentials stick or a way around this problem ?
I can share the code if needed but I do not think it would be of any help.

Here is what you should do:
Initialize the phone auth credential first. Try to link that credential to the guest account always. If it fails with an error "credential already in user", the error userinfo will contain a new credential. This credential can then be used to sign in to the existing phone number account. Here is an example in objective-c.
[[FIRAuth auth].currentUser linkWithCredential:credential
completion:^(FIRUser *_Nullable user,
NSError *_Nullable error) {
if (user) {
// Successfully linked credential.
return;
}
if (error.code == FIRAuthErrorCodePhoneAlreadyInUse) {
// Save guest user data.
// Sign in the user instead if applicable.
FIRAuthCredential *credential = error.userInfo[FIRAuthUpdatedCredentialKey];
[[FIRAuth auth] signInWithCredential:credential
completion:^(FIRUser *_Nullable user,
NSError *_Nullable error) {
// copy guest user data to existing phone number user.
}];
return;
}
// Other errors.
}];
You can then programmatically copy the data of the guest user to the existing user and delete the guest user.
All this can be done efficiently with one SMS sent.

Related

Firebase Anonymous Login - Why FirebaseID changes when logged to Facebook, but it remains the same when not logged to Facebook?

I have the following logic for allowing guest users to login to my app:
(1) Login as Anonymous.
(2) Check if Facebook is logged.
(3) If it is logged to Facebook, link to Anonymous.
(4) If link fails, Login to firebase passing facebook token to Firebase
If I am not logged in Facebook the Anonymous ID given by firebase after step (1) is always the same. However, the first time I login to Facebook, I link the account to firebase as in step (3). And from that onwards, I get a different Anonymous ID every time I go through the login process.
Question 1. Will the Anonymous ID in step (1) ALWAYS be the same until I login to Facebook for the first time?
Question 2. What is the best login flow to allow users to save data in the backend as guests, and link to facebook later when the user decides to do so?
Here is my swift code that implements my pseudo code:
func login() -> Promise<AuthCredential?> {
// Login as Anonymous. Check if FB is logged. If it is, link to Anonymous. If fails, pass FB token to Firebase
print("........................")
print("Starting Login")
return Promise { seal in
Auth.auth().signInAnonymously() { (authResult, error) in
print("-Done Login Anonymously", authResult?.credential, Auth.auth().currentUser?.uid)
if let error = error {
print("-DID NOT SIGNED UP WITH FIREBASE:", error)
seal.reject(error)
} else {
if AccessToken.isCurrentAccessTokenActive { // If Facebook token is active exchange for Firebase
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
print("-Loggged with FACEBOOK!!!!", credential)
Auth.auth().currentUser!.link(with: credential) { (result, error) in
print("-Linked Account!!!!??????????", result as Any, error as Any)
if error != nil {
Auth.auth().signIn(with: credential) { (authResult, error) in
print("-Signed UP in Firebase USIG FB. No Linking")
if let error = error {
print("-DID NOT SIGNED UP WITH FIREBASE:", error)
seal.reject(error)
} else {
print("-DID SIGNED UP WITH FIREBASE using FB:", Auth.auth().currentUser?.uid)
seal.fulfill(authResult?.credential)
}
}
}
}
}
// print("-Done Login", Auth.auth().currentUser?.uid)
// seal.fulfill(authResult?.credential)
}
}
}
}
The API documentation for signInAnonymously (javascript) reads:
If there is already an anonymous user signed in, that user will be
returned; otherwise, a new anonymous user identity will be created and
returned.
You probably only want to call signInAnonymously if there is no user signed into the app. It's best to wait to see if a user is already signed in using an auth state listener, as the sign-in process is not immediate.
Once you link the anonymous account with a full account, you should probably not call signInAnonymously again, since you probably want the user to stay signed in with their full account and no create another new anon account.

iOS ADAL-Make silent call using refresh token

I am using iOS ADAL library version 2.2.6 and receiving refresh token upon successful login. Now I want to make a silent call by using this refresh token. I tried with following method but it fails to return the access token.
ADAuthenticationContext *authContext;
[authContext acquireTokenSilentWithResource:resourceId
clientId:clientId
redirectUri:redirectUri
userId:strUserID //loggedIn userID
completionBlock:^(ADAuthenticationResult *result){
// It alway throws an error //Please call the non-silent acquireTokenWithResource methods.
if(result.error){
ADAuthenticationError *error = nil;
authContext = [ADAuthenticationContext authenticationContextWithAuthority:inputData.authority error:&error];
[authContext acquireTokenWithResource:inputData.ResourceID
clientId:inputData.ClientId // Comes from App Portal
redirectUri:inputData.RedirectUri // Comes from App Portal
completionBlock:^(ADAuthenticationResult *result)
{
if (AD_SUCCEEDED != result.status){
// Show alert with error description
}
else{
//Handle Success token
}
}];
}else{
//Handle Success token
}
}];
But it always throws an error saying "The user credentials are needed to obtain access token. Please call the non-silent acquireTokenWithResource methods."
Is there any way to make a silent call using refresh token? please help me on it. Thanks in advance.
When you use Microsoft's authentication libraries, you should always first check to see if there is a user in the cache that can be used for your resource before prompting the user to sign in. This allows us to check if the user had previously signed in to your app or if there are other apps that share state with your app that may have already asked the user to sign in elsewhere.
If the user is found, we will try to acquire a token without interrupting the user at all. Sometimes a user will have changed their password or done some other action that will require them to sign in again even if they have signed in to your app previously. This is what you are seeing. The library is telling you that for the user you are trying to acquire a token for, they need to sign in again to make something right.
In order to handle all these cases elegantly, we recommend that you use the pseudocode pattern of:
acquireTokenSilent()
(if error InteractiveAuthenticationRequired) {
acquireTokenInteractively() }
The pattern first checks if a user you specify is available in the token cache. If it is, we then call the Azure Active Directory service to see if the Refresh token for that user is valid. If both of these are true, then the user is signed in silently. If the user isn't found or the server rejects the Refresh Token, then an error is sent from the library that indicates the user needs to sign in interactively.
In the above, you are doing this first part, but you aren't handling the case where the user needs to sign in if there is a problem.
The best way is to catch the error with a ADErrorCode of AD_ERROR_USER_INPUT_NEEDED
Here is a code sample on how to do this pattern.
// Here we try to get a token from the stored user information we would have from a successful authentication
[authContext acquireTokenSilentWithResource:data.resourceId
clientId:data.clientId
redirectUri:redirectUri
userId:data.userItem.userInformation.userId
completionBlock:^(ADAuthenticationResult *result) {
if (!result.error)
{
completionBlock(result.tokenCacheStoreItem.userInformation, nil);
} else {
if ([result.error.domain isEqual:ADAuthenticationErrorDomain] && result.error.code == AD_ERROR_USER_INPUT_NEEDED) {
// Here we know that input is required because we couldn't get a token from the cache
[authContext acquireTokenWithResource:data.resourceId
clientId:data.clientId
redirectUri:redirectUri
userId:data.userItem.userInformation.userId
completionBlock:^(ADAuthenticationResult *result) {
if (result.status != AD_SUCCEEDED)
{
completionBlock(nil, result.error);
}
else
{
data.userItem = result.tokenCacheStoreItem;
completionBlock(result.tokenCacheStoreItem.userInformation, nil);
}
}];
} else {
completionBlock(nil, result.error);
}
}
}];
Keep in mind this code is very verbose. You will most likely want to have acquireTokenWithResource: a separate method that you could call with [self acquireTokenWithResource]

Unable to login via Facebook (Parse): error code 251

I have an app in the App Store which is using Parse and automatically creates anonymous users (I set [PFUser enableAutomaticUser]). So now I have several thousands of users in a Parse users table.
Now I'm trying to implement full profiles and convert anonymous users to non-anonymous by calling:
[PFFacebookUtils linkUser:currentUser permissions:#[#"public_profile", #"email", #"user_friends"] block:^(BOOL succeeded, NSError *error) {
if (succeeded) {
//some logic
} else {
NSLog(#"Error: %#", error);
}
}];
BTW I've also tried:
[PFFacebookUtils logInWithPermissions:#[#"public_profile", #"email", #"user_friends"] block:^(PFUser *user, NSError *error) {
if (!user) {
NSLog(#"Uh oh. The user cancelled the Facebook login.");
} else if (user.isNew) {
NSLog(#"User signed up through Facebook!");
} else {
NSLog(#"User logged in through Facebook!");
}
}];
But in both cases I get an error: 251 - The supplied Facebook session token is expired or invalid.
What I've already tried:
Logout anonymous user
Refresh session
Close and re-auth session
Nothing here works for me. Does anyone know what to do in this case? How do I correctly convert anonymous user to non-anonymous?
I'm pretty sure the problem is your Facebook App configuration.
Please go to your Facebook App settings (Advanced section) https://developers.facebook.com/apps/YOUR_FB_APP_ID/settings/advanced/
You should have enabled the option "Native or desktop app?" When you do so, you'll have other option with the message "Is your App Secret embedded?" The error 251 on iOS only comes up when you enable this second option because your app secret is not embedded so the token is invalid.
Please go to your settings and make sure the option "Is your App Secret embedded?" is NOT enabled.
I hope it helps.
verify your fb dev account at "Approved Items" > "Login Permissions" must be the same as you requesting in your permissions array in your code. In my case I am using the new API 2.0.
Hope you help it.
Regards from Cancun
The problem for me was that i copied the App Secret when it was in dots, click on the show button in your Developer Settings and copy the string in there on to Parse.
Hope it helps.

Anonymous user persists in database after successful log in

Using Parse.com and anonymous users enabled (w/ automatic creation) - this is the scenario:
Start the app - anonymous user #1 gets created on database
Sign up with username & password - anonymous user #1 gets converted to regular user
Delete app - reinstall & start up - anonymous user #2 gets created
This time do not sign up but rather log in - log in successful, but anonymous user #2 persists in cloud.
How do I make anonymous user #2 from step 4 to go away (completely, I don't want junk in the database)?
As long as the user is otherwise logged out and you therefore know that currentUser either contains nil or the anonymous user object, you should be able to store the anonymous user object then delete that user object upon successful login, ex:
PFUser *anonymousUser;
if ([PFUser currentUser] != nil) {
anonymousUser = [PFUser currentUser];
}
[PFUser logInWithUsernameInBackground:emailString password:passwordString block:^(PFUser* user, NSError* error){
if (user) {
if (anonymousUser)
[anonymousUser deleteInBackground];
}
}

How can I authorize more than one Dropbox account in an app?

I'm looking to update one of my apps (which is a Dropbox client) to have support for multiple accounts, but I can't seem to find a way to do it.
I have analyzed the SDK many times and no matter how many times I look at, it looks like an account using the official SDK can only support one account at a time. Although I'm sure it can support more as I know of many apps that allow you to link more than one.
Any pointers on doing this will be highly appreciated. I can't even find a way to fetch tokens to store them separately later.
I found this to be a challenge but finally made it work after lots of experimentation. Here are some bits of information that should help:
Each Dropbox (DB) account has a userid (uid) associated with it once the user has been authorized. In your own app's model for an account, you need to keep track of the uid. Initially, before the user links their DB account, this uid will be nil.
When the user wants to access their DB account, you get your associated uid for the account. If the uid isn't nil you setup the DBRestClient as follows:
_client = [[DBRestClient alloc] initWithSession:[DBSession sharedSession] userId:uid];
If the uid isn't set yet, you need to present the login screen.
[[DBSession sharedSession] linkFromController:someController];
This, of course, launches the DB app to present the login (or presents a web interface if the DB app isn't installed). Either way, your app will be launched again by DB when the user finishes the authorization process.
In your app delegate's application:openURL:sourceApplication:annotation: method you do something like:
if ([[DBSession sharedSession] handleOpenURL:url]) {
NSString *query = url.query;
if ([[url absoluteString] rangeOfString:#"cancel"].location == NSNotFound) {
NSDictionary *urlData = [DBSession parseURLParams:query];
NSString *uid = [urlData objectForKey:#"uid"];
if ([[[DBSession sharedSession] userIds] containsObject:uid]) {
// At this point we know the login succeeded and we have the newly linked userid
// make a call to process the uid
}
} else {
// user cancelled the login
}
}
In the code that processes the newly linked uid, you can store the uid in your own account data model. Then you use the uid to create the DBRestClient like I showed earlier.
If you have a uid, you can determine if the uid is properly linked with a simple check:
if ([[[DBSession sharedSession] userIds] containsObject:uid]) {
// the uid is linked
}
To unlink a user based on their uid you can do:
[[DBSession sharedSession] unlinkUserId:uid];
At that point I would also clear out the saved uid from your own account model.
Hopefully that is enough pieces to build the puzzle. Good luck.

Resources