AWS Cognito sign in not working (Swift-iOS)

2020-06-27 05:59发布

问题:

I've integrated cognito into my xcode project. The sign up/password update features are working correctly. However I can't seem to get the sign in process to work. I turned on the logs and I get the following error

{"__type":"NotAuthorizedException","message":"Access Token has expired"}


Domain=com.amazonaws.AWSCognitoIdentityProviderErrorDomain Code=-1000 "Authentication delegate not set" UserInfo={NSLocalizedDescription=Authentication delegate not set}]

I have also implemented the AWSCognitoIdentityInteractiveAuthenticationDelegate delegate in the AppDelegate script.

Here's the AppDelegate code

class AppDelegate: UIResponder, UIApplicationDelegate {

    class func defaultUserPool() -> AWSCognitoIdentityUserPool {
        return AWSCognitoIdentityUserPool(forKey: userPoolID)
    }

    var window: UIWindow?
    var loginViewController: LoginVC?
    var navigationController: UINavigationController?
    var storyboard: UIStoryboard?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // Warn user if configuration not updated
        if (CognitoIdentityUserPoolId == "us-east-1_TavWWBZtI") {
            let alertController = UIAlertController(title: "Invalid Configuration",
                                                    message: "Please configure user pool constants in Constants.swift file.",
                                                    preferredStyle: .alert)
            let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil)
            alertController.addAction(okAction)
            self.window?.rootViewController!.present(alertController, animated: true, completion:  nil)
            //print("Please configure user pool constants in Constants.swift file.")
        }

        // setup logging
        AWSDDLog.sharedInstance.logLevel = .verbose
        AWSDDLog.add(AWSDDTTYLogger.sharedInstance)

        // setup service configuration
        let serviceConfiguration = AWSServiceConfiguration(region: CognitoIdentityUserPoolRegion, credentialsProvider: nil)

        // create pool configuration
        let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: CognitoIdentityUserPoolAppClientId,
                                                                        clientSecret: CognitoIdentityUserPoolAppClientSecret,
                                                                        poolId: CognitoIdentityUserPoolId)

        // initialize user pool client
        AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: poolConfiguration, forKey: AWSCognitoUserPoolsSignInProviderKey)

        // fetch the user pool client we initialized in above step
        let pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
        self.storyboard = UIStoryboard(name: "Main", bundle: nil)
        pool.delegate = self


        return AWSMobileClient.sharedInstance().interceptApplication(
            application, didFinishLaunchingWithOptions:
            launchOptions)
        //return true
    }

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if let navigationController = self.window?.rootViewController as? UINavigationController {
           if navigationController.visibleViewController is SummaryReportVC ||
              navigationController.visibleViewController is GoalStatusReportVC || navigationController.visibleViewController is YearTotalsReportVC || navigationController.visibleViewController is DailyActivityReportVC {
                return UIInterfaceOrientationMask.all
            } else {
                return UIInterfaceOrientationMask.portrait
            }
        }
        return UIInterfaceOrientationMask.portrait
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }

}

extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {

    func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
        print("Calling signin VC from app delegate")
        if (self.navigationController == nil) {
            self.navigationController = self.storyboard?.instantiateViewController(withIdentifier: "NCFirst") as? UINavigationController
        }

        if (self.loginViewController == nil) {
            self.loginViewController = self.navigationController?.viewControllers[0] as? LoginVC
        }

        DispatchQueue.main.async {
            self.navigationController!.popToRootViewController(animated: true)
            if (!self.navigationController!.isViewLoaded
                || self.navigationController!.view.window == nil) {
                self.window?.rootViewController?.present(self.navigationController!,
                                                         animated: true,
                                                         completion: nil)
            }

        }
        return self.loginViewController!
    } 
}

Here's my LoginVC code

class LoginVC: UIViewController {

    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var forgotPasswordLabel: UILabel!
    @IBOutlet weak var signUpLabel: UILabel!
    @IBOutlet weak var emailTF: UITextField!
    @IBOutlet weak var passwordTF: UITextField!
    var passwordAuthenticationCompletion: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>?
    let pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
    var usernameText: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.navigationBar.tintColor = UIColor.white
        self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
        self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationController!.navigationBar.shadowImage = UIImage()
        self.navigationController!.navigationBar.isTranslucent = true

        loginButton.addTarget(self, action: #selector(loginUser), for: .touchUpInside)

        loginButton.layer.cornerRadius = 18
        emailTF.addPadding(.left(35))
        passwordTF.addPadding(.left(35))

        let tap = UITapGestureRecognizer(target: self, action: #selector(goToForgotPasswordVC))
        let tap2 = UITapGestureRecognizer(target: self, action: #selector(goToSignUp1VC))
        forgotPasswordLabel.isUserInteractionEnabled = true
        forgotPasswordLabel.addGestureRecognizer(tap)
        signUpLabel.isUserInteractionEnabled = true
        signUpLabel.addGestureRecognizer(tap2)


    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.passwordTF.text = nil
        self.emailTF.text = usernameText
    }


    @objc func loginUser() {
        print("Got inside Login func")
        if (self.emailTF.text != nil && self.passwordTF.text != nil) {
            print("Calling login method now")
            let authDetails = AWSCognitoIdentityPasswordAuthenticationDetails(username: self.emailTF.text!, password: self.passwordTF.text! )
            self.passwordAuthenticationCompletion?.set(result: authDetails)

        } else {
            print("Empty fields")
            let alertController = UIAlertController(title: "Missing information",
                                                    message: "Please enter a valid user name and password",
                                                    preferredStyle: .alert)
            let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
            alertController.addAction(retryAction)
        }
    }

    @objc func goToActivitySessionsVC() {
        let storyboard = UIStoryboard(name: "TabBar", bundle: nil)
        let destVC = storyboard.instantiateViewController(withIdentifier: "TabBarVC")
        self.navigationController?.pushViewController(destVC, animated: true)
        self.navigationController?.isNavigationBarHidden = true
    }

    @objc func goToForgotPasswordVC() {
        let storyboard = UIStoryboard(name: "ForgotPassword", bundle: nil)
        let destVC = storyboard.instantiateViewController(withIdentifier: "ForgotPasswordVC")
        self.navigationController?.pushViewController(destVC, animated: true)
    }

    @objc func goToSignUp1VC() {
        let storyboard = UIStoryboard(name: "SignUp", bundle: nil)
        let destVC = storyboard.instantiateViewController(withIdentifier: "SignUp1VC")
        self.navigationController?.pushViewController(destVC, animated: true)
    }

 /*   func checkLoginStatus() {
        if !AWSSignInManager.sharedInstance().isLoggedIn {
            showSignIn()
        }
        else {
            print("Logged In")
            AWSSignInManager.sharedInstance().logout(completionHandler: {(result: Any?, error: Error?) in
                self.showSignIn()
                print("Sign-out Successful");

            })
        }
    }

}
*/
extension LoginVC: AWSCognitoIdentityPasswordAuthentication {

    public func getDetails(_ authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>) {
        print("Get details called")
        self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
        DispatchQueue.main.async {
            if (self.usernameText == nil) {
                self.usernameText = authenticationInput.lastKnownUsername
            }
        }
    }

    public func didCompleteStepWithError(_ error: Error?) {
        print("Did commplete step with error called")
        DispatchQueue.main.async {
            if let error = error as NSError? {
                let alertController = UIAlertController(title: error.userInfo["__type"] as? String,
                                                        message: error.userInfo["message"] as? String,
                                                        preferredStyle: .alert)
                let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
                alertController.addAction(retryAction)

                self.present(alertController, animated: true, completion:  nil)
                 print(error.description)
            } else {
                self.emailTF.text = nil
                self.dismiss(animated: true, completion: nil)
                print("Got in else")
            }
        }
    }
}

One other thing to note is that getDetails never gets called and so does the didCompleteStepWithError method. When I click the sign in button, nothing happens.

回答1:

The AWS documentation is quite confusing. After much trial and error, I was able to successfully set up Cognito, sign up, authenticate on log in, and un-authenticate on sign out. To be quite honest, I don't fully understand why I call certain things. To the best of my ability, I will explain here.

Here's how Cognito works.. First it assumes that the user is already logged in and authenticated. It checks to see if this is true. This is the reason why the entry point for your storyboard is the view controller that users see after they are logged in. This is all done with the code that runs in your AppDelegate on launch. More on what you need to fix in that below.

If the user is not logged in, startPasswordAuthentication() will be called. In your code, (as it should be) this defined in the extension of AppDelegate for the protocol AWSCognitoIdentityInteractiveAuthenticationDelegate. Furthermore, startPasswordAuthentication() is called every time the user needs to log in. If the user is already logged in once the app starts, this is not called.

Another note on your question - getDetails is only called on launch if the user is not signed in. If on launch the user is not signed in, then this is called. It is also called if you are signed in and then you sign out.

So make sure the entry point for your storyboard is the logged-in screen.

On the statement that follows I am not entirely sure, so feel free to correct me if so: AWS automatically accesses the entry point upon successful log-in. Everything you are going in your @objc func loginUser() looks correct to me. That's what I have. But make sure your entry point is not the log-in screen but rather what shows after successful log in. Here is a picture of my storyboard:

Try adding the following. I am not quite sure why exactly this works, but it results in proper authentication:

In your AppDelegate, right after your variable for the storyboard, add a boolean isInitialized as such:

     var isInitialized : Bool = false

Then add this code after you set up your Cognito configuration. (right before your return statement in didFinishLaunchingWithOptions) :

    let didFinishLaunching = AWSSignInManager.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)

    if (!self.isInitialized) {
        AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, error: Error?) in
            print("Result: \(String(describing: result)) \n Error:\(String(describing: error))")
        })
        self.isInitialized = true
    }

Now replace the return statement you currently have for didFinishLaunching with the following:

    return didFinishLaunching

Make sure you have this delegate set in your viewDidLoad() method for your login screen (Note you have to import AWSAuthCore):

   AWSSignInManager.sharedInstance().delegate = self

and implement the protocol in your log-in VC as such:

   extension LoginViewController : AWSSignInDelegate {
       func onLogin(signInProvider: AWSSignInProvider, result: Any?, error: Error?) {
           if error == nil {

           }
       }
   }

Add these variables as class variables to your view controller that users see after they are logged in. They are referenced below.

var user : AWSCognitoIdentityUser?
var userAttributes : [AWSCognitoIdentityProviderAttributeType]?

/*
     * :name: defaultUserPool
     * :description: Returns the cognito identity pool for the global pool ID.
     * :return: A Cognito Identity pool instantiation
     */
    class func defaultUserPool() -> AWSCognitoIdentityUserPool {
        return AWSCognitoIdentityUserPool(forKey: userPoolID)
    }

Finally, make sure that you are checking the user attributes in the initial screen in viewWillAppear(). For example call the function fetchUserAttributes in this method:

func fetchUserAttributes() {
    self.user = AppDelegate.defaultUserPool().currentUser()
    self.user?.getDetails().continueOnSuccessWith(block: { [weak self = self] (task) -> Any? in

        AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, error: Error?) in
            print("Result: \(String(describing: result)) \n Error:\(String(describing: error))")
        })
        guard task.result != nil else {
            // alert error
            return nil
        }
        self?.username = self?.user?.username
        self?.userAttributes = task.result?.userAttributes
        self?.userAttributes?.forEach({ (attribute) in
            print("Name: " + attribute.name!)
        })
        DispatchQueue.main.async {
                self?.setAttributeValues()
            }
        }
        return nil
    })
}

func resetAttributeValues() {
        self.user = nil
        self.userAttributes = nil
}

Finally, here is my code for signing out:

    let comp = { [weak self = self] (_ result: Any?, _ error: Error?) -> Void in
        if error == nil {
            self?.user?.signOut()
            self?.resetAttributeValues()
            self?.fetchUserAttributes()
        }
    }
    AWSSignInManager.sharedInstance().logout(completionHandler: comp)

I hope this helps. I understand this is really confusing, and to be honest, I was quite confused just writing this.. Good luck and feel free to message me with any questions.



回答2:

You need to call getDetails. In the sample app, they call getDetails in UserDetailTableViewController. Try this line of code below pool.delegate = self in AppDelegate.

self.pool?.currentUser()?.getDetails()

I referred AWS Cognito User Pools in iOS (Swift) app