Firebase iOS user authentication: avoiding getting logged out of app - ios

Throughout my app, I am using two ways of getting the current userID at any given time. Both of them I have picked up somewhere and (as far as I can tell) work largely fine. And then I use Method 3 as a variation of Method 2.
Method 1:
if let user = Auth.auth().currentUser{
let uid = user.uid
}
Method 2:
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// User is signed in.
self.USER = user
self.userID = self.USER?.uid
} else {
// No user is signed in.
let vc = self.storyboard?.instantiateViewController(withIdentifier: "LoginScreen")
self.present(vc!, animated: true, completion: nil)
}
Method 3:
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// User is signed in.
self.USER = user
self.userID = self.USER?.uid
} else {
// No user is signed in.
}
Now it seems that Method 1 and 3 work largely equivalently, while Method 2 sends me back to the Login screen more often (e.g. when the phone goes from 3G to Wifi or flight mode).
Given that I would like my app to stay logged in for long (even when going to the background and coming back) that would suggest using Method 1 or 3. However, I don't quite understand
the difference between Methods 1 and 3
what is the app supposed to be doing when 1 and 3 cannot establish a connection? Is it supposed to freeze until a connection is re-established? I worry Methods 1 and 3 might be more prone to crashes. But Method 2 looks out annoyingly often.
Generally, once a user has authenticated correctly with Firebase, is there ever a reason for the app to go back to a Login screen? Can the user not stay logged in for arbitrary periods of time (as e.g. a Facebook would)? If so, with which method can I achieve that?

Just picking up on a couple of points you have raised first, you mention the app freezing when method 2 and 3 are called. Where are you calling these? You need to ensure that these are run on the main thread so that they don't interfere with your UI given it's an asynchronous function.
Also, are you removing the state change listener once logged in? Within your login VC you could have:
deinit {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
Furthermore, you can use the GIDSignIn.sharedInstance().signInSilently() method.
Take a look at the iOS friendly chat sample, they have a good login flow and handle the user already being logged in quite well. https://codelabs.developers.google.com/codelabs/firebase-ios-swift/#0

Related

Swift - Firebase Authentication State Persistence

I'm currently thinking about implementing Firebase Auth to my Swift project, hence I've been reading some articles. - Namely among others this one.
I need some help understanding the given article. It's about "Authentication State Persistence". Does this mean, that if the value is set to local, the user will stay logged in even after closing the app? In other words, will he be able to sign up once and stay logged in until he decides to log out - even when he's offline?
Let's say a user decides not to create an account and logs in with "Anonymous Authentication" (I assume this is the type of login in this kind of case) - will he stay logged in forever as well or is there a danger of data loss, in case of going offline or closing the app?
First: the link you provided refers to a javascript firebase documentation
Second: the only thing available in IOS is you can create an anonymous user with
Auth.auth().signInAnonymously() { (authResult, error) in
// ...
let user = authResult.user
let isAnonymous = user.isAnonymous // true
let uid = user.uid
}
and you can convert it to a permanent user check This
Finally: whether the user is usual / anonymous , after you sign in you need to check this to show login/home screen every app open
if FIRAuth.auth()?.currentUser != nil {
print("user exists")
}
else {
print("No user")
}
and the user still exists unless you sign out regardless of whether you closed the app or not
If you are using the latest Firebase version, FIRAuth is now Auth:
if Auth.auth()?.currentUser != nil {
print("user exists")
}
else {
print("No user")
}

Do authenticated user stay signed in when i just terminate app without signing out?

I am novice in Firebase, and I an currently trying to handle (upon loading app) if user is logged in.
So according to a book i have to check it like that:
if Auth.auth().currentUser?.uid == nil {
//do smth
}
So as it turns out(if i am not missed something and not wrong) it only checks the last user in Authentication section of a console. I mean if i registered user that way:
Auth.auth().createUser(withEmail: email, password: password) { (User, Error) in
if Error != nil {
print(Error)
return
}
And just terminate my app,then during my check it will see only the last registered user. And after detecting this signed in user and if i sign out then app won't see rest signed in users in the next checking procedure.
So it seems like:
I ran app then create user several times (create then terminate app and again create then terminate app and back again)
Then after step 1 and check if user signed in. Is this last user or 1st user i don't know because i did not found and detail info about it .
if Auth.auth().currentUser?.uid == nil {
//do sign out
do {
try Auth.auth().signOut()
} catch let logoutError {
print(logoutError)
}
}
It finds signed in user then i sing out this user.
I terminate app.
I run this app again and during initial check of signed in users it does not found any of them.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(handleLogout))
//user is not logged in
if Auth.auth().currentUser?.uid == nil {
signOut()
}
}
Why? I mean I did not sign out by myself the rest registered users, and I know upon calling createUser func, this createUser func also signs user in.
Maybe app signs out user if i create new? then why i did not found any info about this behavior( Please, if someone have any clue i would appreciate it.
Firebase Authentication users stay signed in until:
either your code signs them out explicitly
or you create another user in the same app on the same device
or until an event happens that requires them to sign in again (such as a password change)
A single app on a single device can only have a single signed in user. If you sign in (or create) another user on the same device, the previous user will be signed out.

Ondisconnect is fired if app goes to background mode

I have the following code:
func OnlineStatus(userID: String){
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// User is signed in.
self.UID = user.uid
self.connectedRef.observe(.value, with: { snapshot in
if let connected = snapshot.value as? Bool, connected {
// print("############################ Connected")
self.ref.child(self.UID!).child("OnlineStatus").setValue("ON")
} else {
// print("############################ Not connected")
self.ref.child(self.UID!).child("OnlineStatus").setValue("OFF")
}
self.ref.child(self.UID!).child("OnlineStatus").onDisconnectSetValue("OFF")
})
}}
}
The function will be triggered in viewWillAppear. The idea is to build a simple presence system. For some reason onDisconnect gets fired when I send the app to background and than send my iPhone to sleep. I actually would like that online status goes to off only when user logs out or looses internet connection. What is wrong with my code or settings?
The onDisconnect event fires when the client disconnects from the Firebase Database servers, and that happens when your app goes to the background. There is no difference from Firebase's perspective between the user being on train that drives into a tunnel, and their phone going to sleep. In both cases the connection between the client and the server gets dropped, so the onDisconnect() fires.
You'll typically end up using .info/connected and onDisconnect() to set a value of when the user was last seen, while using onAuthStateChanged() to set a status flag of the user being signed in. Then you show the list of users by first showing users that are signed in, in the order of how recently they were active.

Checking Firebase current signed-in user via Listener in iOS

I've implement Firebase Authorization to login on my iOS app via Facebook and Google. I'm coding Swift.
When the app starts up I need to check whether a user is already signed-in in order to present the proper ViewController (e.g., if nobody is signed in I present the Login View Controller, otherwise I present the Home View Controller).
If I use the "easy" solution offered by Firebase, meaning
if FIRAuth.auth()?.currentUser != nil {
// User is signed in.
// ...
} else {
// No user is signed in.
// ...
}
So checking if the current user is not nil, it happens exactly what the Firebase guide (https://firebase.google.com/docs/auth/ios/manage-users) alerts might happen meaning
"Note: currentUser might also be nil because the auth object has not finished initializing. If you use a listener to keep track of the user's sign-in status, you don't need to handle this case."
So I would like to implement the listener as suggested in the guide:
handle = FIRAuth.auth()?.addStateDidChangeListener() { (auth, user) in
// ...
}
The listener will handle also intermediate status so that it is triggered when the Auth object is created. Point is I really cannot make it to work properly. Anybody can help me to use this listener in order to check if a user is logged in?
Thanks
I've implemented it like this:
FIRAuth.auth()?.addStateDidChangeListener { auth, user in
if let user = user {
// User is signed in. Show home screen
} else {
// No User is signed in. Show user the login screen
}
}
If you don't need the User object after checking, you can replace if let user = user with a boolean test, like this:
FIRAuth.auth()?.addStateDidChangeListener { auth, user in
if user != nil {
// User is signed in. Show home screen
} else {
// No User is signed in. Show user the login screen
}
}
Where to put the listener (from the comments):
For the cases I used to check if a user is signed in, it was enough to put it at the beginning of viewDidLoad in the specific view controller. But if you have any cases where you need to check every time you enter the specific view controller then it would be better to put it at the beginning of viewDidAppear. But I think in most cases you need to check only once, if the user enters the view
If you're setting up the StateDidChangeListener in application:didFinishLaunchingWithOptions, you'll notice that it fires once when the listener is attached (to set the initial state, which is nil when initialising) and then again once it's finished initialising (potentially not nil). This is intended behaviour, but really impractical if you're setting it up early.
An alternative to using the listener is using NotificationCenter. This will fire once initialisation has finished:
NotificationCenter.default.addObserver(forName: NSNotification.Name.AuthStateDidChange, object: Auth.auth(), queue: nil) { _ in
let user = Auth.auth().currentUser
}

iOS - AWS Cognito - Check if user already exists

I want to allow a user to enter their email address/password in a field. Upon continuing, I want to run a check to see if that user already exists. If they do, log them in and continue with app, if they do not, move to account creation flow where they will be instructed to add name, phone number, etc.
I cannot for the life of me find documentation on how to log a user in using AWS Cognito. I should be able to pass email/pass in a call and get a response back that says User Exists/User does not exist or whatever! Am I missing something here?
Any help would be greatly appreciated. I've scoured the documentation..this is my last resort.
In the current SDK, calling getUser on your AWSCognitoIdentityUserPool just constructs the in-memory user object. To make the call over the network, you need to call the getSession method on the constructed user. Here's a Swift 3 method I wrote to check whether an email is available:
/// Check whether an email address is available.
///
/// - Parameters:
/// - email: Check whether this email is available.
/// - completion: Called on completion with parameter true if email is available, and false otherwise.
func checkEmail(_ email: String, completion: #escaping (Bool) -> Void) {
let proposedUser = CognitoIdentityUserPoolManager.shared.pool.getUser(email)
UIApplication.shared.isNetworkActivityIndicatorVisible = true
proposedUser.getSession(email, password: "deadbeef", validationData: nil).continueWith(executor: AWSExecutor.mainThread(), block: { (awsTask) in
UIApplication.shared.isNetworkActivityIndicatorVisible = false
if let error = awsTask.error as? NSError {
// Error implies login failed. Check reason for failure
let exceptionString = error.userInfo["__type"] as! String
if let exception = AWSConstants.ExceptionString(rawValue: exceptionString) {
switch exception {
case .notAuthorizedException, .resourceConflictException:
// Account with this email does exist.
completion(false)
default:
// Some other exception (e.g., UserNotFoundException). Allow user to proceed.
completion(true)
}
} else {
// Some error we did not recognize. Optimistically allow user to proceed.
completion(true)
}
} else {
// No error implies login worked (edge case where proposed email
// is linked with an account which has password 'deadbeef').
completion(false)
}
return nil
})
}
For reference, my ExceptionString enum looks like this:
public enum ExceptionString: String {
/// Thrown during sign-up when email is already taken.
case aliasExistsException = "AliasExistsException"
/// Thrown when a user is not authorized to access the requested resource.
case notAuthorizedException = "NotAuthorizedException"
/// Thrown when the requested resource (for example, a dataset or record) does not exist.
case resourceNotFoundException = "ResourceNotFoundException"
/// Thrown when a user tries to use a login which is already linked to another account.
case resourceConflictException = "ResourceConflictException"
/// Thrown for missing or bad input parameter(s).
case invalidParameterException = "InvalidParameterException"
/// Thrown during sign-up when username is taken.
case usernameExistsException = "UsernameExistsException"
/// Thrown when user has not confirmed his email address.
case userNotConfirmedException = "UserNotConfirmedException"
/// Thrown when specified user does not exist.
case userNotFoundException = "UserNotFoundException"
}
Some clarification is in order. Cognito has several parts. The part that does "Authentication" (which is what you are talking about) is called "Cognito User Pools". Not to be confused with Cognito Federated Identity Pools.
With User Pools you can create usernames and password combinations with attributes, and these can be used to authenticate and deliver a persistent, cross device, Cognito Federated identity identityId to a user (across multiple devices).
Once logged in, the Federated Identity Pool is hooked to roles which can get your "Authorized" to use AWS services (like Dynamo DB etc).
It can be tricky to get all these parts working together and AWS has an online site called "Mobile Hub" that will build code for you and download an xcode project. This process configures the Federated Identity Pool and the User Pool correctly, and connects them all up to a set of example code.
Connecting the credentials provider to the user pool to the identity pool is a bit counterintuitive, but the AWSIdentityManager in the aws-mobilehub-helper-ios on github manages all that for you. So I would recommend starting with mobile hub on the console.
Cognito is a somewhat confusing system, here is a link to a brief powerpoint that hits the highlights of how it works (for people that can't understand the AWS docs (like me)).
With that said, "how to check if a user already exists?"
The most reasonable approach is to create the user (via signup), and get a reject if the name is in use, and suggest that your user try a different username. With respect to the email being in use, you will get that reject upon confirmation (signup sends confirmation id's by email and/or via text). This can be overridden to reclaim the email address, or you can do a test beforehand to see if the email is in use by attempting to log in and looking at the failure code.
you can fetch the user as the other answer suggests, however if you have established in user pools an alias for login (like email) you will find this problematic, because this just tells you if someone has the user name, not if someone is already using the email address, and you will get a reject later at confirmation time.
ListUsers is now a nice way to check for existing usernames.
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html
You can also look for existing emails, phone numbers, and other default attributes.
Here is a simple .NET example:
Dim userRequest = New ListUsersRequest With {
.UserPoolId = "poolId",
.Filter = "username = bob#email.com"
}
Dim response = amazonCognitoIdentityProviderClient.ListUsers(userRequest)
Debug.WriteLine(response.Users.Count)

Resources