(Note: this is related to this question I posted, but since my original question was answered and I am now encountering a different issue, I am posting this as a new question.)
I am setting up registration and login for an iOS app which uses DynamoDB and AWS Cognito. I eventually got the registration login process to work, but I've noticed that whenever I log out and then immediately try to log back in, the app fails to do so and I get the error message Invalid login token. Can't pass in a Cognito token. Only after I close and relaunch the app can I successfully log in again.
I primarily used this example project to set up registration, but when I was implementing the sign-in method, I had some trouble converting from Objective-C to Swift. I wasn't able to get the login process from the example to work, so I instead set up an explicit login method:
if locked { return }
trimRegistrationValues()
let name = usernameField.text!
let user = pool!.getUser(name)
lock()
user.getSession(name, password: passwordField.text!, validationData: nil, scopes: nil).continueWithExecutor(AWSExecutor.mainThreadExecutor(), withBlock: {
(task:AWSTask!) -> AnyObject! in
if task.error != nil {
self.sendErrorPopup("ERROR: Unable to sign in. Error description: " + task.error!.description)
} else {
print("Successful Login")
let loginKey = "cognito-idp.us-east-1.amazonaws.com/" + USER_POOL_ID
var logins = [NSString : NSString]()
self.credentialsProvider!.identityProvider.logins().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("ERROR: Unable to get logins. Description: " + task.error!.description)
} else {
if task.result != nil{
let prevLogins = task.result as! [NSString:NSString]
print("Previous logins: " + String(prevLogins))
logins = prevLogins
}
logins[loginKey] = name
let manager = IdentityProviderManager(tokens: logins)
self.credentialsProvider!.setIdentityProviderManagerOnce(manager)
self.credentialsProvider!.getIdentityId().continueWithBlock { (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("ERROR: Unable to get ID. Error description: " + task.error!.description)
} else {
print("Signed in user with the following ID:")
print(task.result)
dispatch_async(dispatch_get_main_queue()){
self.performSegueWithIdentifier("mainViewControllerSegue", sender: self)
}
}
return nil
}
}
return nil
}
}
self.unlock()
return nil
})
Currently, the code in my AppDelegate class for setting up Cognito looks like this:
let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId:APP_CLIENT_ID, clientSecret: APP_CLIENT_SECRET, poolId: USER_POOL_ID)
let pool = AWSCognitoIdentityUserPool(forKey:USER_POOL_NAME)
pool.delegate = self
self.storyboard = UIStoryboard(name: "Main", bundle: nil)
self.credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager:pool)
let serviceConfiguration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider!)
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: userPoolConfiguration, forKey: USER_POOL_NAME)
let manager = IdentityProviderManager(tokens: [NSString:NSString]())
self.credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: IDENTITY_POOL_ID, identityProviderManager: manager)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
startPasswordAuthentication()
The viewDidLoad() method in the login ViewController only contains this line regarding the Cognito values I use for logging in:
if pool == nil{
pool = AWSCognitoIdentityUserPool(forKey:USER_POOL)
}
In the prepareForSegue() case from the login ViewController to the first view the user sees after logging in, I set the user by calling:
destination.user = pool!.getUser(usernameField.text!)
In the method for signing out from this view, I call user!.signOut().
I've noticed that many Cognito example projects call credentialsProvider.clearKeychain() after signing out, but this did not solve the issue for me. I've been having trouble finding many examples showing specifically how to log out through Cognito. I've also heard that AWS credentials expire after an hour after signing into an app like this. What is the proper way to handle credentials if I want to solve this problem and avoid other situations that might force my users to relaunch the app in order to sign in?
On Log out in additions to user.signOut() getDetails needs to be called again! Not quite sure why this would be the case after signOut, but definitely fixed it for me.
self.user?.getDetails().continueOnSuccessWith { (task) -> AnyObject? in
return nil
}
Related
So my goal is to have the correct user sign up and be shown the correct segue as well as the user info be written to Firestore. So I have a basic sign up function that gets triggered when the sign up button is pressed:
#IBAction func schoolSignupPressed(_ sender: UIButton) {
let validationError = validateFields()
let schoolName = schoolNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolEmail = schoolEmailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolPassword = schoolPasswordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolID = schoolIDTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolDistrict = schoolDistrictTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let dateCreated = Date()
if validationError != nil {
return
}
Auth.auth().createUser(withEmail: schoolEmail, password: schoolPassword) { (result, error) in
guard let signUpError = error?.localizedDescription else { return }
guard error == nil else {
self.showAlert(title: "Error Signing Up", message: "There was an error creating the user. \(signUpError)")
return
}
let db = Firestore.firestore()
guard let result = result else { return }
db.document("school_users/\(result.user.uid)").setData(["school_name":schoolName,
"school_id":schoolID,
"emailAddress": result.user.email ?? schoolEmail,
"remindersPushNotificationsOn": true,
"updatesPushNotificationsOn": true,
"schoolDistrict":schoolDistrict,
"time_created":dateCreated,
"userID": result.user.uid],
merge: true) { (error) in
guard let databaseError = error?.localizedDescription else { return }
guard error == nil else {
self.showAlert(title: "Error Adding User Info", message: "There was an error adding the user info. \(databaseError)")
return
}
}
let changeRequest = result.user.createProfileChangeRequest()
changeRequest.displayName = schoolName
changeRequest.commitChanges { (error) in
guard error == nil else {
return
}
print("School Name Saved!")
}
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.performSegue(withIdentifier: Constants.Segues.fromSchoolSignUpToSchoolDashboard, sender: self)
}
}
}
This is the sign up function for the 'school' user, but the 'student' user is essentially the same thing just different fields and of course a different segue destination. Now maybe like a day ago or 2, I was testing this function out and it was working completely fine the user was succesfully signed up, the user info was written to firestore, the correct view controller was displayed, the only difference was I had some DispatchGroup blocks within the function because when i was running the method in TestFlight, there would be a couple of bugs that would crash the app.
So I figured since everything was working fine in the simulator, I archive the build, upload it to TestFlight and wait for it to be approved. It got approved last night and I ended up testing it out on my phone this morning to see it again, now when I try to sign up as either a school user or a student user, it segues to the wrong view controller every time and no info gets written to firestore, the user just gets saved in Firebase Auth and that is not the outcome I expect in my app.
I've checked the segue identifiers, I've checked the connections tab, and even though it was working amazing 24 hours ago, I still checked it all. I'm trying my best to really appreciate what Apple does for developers but I'm really starting to grow a hatred towards TestFlight, everything I do and run in the simulator works fantastic on Xcode, as soon as I run it in TestFlight, everything just goes out the window. I hate these types of bugs because you genuinely don't know where the issue is stemming from simply because you've used, if not very similar, the exact same method in every other previous situation.
The login process works fine on both student and school user, I'll show an example of the school user login method:
#IBAction func loginPressed(_ sender: UIButton) {
let validationError = validateFields()
let email = schoolEmailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = schoolPasswordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if validationError != nil {
return
} else {
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
guard let signInError = error?.localizedDescription else { return }
let group = DispatchGroup()
group.enter()
guard error == nil else {
self.showAlert(title: "Error Signing In", message: "There was an issue trying to sign the user in. \(signInError)")
return
}
group.leave()
group.notify(queue: .main) {
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.performSegue(withIdentifier: Constants.Segues.fromSchoolLoginToSchoolEvents, sender: self)
}
}
}
}
}
Pretty much the same for student users. If anyone can point out possible issues for this bug in the first code snippet that would be amazing. Thanks in advance.
Although it is helpful, removing the error.localizedDescription line brought everything back to normal.
I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}
I am currently working on the signup/login of my app using Firebase Authentication.
I have custom buttons for Facebook Signup/Login and Google Signup/Login.
For example, the code for the Facebook Signup/Login button is
#objc private func performFacebookLogin(_ sender: UIButton) {
let fbLoginManager : LoginManager = LoginManager()
fbLoginManager.logIn(permissions: ["email"], from: self) { (result, error) -> Void in
if let error = error {
print(error)
} else {
if result == nil {
print("no result!")
return
}
if AccessToken.current == nil {
print("no token")
return
}
DispatchQueue.main.async {
ThirdPartySignupViewController.push(fromVC: self, method: .facebook)
}
}
}
}
This will take the user to another view controller to add additional information to their account if they have not yet created an account in the app using this Facebook account.
However, I can not find any way to determine whether or not the Facebook account has previously used for an account in the app, in which case I want to just log the user in and take them to the main screen. Is there a way to do this?
I have tried the signIn(with:) method, hoping that it would return an error if there is no account in the app with the credentials of this Facebook account, but this will not work because this will end up creating a new account if the Facebook account hasn't been used previously to create an account.
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
//check if I get an error if the Facebook account has not been used
}
(EDIT) The desired flow is 1.Sign in with Facebook/Google → 2.Enter additional info → 3.Create account on Firebase Authentication.
When authenticating with Firebase - the Auth object returns isNewUser through additionalUserInfo. Therefore you can see the status of a new or existing user.
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
//authResult?.additionalUserInfo?.isNewUser
if let auth = authResult{
if auth.additionalUserInfo?.isNewUser {
// User has not been authenticated before. Do what you need
} else {
// User has been authenticated before
}
}
}
This question already has an answer here:
Handing Firebase + Facebook login process
(1 answer)
Closed 5 years ago.
I am finding it difficult to separate sign-in logic from sign-up logic because firebase only has one method to authenticate users through Facebook: signIn(with: AuthCredential). This one method will either sign-in an existing user, or register them automatically if they don't already exist, so there is no way for me to know that a user is signing up for the first time so i can persist additional info about user in firebase database.
Any help to solve this issue is greatly appreciated. My app is written in Swift so any solution with code would be most helpful in Swift. Thanks
See comments inside callback
private func rx_firebaseLogin(with credential: AuthCredential, userInfo: [String: AnyObject]) -> Observable<[String: AnyObject]> {
return Observable<[String: AnyObject]>.create { observable in
Auth.auth().signIn(with: credential) { (user, error) in
// IF user is already authenticated, User will get signed in automatically.
// IF user is new, that user will automatically get signed up.
// So I cannot put code here to save user info in db because I could potentially just be signing in a user, and end up duplicating a pre-existing user.
guard error == nil else {
observable.onError(AuthError.custom(message: error!.localizedDescription))
print(error!.localizedDescription)
return
}
guard user != nil else {
observable.onError(AuthError.invalidFirebaseUser)
print("debugger: error logging user")
return
}
var data = userInfo
data[Constant.id] = user!.uid as AnyObject
observable.onNext(data)
observable.onCompleted()
}
return Disposables.create()
}
}
If you are using Firebase's Database, I assume you are storing information about that user in there. When you authorize a user, check to see if that user is in your database. If they aren't, it is the first time they are signing up. Here is what I do:
let accessToken = FBSDKAccessToken.current()
guard let accessTokenString = accessToken?.tokenString else {return}
let credentials = FacebookAuthProvider.credential(withAccessToken: accessTokenString)
Auth.auth().signIn(with: credentials, completion: { (user, error) in
let id = Auth.auth().currentUser?.uid
let ref = Database.database().reference(withPath: "users")
ref.child(id).observeSingleEvent(of: .value, with: {(snapshot) in
if snapshot.exists() {
//User is signing IN
} else {
//User is signing UP
}
}
}
Assumed database structure:
users
uniqueUserId
I have successfully implemented Cognito for iOS. I have decided to split the authentication process in 3 phases :
sign-up
confirmation with code
sign-in
For the confirmation with code I have this piece of code :
func sendCode() {
self.pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
let userForCode = self.pool?.getUser(self.emailTextField.text!)
// Bizarre ce test ne passe pas pourtant le client est confirmé dans Cognito...
if (userForCode?.confirmedStatus != AWSCognitoIdentityUserStatus.confirmed) {
userForCode?.confirmSignUp(confirmCodeTextField.text!, forceAliasCreation: true).continueWith { [weak self] (task) -> Any? in
DispatchQueue.main.async {
if let error = task.error as? NSError {
print("erreurCode : \(error.userInfo["message"])")
} else if let result = task.result {
print("client créé confirmé")
}
}
return nil
}
}
}
I get a user from the userPool and I want to ask the user for is confirmation code if his account is not yet confirmed so the test on userCode.confirmedStatus. Unfortunately, the test is always true even if the user has the confirmed status in the Cognito AWS Console.
I've done the confirmation code in my app, and I did it using verifyAttribute after sending an email to the user:
self.user?.verifyAttribute("email", code: code).continueWith { (task) in
DispatchQueue.main.async(execute: {
if let error = task.error as NSError? {
// handle error (display message perhaps)
} else {
// the verification code is correct, continue with your application
}
})
return nil
}
The variable code is taken from the textField where the user enters his verification code.