问题
I'm trying to implement the new AWS Cognito User Pools in my iOS (Swift) app, but I'm struggling to get the sign in process to work. I am essentially trying to follow the example available here.
This is what I have so far:
AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate, AWSCognitoIdentityInteractiveAuthenticationDelegate {
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let serviceConfiguration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: nil)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
let configurationUserPool = AWSCognitoIdentityUserPoolConfiguration(
clientId: "###",
clientSecret: "#########",
poolId: "###")
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: configurationUserPool, forKey: "UserPool")
self.userPool = AWSCognitoIdentityUserPool(forKey: "UserPool")
self.userPool!.delegate = self
return true
}
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let logInNavigationController = mainStoryboard.instantiateViewControllerWithIdentifier("LogInNavigationController") as! UINavigationController
dispatch_async(dispatch_get_main_queue(), {
self.window?.rootViewController = logInNavigationController
})
let logInViewController = mainStoryboard.instantiateViewControllerWithIdentifier("LogInViewController") as! LogInViewController
return logInViewController
}
}
LogInViewController:
class LogInViewController: UIViewController, AWSCognitoIdentityPasswordAuthentication {
var usernameText : String?
var passwordAuthenticationCompletion = AWSTaskCompletionSource()
func getPasswordAuthenticationDetails(authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource) {
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
dispatch_async(dispatch_get_main_queue(), {
if self.usernameText == nil {
self.usernameText = authenticationInput.lastKnownUsername
}
})
}
func didCompletePasswordAuthenticationStepWithError(error: NSError) {
dispatch_async(dispatch_get_main_queue(), {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let mainNavigationController = mainStoryboard.instantiateViewControllerWithIdentifier("MainNavigationController") as! UINavigationController
(UIApplication.sharedApplication().delegate as! AppDelegate).window?.rootViewController = mainNavigationController
})
}
func logInButtonPressed() {
self.passwordAuthenticationCompletion.setResult(AWSCognitoIdentityPasswordAuthenticationDetails(username: emailTextField.text, password: passwordTextField.text))
}
}
Nothing seems to happen when I hit the log in button, although if I hit it again I get an NSInternalInconsistencyException (which I believe is because the AWSTask result has already been set).
Any help with this would be appreciated. I am using the AWS SDK for iOS version 2.4.1.
UPDATE:
Not a solution to my original problem, but I've been able to get User Pools working by using the explicit sign in method rather than the delegate method (see this page for details). Here is the code from my SignInViewController:
class SignInViewController: UIViewController {
@IBAction func signInButtonTouched(sender: UIButton) {
if (emailTextField.text != nil) && (passwordTextField.text != nil) {
let user = (UIApplication.sharedApplication().delegate as! AppDelegate).userPool!.getUser(emailTextField.text!)
user.getSession(emailTextField.text!, password: passwordTextField.text!, validationData: nil, scopes: nil).continueWithExecutor(AWSExecutor.mainThreadExecutor(), withBlock: {
(task:AWSTask!) -> AnyObject! in
if task.error == nil {
// user is logged in - show logged in UI
} else {
// error
}
return nil
})
} else {
// email or password not set
}
}
}
Then, to consume an AWS service (which in my case is located in a different region to Cognito) I have created a new Credentials Provider using the User Pool:
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: "###", identityProviderManager: (UIApplication.sharedApplication().delegate as! AppDelegate).userPool!)
let serviceConfiguration = AWSServiceConfiguration(region: .APNortheast1, credentialsProvider: credentialsProvider)
AWSLambdaInvoker.registerLambdaInvokerWithConfiguration(serviceConfiguration, forKey: "Lambda")
let lambdaInvoker = AWSLambdaInvoker(forKey: "Lambda")
One additional issue is that I was seeing this error each time I launched the app: "Could not find valid 'AWSDefaultRegionType', 'AWSCognitoRegionType', and 'AWSCognitoIdentityPoolId' values in info.plist.". This seems to be related to Fabric, which I am using to track crashes. I've solved this by changing this line in the AppDelegate:
Fabric.with([AWSCognito.self, Crashlytics.self])
to this:
Fabric.with([Crashlytics.self])
I hope this helps someone else.
回答1:
Update 6: (and really final this time )
It is worth mentioning that (finally) AWS has made the AWS Mobile Hub build a very nice demo app that INCLUDES User Pools as a SignInProvider (With Google and Facebook too). The architecture is (in my opinion) excellent (they have separated Identity Management and getting credentials from Authentication) Check it out
Update 5: (and final)
There is a fairly complete example implementation, and some documentation of how it works in this other answer.
iOS - AWS MobileHub sign in with developer authenticated provider
Update 4:
If you want to get access to AWS services, there are more steps needed
It turns out that this does not get you authenticated with Cognito Federated Identities (the "logins" count on the identity browser remains at 0). To fix this you need to establish a credentialsProvider and do "credentialsProvider.getIdentityId". After that logins will show positive, and you can get services from AWS based upon your authenticated role.
If you are trying to do both Authenticated and UnAuthenticated access for your mobile app, then you need to create an AWSAnonymousCredentialsProvider (in a separate service configuration). Then you self.credentialsProvider?.invalidateCachedTemporaryCredentials() and self.credentialsProvider?.clearCredentials() when logging out and do the getidentityid again with the anonymous service configuration and you will get an anonymous id. (Note: I found it seemed like if you clearkeychain on the credentialsProvider it starts with a new id each time a user logs out, which could burn up your free 50,000 ids pretty quick. )
Update 3:
Uploaded a github sample app for AWS User Pools for IOS in Swift.
https://github.com/BruceBuckland/signin
Update 2:
I finally got AWS User Pools to work correctly in Swift
My problem was that each time the authentication start happened it was caused by an authentication failure in a different viewcontroller (my error). I ended up with a bunch of them running waiting for completion returns which never came and the API was "silent" (showed no errors). The API does not notice that it is being initiated multiple times ( by a different viewController each time) so it silently lets log in over and over. There is not enough of your code in the original post to see if you are having that same issue.
You have to be careful, the AWS sample code (in Objective-C) has two navigation controllers, and the code re-uses them. I don't like the way the sample app flashes the logged in view controller before the authentication delegate gets going and I was trying to improve that in the swift version and that caused my problem.
AWS User Pools API is set up to work with a storyboard or app structure that works like this:
1) Your app ASSUMES it is logged in, and then triggers the delegate which triggers authentication and the login screens if it is not.
2) In original logged in view controller pool.currentUser() is NOT enough to get the authentication going, API will only trigger the delegate when you do more (in my case user.getDetails()).
3) The authentication is completed through the didCompletePasswordAuthenticationStepWithError. This delegate method is called if you get an authentication (or other) error AND if you SUCCESSFULLY authenticate. In the case of successful authentication the NSError is nil, so it should be declared as NSError? in the delegate (this causes a warning). The API is beta, they will probably fix this.
4) One other little “gotcha”, it may be obvious to you, it caught me, when you define your User Pool in the console you specify allowed apps, and each of these apps HAS DIFFERENT STRINGS for Client ID strings. ( I was just plugging the same thing into the example) which works badly (but does not report errors). The API needs some work in the reporting department. It is very Verbose when it is working, but says nothing if you pass it the wrong Client Strings. Also it seems to say nothing if you (like I did) call the API from different viewcontrollers. It was just taking each new authentication request from a different viewcontroller and saying nothing.
Anyway, it works now. I hope this helps resolve your issue.
Update:
I finally got getPasswordAuthenticationDetails to execute.
It turns out it does not get executed until user.getDetails for the current user (even if there is no current user).
So
let user = appDelegate.pool!.currentUser()
let details = user!.getDetails()
will result in the getPasswordAuthenticationDetails callback getting executed on the second line.
It seems the AWS UserPool concept is that we write an app that assumes we have a logged in user. We get details from that user (for instance in the initial view controller) and the delegate gets kicked off if we don't have a user.
The AWS documentation for User Pools on IOS is missing some important concept pages. Those pages ARE included in the (otherwise parallel) Android documentation. I admit that I am still struggling (days now) with getting User Pools to work in swift, but reading the "Main Classes" and "Key Concepts" Parts of the Android documentation clarified a lot for me. I can't see why it was omitted from the IOS doc.
回答2:
Just adding my 2 cents for people who work with Objective-c and the sample app CognitoYourUserPoolsSample provided by Amazon. @Bruce0 already covered everything with his swift solution. But if you're running into this problem where getPasswordAuthenticationDetails doesn't get called when you hit sign-in is because you're not calling [self.user getDetails] at all. Indeed - getDetails triggers getPasswordAuthenticationDetails. If you look closer in the AWS sample app, they call it right when they launch the app in viewDidLoad of the UserDetailTableViewController, which is the first controller that gets loaded. If the user is not signed-in then getDetails response somehow triggers the SignInViewController. This is what I will explain below. It's like a "myHomeViewController" kind of, where you want to display user related info. Otherwise, you want to display the login/sign-up screen by default.
As a general rule of thumb, connect and init the Cognito User Pool in your AppDelegate (didFinishLaunchingWithOptions) exactly like they did in the sample app. Make sure to add the AWSCognitoIdentityInteractiveAuthenticationDelegate and implement startPasswordAuthentication where you'll bring up your sign-in ViewController. Let the AppDelegate take care of the WHAT_TO_DO_IF_USER_NOT_SIGNED_IN (e.g. bring the SignInViewController on top) then focus on the WHEN_DOES_THE_USER_NEEDS_TO_SIGNIN somewhere in your app.
When you need user specific data then tell the App that it's time to check if the user is signed-in (self.user getDetails). Again, if user isn't signed-in then the AppDelegate knows what to do. It over-rules the app and displays the sign-in View on top of everything. So, it could be right at the beginning (e.g. Facebook, Twitter, etc.) or somewhere else (e.g. Ebay, etc.). Simply call [self.user getDetails] at the end of viewDidLoad. This will prevent the current ViewController to show prior to the authentication step (sign-in/sign-up) OR simply load the current ViewController if the user is already signed-in.
When using AWS User Pool feature in your app, follow those steps:
- Find out where you need user specific data in any of YourViewControllers
- in the related ViewDidLoad, call [self.user getDetails]
- if User is signed-in already then display user specific data using the completionHandler of self.user getDetails.
- if not then startPasswordAuthentication gets automatically called in the AppDelegate, which brings up the sign-in ViewController before displaying YourViewController since you need user specific data
- User sign-in or sign-up
- dissmiss sign-in/sign-up ViewController and there you are back into YourViewController which can now be loaded with some user specific data.
The AWS sample App isn't straight forward but it is really simple.
回答3:
Thanks Elliot. I am trying to write the swift version of this code for few days now.
I tried using the explicit signIn using below code.
@IBAction func signInButtonPressed(sender: AnyObject) {
var emailTextField = "username"
var passwordTextField = "password"
let serviceConfiguration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: nil)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
let configurationUserPool = AWSCognitoIdentityUserPoolConfiguration.init(clientId: "####", clientSecret: "#####", poolId: "#####")
AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithConfiguration(serviceConfiguration, userPoolConfiguration: configurationUserPool, forKey: "TestUserPool")
let userPool = AWSCognitoIdentityUserPool(forKey: "TestUserPool")
let user = userPool.getUser(emailTextField)
user.getSession(emailTextField, password: passwordTextField, validationData: nil, scopes: nil).continueWithExecutor(AWSExecutor.mainThreadExecutor(), withBlock: {
(task:AWSTask!) -> AnyObject! in
if task.error == nil {
print("No Error")
print(task.result)
} else {
print("Some Error")
print(task.error)
}
return nil
})
}
When I provide correct credentials it goes to the Error Block. A verfication code is sent to my mobile each time I run the code although I have already verified my user during sign up process. Response Body is
Response body:
{"AuthState":"H4sIAAAAAAAAAAXB3ZJzMAAA0EeqBDvTnfkulhVCpRXyI3dNmI8KzU7bLZ5+z+m3HBjINz2jp6rxB174rmT+agWweHyPLVydEqFXi2o8j9gjTT6XcH1qeA+vWWQVbAMDW6gXvhEYgHOMH3gmg06pNTP61pBaNvO1E3zvEPFaSS2+3ccuQ6qUVvXcYjqBQKFoKvfoJHgLDKJx3VhlkKsIUGs7qbhH6qXZ3a9kl+v0uPEEOWqR0/7gk4T8iiYPm0XBXt59LivPwAGUmSr1RAfDqSz8COhkZcQLFdsev3oGVw3oWTRRXIHuRkTuqYS6/juHBIYRgzTsZ1crqHB5I5OZ2JvaMmB2aKqpS2qYizMqg5KjgqI24DtNGLfXenGu8/+/zU5ZnZlVCXTRNwtKxgXP2k0LJK9T58TCnxxRJtLnQ7AAFD4lZpnWk+dY4fGBCFqZlP4YyUGfqVQ3rW/i/PgJPnd8WN8fw/Hr5D0OChfhfCleb290yaV/AXf4itllINJONfv3B7RgGQzfAQAA","CodeDeliveryDetails":
{"DeliveryMedium":"SMS","Destination":"+*******8869"}}
Some Error
Optional(Error Domain=com.amazonaws.AWSCognitoIdentityProviderErrorDomain Code=-1000 "startMultiFactorAuthentication not implemented by authentication delegate" UserInfo={NSLocalizedDescription=startMultiFactorAuthentication not implemented by authentication delegate})
When I provided wrong password the response body is
Response body:
{"__type":"NotAuthorizedException","message":"Incorrect username or password."}
Some Error
Optional(Error Domain=com.amazonaws.AWSCognitoIdentityProviderErrorDomain Code=12 "(null)" UserInfo={__type=NotAuthorizedException, message=Incorrect username or password.})
Could you suggest what I am doing wrong here?
回答4:
I've also followed the same steps mentioned in the original posters question, but the app never switches to the Login screen on startup. I've verified that the code to switch views is correct by placing directly in the AppDelegate.application(....) method. It appears that the startPasswordAuthentication(...) delete method is never called. Can someone post a link to a sample app that switches to the login screen using the AWSCognitoIdentityInteractiveAuthenticationDelegate protocol?
回答5:
It seems like the method "startMultiFactorAuthentication" is not implemented in the delegate, thats why incorrect password is getting detected, but when corrected password is given, it is escalated to MFA, but start MFA function is not found in the delegate and hence the login fails.
回答6:
For me I kept getting this error because I have verification on, but I did NOT verify the user before attempting to login. Once it was verified everything worked.
回答7:
I am following the same example and dealing with the same problem, and I haven't completely managed to solve it, but I strongly suspect the problem has something to do with this function never executing:
func getPasswordAuthenticationDetails(authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource) {
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
dispatch_async(dispatch_get_main_queue(), {() -> Void in
if self.usernameText == nil{
self.usernameText = authenticationInput.lastKnownUsername
}
})
}
I have tried putting breakpoints and print statements in this method and it never seems to be activated. I would suggest doing the same in your code, since it sounds like your issue is identical to mine. I looked in the example and was unable to find a place where the method was called manually. I noticed that in your code you intialize the value of passwordAuthenticationCompletion
like so:
var passwordAuthenticationCompletion = AWSTaskCompletionSource()
It seems as though getPasswordAuthenticationDetails()
is supposed to be called before this line in the login method uses the relevant value: self.passwordAuthenticationCompletion.setResult(AWSCognitoIdentityPasswordAuthenticationDetails(username: emailTextField.text, password: passwordTextField.text))
I've been stuck for a while now trying to get any further than this, but I still think this is the correct route to implementing user registration/login. It's possible that some of the code in the example doesn't translate cleanly to Swift, and so some important function isn't being triggered as a result. I'll keep looking into it and update my answer if I confirm a solution.
回答8:
I've followed the same steps mentioned earlier using Swift and I noticed that the didCompletePasswordAuthenticationStepWithError
is never called, although LogInViewController
extends AWSCognitoIdentityPasswordAuthentication
.
Also, startPasswordAuthentication()
is not called in the delegate even though the delegate also implements AWSCognitoIdentityInteractiveAuthenticationDelegate
.
I wonder if this is a problem with the Swift implementation since the sample Objective-C that Amazon provides has all these working.
回答9:
I think there is problem in this :
Object-c ->
self.passwordAuthenticationCompletion.result = [[AWSCognitoIdentityPasswordAuthenticationDetails alloc] initWithUsername: username password:password];
Swift -> self.passwordAuthenticationCompletion.setResult(AWSCognitoIdentityPasswordAuthenticationDetails.init(username: username, password: password))
and because there is something wrong in the previous statement , the "didCompletePasswordAuthenticationStepWithError" method did not fire.
I tried many things without luck :( - I tried to implement every thing in Swift (not Work) - I tried to add the Object-C files to my Swift based project (not Work)
So, I think I will use the orginal sample as my starter for my project.
Update:
I implement the SignInViewController & MFAController in Swift and import these Swift files in Object-C based project. and it is work fine ! So, now I am sure there issue or bug when we try to implement "AWSCognitoIdentityPasswordAuthentication" && "AWSCognitoIdentityMultiFactorAuthentication" protocols in Swift based project. The only solution I found is to go with Object-c based project.
回答10:
This is how I made it happen in Swift, with a single ViewController and without setting anything up in the AppDelegate:
class LoginViewController: UIViewController, AWSCognitoIdentityInteractiveAuthenticationDelegate, AWSCognitoIdentityPasswordAuthentication {
var passwordAuthenticationCompletion = AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>()
var pool = AWSCognitoIdentityUserPool.init(forKey: "UserPool")
override func viewDidLoad() {
super.viewDidLoad()
//setup service configuration
let serviceConfiguration = AWSServiceConfiguration.init(region: .USEast1, credentialsProvider: nil)
//create and config a pool
let configuration = AWSCognitoIdentityUserPoolConfiguration.init(clientId: "YourClientId", clientSecret: "YourClientId", poolId: "YourPoolId")
AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: configuration, forKey: "UserPool")
pool = AWSCognitoIdentityUserPool.init(forKey: "UserPool")
pool.delegate = self
}
@IBAction func logInButtonPressed(sender: UIButton) {
pool.getUser().getDetails()
}
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
return self
}
func getDetails(_ authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>) {
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
let result = AWSCognitoIdentityPasswordAuthenticationDetails.init(username: "username", password: "password")
}
}
来源:https://stackoverflow.com/questions/37176334/aws-cognito-user-pools-in-ios-swift-app