Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.4.0 breaks custom login provider? #357

Closed
helloniklas opened this issue Apr 20, 2016 · 137 comments
Closed

2.4.0 breaks custom login provider? #357

helloniklas opened this issue Apr 20, 2016 · 137 comments
Labels
question General question

Comments

@helloniklas
Copy link

So I spent ages getting a custom login provider to work. I have a class BYOIProvider extending AWSAbstractCognitoIdentityProvider based on some AWS sample. This was all working fine. However, upgrading to 2.4.0 the AWSAbstractCognitoIdentityProvider is no longer available? Do I have to rewrite this code now?

@helloniklas
Copy link
Author

Downloading the sample code again, they still seem to use DeveloperAuthenticatedIdentityProvider : AWSAbstractCognitoIdentityProvider. So not sure what to do here, I guess I just have to revert to the old 2.3.6, as getting this custom identity to work was very complex in the first place.

@googoodalls
Copy link

YES, same issue. the 2.4.0 version will break my production app.

I have a customerSignin Provider based on AWSEnhancedCognitoIdentityProvider

@interface CustomIdentityProvider : AWSEnhancedCognitoIdentityProvider

  • (instancetype)initWithRegionType:(AWSRegionType)regionType
    identityId:(NSString *)identityId
    identityPoolId:(NSString *)identityPoolId
    logins:(NSDictionary *)logins
    providerName:(NSString *)providerName;
    @EnD

@googoodalls
Copy link

I hope there is a transition tutorial for the removed class.
Thank you!

@lakhman
Copy link

lakhman commented Apr 20, 2016

Same issue, just updated via cocoapods to 2.4.0, I used the Cognito sample app to build off, I now have the following errors.


Fatal: Value of type 'AWSCognitoCredentialsProvider' has no member 'refresh'
https://github.com/awslabs/aws-sdk-ios-samples/blob/master/CognitoSync-Sample/Swift/CognitoSyncDemo/AmazonClientManager.swift#L130


Warning: 'logins' is deprecated: Use 'AWSIdentityProviderManager' to provide a valid logins dictionary to the credentials provider.
In 2 Lines:
https://github.com/awslabs/aws-sdk-ios-samples/blob/master/CognitoSync-Sample/Swift/CognitoSyncDemo/AmazonClientManager.swift#L118

https://github.com/awslabs/aws-sdk-ios-samples/blob/master/CognitoSync-Sample/Swift/CognitoSyncDemo/AmazonClientManager.swift#L127


It would be great if the samples could be updated to reflect the changes in 2.4.0.

https://github.com/awslabs/aws-sdk-ios-samples
https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoSync-Sample

I had a look at the changelog, It mentions the deprecated logins issue, but nothing about the Value of type 'AWSCognitoCredentialsProvider' has no member 'refresh' Fatal error.

https://github.com/aws/aws-sdk-ios/blob/master/CHANGELOG.md

@yosuke-matsuda @aws-dpt @behrooziAWS Can someone spare a few mins to update the samples to reflect the new changes when you get a chance?

@yosuke-matsuda yosuke-matsuda added the question General question label Apr 20, 2016
@yosuke-matsuda
Copy link
Contributor

You can take a look at the implementation of AWSCognitoCredentialsProvider and its use of AWSCognitoCredentialsProviderHelper as a reference.

We are preparing the sample app updates, and it should be available soon. When we update our samples, we'll update this thread. Thanks.

@helloniklas
Copy link
Author

I can't seem to figure this out. Where do I implement my previous refresh() method? Do I now have to build a AWSCognitoIdentityProviderRefreshTokensRequest instead? Hopefully your samples will help.

It's bit daunting that after spending so much time to get custom logins working with Lambda a point update has changed everything it seems. Especially considering this is something the competition is offering with zero implementation and is something almost every app will need, as relying on 3rd party logins is not feasible for a real life app.

@isadon
Copy link
Contributor

isadon commented Apr 21, 2016

breaking changes like this should not be a point update 👎. Dealing with this same issue myself. What makes it worse is documentation of this breaking change shouldve gone up with the 2.4.0 release :(.

@helloniklas
Copy link
Author

Normal practice would have been to leave the deprecated method in there still working but flagged as depcricated. To sink the whole sdk like this I don't think is acceptable unless you're on a 0.9 release cycle.

@guillecom
Copy link

Did anybody manage to do this? Cause i'm in need to implement it and i can't seem to figure out which methods should i implement of the provider, maybe i should return NO in the isAuthenticated method and then my getIdentityId will be called?

@helloniklas
Copy link
Author

@guillecom you might want to go with the new user pools, although it's in beta only. https://mobile.awsblog.com/post/TxGNH1AUKDRZDH/Announcing-Your-User-Pools-in-Amazon-Cognito

@guillecom
Copy link

guillecom commented Apr 26, 2016

Thanks for the advice, here is one that might shed some light on the original issue:
The class we need to implement is

@protocol AWSCognitoCredentialsProviderHelper <AWSIdentityProvider, AWSIdentityProviderManager>

which the implementation of the identity provider manager has this method

@protocol AWSIdentityProviderManager <NSObject>

/**
 * Each entry in logins represents a single login with an identity provider.
 * The key is the domain of the login provider (e.g. 'graph.facebook.com') and the value is the
 * OAuth/OpenId Connect token that results from an authentication with that login provider.
 */
- (AWSTask<NSDictionary<NSString *, NSString *> *> *)logins;

I think that this is our new refresh method (altought i never implemented a refresh method in the first place :P)

@guillecom
Copy link

guillecom commented Apr 27, 2016

I was right, i just managed to implement this, if you implement the logins method of the AWSCognitoCredentialsProvider, which will set the identityId and also return an AWSTask with a dictionary containing [self.indentityProviderName : <THE OPEN ID RETURNED> ], everything works

Remember not to overload identityProviderName (it has to be something like cognito-identity.amazon.etc)
And to overload the identityPoolId to be the one you have setup

@isadon
Copy link
Contributor

isadon commented Apr 27, 2016

7 days already and no proper documentation for how to reimplement something that was broken. Considering how huge this SDK you can imagine how agonizing this can get. I wasted various hours already on this to no avail. Rolling back to pre 2.4.0 and scrutinizing every minor update on this repo.

@Sneakr
Copy link

Sneakr commented Apr 27, 2016

I agree with everyone else here. Ive spent countless hours debugging and googling and reading aws documentation, until i found this thread. If you make an update like this , atleast provide documentation for something like this , custom developer identity is not a small thing to skip documenting.

Whats even more fun is googling around every freaking website to find the older version . .. . great

@yosuke-matsuda
Copy link
Contributor

yosuke-matsuda commented Apr 28, 2016

I want to take a little bit of time to explain the version numbering convention we use in our SDK.

The incremented first digit indicates a total overhaul of the SDK. There is almost no backward compatibility, and you need to completely rewrite your app. This happened when we jumped from 1.x.x to 2.x.x.

The second digit indicates minor breaking changes. For example, from 2.2.x to 2.3.0, we started including bitcode. Because Xcode 6 does not support bitcode well, we bumped the required version of Xcode from 6 to 7. From 2.3.x to 2.4.0, we dropped the support for iOS 7 and updated the credentials provider protocols (the custom login provider is one implementation of credentials provider). One exception to this rule is that we do not bump the second digit version when we make breaking changes to beta labeled components (e.g. AWSS3TransferUtility).

We release updated API docs with the SDK update. @guillecom used them to implement the credentials provider on his own.
http://docs.aws.amazon.com/AWSiOSSDK/latest/Protocols/AWSCredentialsProvider.html
http://docs.aws.amazon.com/AWSiOSSDK/latest/Protocols/AWSIdentityProvider.html
http://docs.aws.amazon.com/AWSiOSSDK/latest/Protocols/AWSIdentityProviderManager.html
http://docs.aws.amazon.com/AWSiOSSDK/latest/Protocols/AWSCognitoCredentialsProviderHelper.html

We are still working on more detailed documentation with sample apps. They will be available shortly. If API documentation is not enough, and you need more help, please watch out for 2.x.0 releases and evaluate thoroughly before jumping on it right away since they contain some breaking changes.

We understand this is not Semantic Versioning 2.0.0 compliant, and this may be one of the reasons some people got confused. We will see if we can adopt Semantic Versioning in our SDK.

Thanks,

@helloniklas
Copy link
Author

@yosuke-matsuda @guillecom I'm no being able to get as far as login in and getting my token and cognito ID. But once I try and do something like query a table it comes back with error:

> [Error] AWSCredentialsProvider.m line:563 | __44-[AWSCognitoCredentialsProvider credentials]_block_invoke.345 | Unable to refresh. Error is [Error Domain=com.amazonaws.AWSServiceErrorDomain Code=11 "(null)" UserInfo={Type=Sender, Message=Not authorized to perform sts:AssumeRoleWithWebIdentity, Code=AccessDenied, __text=(
>     "\n    ",
>     "\n    ",
>     "\n    ",
>     "\n  "
> )}]
> Optional("The operation couldn’t be completed. (com.amazonaws.AWSServiceErrorDomain error 11.)")

Before I never had to deal with sts:AssumeRoleWithWebIdentity ? Is this something I need to add now?

@helloniklas
Copy link
Author

@yosuke-matsuda Might it be easier to get this to work being it on the mobile hub code example where there is a AWSMobileHubHelper that has a AWSSignInProvider.

Looking at that code though, I'm not sure how to implement the login() method in order to get the AWSIdentityManager.defaultIdentityManager().loginWithSignInProvider(signInProvider, completionHandler: {(result: AnyObject?, error: NSError?) to be called back?

@Stradix
Copy link

Stradix commented Apr 30, 2016

Same problem here!
If I want to add AWSCognitoIdentityProvider pod, it asks me to update AWScore to 2.4.0. If I update to 2.4.0 then I get this thread problem! So I'm in a loop. I can't user AWSCognitoIdentityProvider and I can't update. Please solve this problem asap and update samples! Everybody would appreciate it! Thanks

@yosuke-matsuda
Copy link
Contributor

Just pushed an updated Cognito Sync Demo app to a branch. You should take a look at the implementation of DeveloperAuthenticatedIdentityProvider. This branch uses only Developer Authenticated Identities to highlight the usage of DAI. However, you can simply provide identityProviderManager to DeveloperAuthenticatedIdentityProvider to add any other identity providers.

This Cognito Sync sample app seems to be a little behind of the latest AWS SDK updates, and there are many areas where it overlaps with AWS Mobile Hub generated sample apps. We will try to integrate AWSIdentityManager from AWS Mobile Hub to this sample app as @helloniklas suggested. When it is done, we'll update the master branch. Thanks.

@helloniklas
Copy link
Author

@yosuke-matsuda So I now get the token and the identiyID fine, however, once I try and make a dynomdb call I then get an error saying

Not authorized to perform sts:AssumeRoleWithWebIdentity, Code=AccessDenied

This does not happen when running 2.3.6 with the same dev identity providers. Not sure if there's some clash with the new identity pools somehow, as I'm not aware of this sts:AssumeRoleWithWebIdentity and why it's being denied when running 2.4.0 and not while running 2.3.6 ?

@Stradix
Copy link

Stradix commented May 3, 2016

@yosuke-matsuda I did create in AmazonClientManager.swift a new function called customLogin:

func customLogin() {
        let userPoolConfiguration: AWSCognitoIdentityUserPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(
            clientId: "3ds8p7eog7rddXXXXXXXXXXX",
            clientSecret: "cu3n1h7kb4n7j72q4XXXXXXXXXXXXXXXXXXX",
            poolId: "us-east-1_onzXXXXXX")


        AWSCognitoIdentityUserPool.registerCognitoIdentityUserPoolWithUserPoolConfiguration(
            userPoolConfiguration,
            forKey: "UserPool")
        let pool = AWSCognitoIdentityUserPool(forKey: "UserPool")
        self.completeLogin(["us-east-1_onzXXXXXXX" : pool.token()])


        let phone: AWSCognitoIdentityUserAttributeType = AWSCognitoIdentityUserAttributeType()
        phone.name = "phone_number"
        phone.value = "+44434343434"

        let nickname: AWSCognitoIdentityUserAttributeType = AWSCognitoIdentityUserAttributeType()
        nickname.name = "nickname"
        nickname.value = "Hello"

        let email: AWSCognitoIdentityUserAttributeType = AWSCognitoIdentityUserAttributeType()
        email.name = "email"
        email.value = "[email protected]"

        pool.signUp("Hello", password: "Rasdfgf123456", userAttributes: [email, nickname], validationData: nil) .continueWithBlock({ (task: AWSTask!) -> AnyObject! in
            if ((task.error) != nil) {
                print("Error: \(task.error)")
            }
            if ((task.result) != nil){
                print("User Created")
            }
            return nil
        })
    }

but I'm getting this error compile error:

[Error] AWSCredentialsProvider.m line:423 | __73-[AWSCognitoCredentialsProvider getCredentialsWithCognito:authenticated:]_block_invoke | GetCredentialsForIdentity failed. Error is [Error Domain=com.amazonaws.AWSCognitoIdentityErrorDomain Code=9 "(null)" UserInfo={__type=ResourceNotFoundException, message=Identity 'us-east-1:a2d8ee68-34aa-413d-9a09-38a57e820XXX' not found.}]

It looks like is trying to find this identityId in my Federated Identities panel, but since it doesn't exists, is shows this error. What it missing in order to have this identity Id created?

@helloniklas
Copy link
Author

helloniklas commented May 3, 2016

@yosuke-matsuda If I change the Trust Relationship to

"ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "*"
        }

it does seem to work now... however, not sure what these trust thing is all about as I didn't need that under 2.3.6

@yosuke-matsuda
Copy link
Contributor

@helloniklas Are you passing unauthRoleArn or authRoleArn to the credentials provider? For Enhanced Flow, you do not need to pass these values (See Authentication Flow for more details). If you are passing Role ARNs, can you remove them and try again?

@Stradix The error you are encountering does not seem to be related to the logins dictionary. You can create a new thread with detailed steps to reproduce the issue. AmazonClientManager.swift alone is not enough to reproduce the issue.

@helloniklas
Copy link
Author

@yosuke-matsuda right, that seems to work now, missed that these were set to nil in your example.

@helloniklas
Copy link
Author

helloniklas commented May 4, 2016

@yosuke-matsuda However, I'm now running in to some other issues I didn't have with 2.3.6. If I first log in with DeveloeprAuthenticated ID, then logout, and try and log in with Facebook, it won't work until I completely shut the app down and login. Seems the logout doesn't really work properly. Before I could clear the logins but now with 2.4.1 I set keychain etc to nil and

                AWSCognito.defaultCognito().wipe()
            if let credentialsProvider = AWSServiceManager.defaultServiceManager().defaultServiceConfiguration.credentialsProvider as? AWSCognitoCredentialsProvider {
                credentialsProvider.clearKeychain()
            }

But this somehow doesn't work properly. Even after loging out then authenticating with Facebook, when I then try and do a request, it still tries to refresh the DeveloperAuthenticated thing.

And vice versa, if I first login Facebook, then logout, when then loging in with DeveloperAuth, it still tried to refresh the Facebook manager, although I've set the AWSCognitoCredentialsProvider to use the DeveloperAuth.

@helloniklas
Copy link
Author

@yosuke-matsuda I also now get an error the first time the app is installed and try to login,

 AWSiOSSDK v2.4.1 [Debug] AWSInfo.m line:122 | -[AWSServiceInfo initWithInfoDictionary:checkRegion:] | Couldn't read the region configuration from Info.plist for the client. Please check your `Info.plist` if you are providing the SDK configuration values through `Info.plist`.
2016-05-04 16:16:04.594 Duoo[11194:3028281] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The service configuration is `nil`. You need to configure `Info.plist` or set `defaultServiceConfiguration` before using this method.'

this causes the app to crash. It then works the second time around. Seems 2.4.1 is very flakey? Or I'm missing something in the implementation.

@behrooziAWS
Copy link
Contributor

@helloniklas The reason why you needed to open up your trust relationship above was likely because you were passing in roles for a different identity pool. Since the pool didn't match what was in the amr, requests were getting rejected. Now that you are not passing in roles (you've set them to nil) it is using the correct roles that are associated with your pool. You should always lock down your trust relationship, otherwise anyone using Cognito Identity can assume your roles.

Without seeing what you are doing in your IdentityProviderManager, it is difficult to tell why you are still authenticating as dev auth after logging out. Do you pivot to setting the Facebook token in the logins map after logging out and is the Facebook token the only item in your logins map?

With your app crash, are you correctly setting the defaultServiceConfiguration? See Step 4 for an example.

@behrooziAWS
Copy link
Contributor

behrooziAWS commented Nov 14, 2016

And here is the swift documentation:

To use developer authenticated identities, implement your own identity provider class which extends AWSCognitoCredentialsProviderHelper.

import AWSCore
/*
 * Use the token method to communicate with your backend to get an
 * identityId and token.
 */
class DeveloperAuthenticatedIdentityProvider : AWSCognitoCredentialsProviderHelper {
    override func token() -> AWSTask<NSString> {
    //Write code to call your backend:
    //pass username/password to backend or some sort of refresh token to authenticate user, if successful, 
    //from backend call getOpenIdTokenForDeveloperIdentity with logins map containing "your.provider.name":"enduser.username"
    //return the identity id and token to client
    //You can use AWSTaskCompletionSource to do this asynchronously

    // Set the identity id and return the token
    self.identityId = resultFromAbove.identityId
    return AWSTask(result: resultFromAbove.token)
}

To use this identity provider, pass it into AWSCognitoCredentialsProvider as shown in the following example:

let devAuth = DeveloperAuthenticatedIdentityProvider(regionType: .YOUR_IDENTITY_POOL_REGION, identityPoolId: "YOUR_IDENTITY_POOL_ID", useEnhancedFlow: true, identityProviderManager:nil)
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .YOUR_IDENTITY_POOL_REGION, identityProvider:devAuth)
let configuration = AWSServiceConfiguration(region: .YOUR_IDENTITY_POOL_REGION, credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration

If you want to support both unauthenticated identities and developer authenticated identities, override the logins method in your AWSCognitoCredentialsProviderHelper implementation.

override func logins () -> AWSTask<NSDictionary> {
    if(/*logic to determine if user is unauthenticated*/) {
        return AWSTask(result:nil)
    }else {
        return super.logins()
    }
}

If you want to support developer authenticated identities and social providers, you must manage who the current provider is in your logins implementation of AWSCognitoCredentialsProviderHelper.

override func logins () -> AWSTask<NSDictionary> {
    if(/*logic to determine if user is unauthenticated*/) {
        return AWSTask(result:nil)
    }else if (/*logic to determine if user is Facebook*/){
        if let token = AccessToken.current?.authenticationToken {
            return AWSTask(result: [AWSIdentityProviderFacebook:token])
        }
        return AWSTask(error:NSError(domain: "Facebook Login", code: -1 , userInfo: ["Facebook" : "No current Facebook access token"]))
    }else {
        return super.logins()
    }
}

@helloniklas
Copy link
Author

@behrooziAWS Wouldn't it be better to add this to your official docs, they are very outdated, and downloading samples from mobile hub is even worst.

@behrooziAWS
Copy link
Contributor

I've passed this content to the docs team. When they incorporate it, I will resolve this issue.

@georgel
Copy link

georgel commented Nov 16, 2016

@behrooziAWS Can I please trouble you for a sample on how to use Digits with Cognito? I am encountering the same problem with trying to use Digits with the new SDK. I followed the existing examples for 2.36 and basically stuck on this step to set the logins :(

Please help.

@behrooziAWS
Copy link
Contributor

behrooziAWS commented Nov 16, 2016

@georgel likely you will need to implement an IdentityProviderManager that does something like this:

@implementation DigitsIdentityProviderManager

- (AWSTask<NSDictionary<NSString *, NSString *> *> *)logins {
    AWSTaskCompletionSource<NSDictionary<NSString *, NSString *> *> * completion = [AWSTaskCompletionSource new];
    [[Digits sharedInstance] authenticateWithCompletion:^
     (DGTSession* session, NSError *error) {
         if (!error) {
             NSString value = [NSString stringWithFormat:@"%@;%@", session.authToken, session.authTokenSecret];
             completion.result = @{@"www.digits.com", value};
         } else {
             completion.error = error;
         }
     }];
    return completion.task;
}

@end

@georgel
Copy link

georgel commented Nov 17, 2016

@behrooziAWS Thank you, David and all the helpful folks on this thread. I finally got it to at least login with Digit.

Can someone please give me a tip on how to go from un-auth user to an auth-ed user. When a user first open the app, they are assigned a un-auth identityId. After they login in, I am able to get the correct authed identityId. But when the app relaunches, the credentialProvider.identityId is still the un-auth id.

Here's what I piece together from all the helpful tips above. Please let me know if I am missing a step somewhere :)

An IdentityProviderManager:

class DigitsIdentityProviderManager:NSObject, AWSIdentityProviderManager {
    public func logins() -> AWSTask<NSDictionary> {
        let completion = AWSTaskCompletionSource<NSDictionary>()
        if let configuration = DGTAuthenticationConfiguration(accountFields: .defaultOptionMask) {
            configuration.appearance = DGTAppearance()
            configuration.appearance.backgroundColor = UIColor.white
            configuration.appearance.accentColor = UIColor.tintColor()

            Digits.sharedInstance().authenticate(with: nil, configuration:configuration) {(session, error) in
                if session != nil {
                    let value = session!.authToken + ";" + session!.authTokenSecret
                    completion.setResult(["www.digits.com" : value as NSString])
                } else {
                    completion.setError(error!)
                }
            }
        }
        return completion.task
    }
}

Invoked by

    func handleDigitLogin() {
        let digitsIdentityProviderManager = DigitsIdentityProviderManager()
        let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.usEast1,
                                                                identityPoolId:Constants.Aws.CognitoPoolId,
                                                                identityProviderManager:digitsIdentityProviderManager)

        let serviceConfiguration = AWSServiceConfiguration(region: .usEast1, credentialsProvider: credentialsProvider)
        AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration

        let task = credentialsProvider.getIdentityId()
        task.continue(successBlock: { (task:AWSTask) -> Any? in
            if (task.error != nil ) {
                print("\(task.error)")
            } else {
                print("Task result: \(task.result)")
            }
            return nil
        })
    }

If I may make a request. I am guessing it's challenging to keep docs up to date with each release. It would be nice if the SDK sample contained tests for the supported public providers and a sample custom provider. It would be much easier to use a test as the reference for me.

@behrooziAWS
Copy link
Contributor

@georgel Once an identity becomes authenticated, you cannot get credentials against it unless you provide a valid token. The recommended approach is to instantiate your credentials provider in your AppDelegate because you can only set the defaultServiceConfiguration once. Whenever you need to access it you use AWSServiceManager.default().defaultServiceConfiguration.credentialsProvider If you are switching end users, you should use credentialsProvider.clearKeychain() this will blow away both your identity id and credentials. If you are switching between unauthenticated and authenticated you should use credentialsProvider.clearCredentials() because this will preserve your identity id. In other circumstances you shouldn't clear anything, i.e. if you relaunch the app and your credentials have expired the SDK will invoke your DigitsIdentityProviderManager.logins() to get a current token. To handle unauth and auth together, you should just have your DigitsIdentityProviderManager return a nil logins map when the user has never authenticated before otherwise call the Digits logic. If they sign out, you can start returning a nil logins map again. You should only call getIdentityId if you really need to, simply making calls to AWS services will get an identity id and credentials against that identity on demand if they aren't already present or have expired.

@behrooziAWS
Copy link
Contributor

Thank you for your patience! The documentation has been updated for using developer authenticated identities with iOS SDK 2.4.x. http://docs.aws.amazon.com/cognito/latest/developerguide/developer-authenticated-identities.html We will work to update the documentation for other identity providers, in the meantime there are a few examples of how to integrate with Facebook and Twitter in this thread.

@roymckenzie
Copy link

roymckenzie commented Dec 5, 2016

If I leave out the auth roles from the AWSCognitoCredentialsProvider the request to upload to S3 times out:

Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x171a45160 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=https://s3-eu-west-1.amazonaws.com/myserver-server/staging/avatar/1480977990.5395651.jpg, NSErrorFailingURLKey=https://s3-eu-west-1.amazonaws.com/myserver-server/staging/avatar/1480977990.5395651.jpg, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.}

If I include the roles:

Error Domain=com.amazonaws.AWSServiceErrorDomain Code=11 "(null)" UserInfo={Type=Sender, Message=Not authorized to perform sts:AssumeRoleWithWebIdentity, Code=AccessDenied, __text=(
    "\n    ",
    "\n    ",
    "\n    ",
    "\n  "
)}

Also in the developer authenticated identities Swift example, the identity provider is implemented using Objective-C which is of little help. It would be great to get the meat of that example in Swift.

@roymckenzie
Copy link

I seemed to have solved my problem for my implementation of a Developer Authenticated only support. For those who were having similar troubles to me here is my setup. Open to getting this cleaned up if anyone has any suggestions. (Swift 3):

final class AmazonIdentityProvider: AWSCognitoCredentialsProviderHelper {
    
    var cachedLogin: NSDictionary?
    
    // Handles getting the login
    override func logins() -> AWSTask<NSDictionary> {
        guard let cachedLogin = cachedLogin else {
            return getCredentials().continue({ credentialTask -> AWSTask<NSDictionary> in
                guard let credential = credentialTask.result else {
                    return AWSTask(result: nil)
                }
                let login: NSDictionary = ["cognito-identity.amazonaws.com": credential.token]
                self.cachedLogin = login
                return AWSTask(result: login)
            }) as! AWSTask<NSDictionary>
        }
        return AWSTask(result: cachedLogin)
    }
    
    // Handles getting a token from the server
    override func token() -> AWSTask<NSString> {
        return getCredentials().continue({ credentialTask -> AWSTask<NSString> in
            guard let credential = credentialTask.result else {
                return AWSTask(result: nil)
            }
            return AWSTask(result: credential.token as NSString)
        }) as! AWSTask<NSString>
    }
    
    // Handles getting the identity id
    override func getIdentityId() -> AWSTask<NSString> {
        return getCredentials().continue({ credentialTask -> AWSTask<NSString> in
            guard let credential = credentialTask.result else {
                return AWSTask(result: nil)
            }
            return AWSTask(result: credential.identityId as NSString)
        }) as! AWSTask<NSString>
    }
    
    // Gets credentials from server
    func getCredentials() -> AWSTask<AmazonCognitoCredential> { // AmazonCognitoCredential is a class I created to store credentials from my server
        let tokenRequest = AWSTaskCompletionSource<AmazonCognitoCredential>()
        
        // Replace this with your code that calls your backend
        Session.getAwsToken.makeRequest(failure: { error in
            guard let error = error else { return }
            tokenRequest.setError(error)
            print("Could not fetch AWS credentials from server: \(error)")
            
        }, completion: { credentials in
            guard let credentials = credentials else { return }
            tokenRequest.setResult(credentials)
        })
        
        return tokenRequest.task
    }
}
/// Handles uploads to AWS S3
struct S3ImageUploader {
    
    /// Singleton instance so that the credentials provider
    /// can cache AWS credentials without having to hit
    /// server constantly
    static let main = S3ImageUploader()
    
    private lazy var identityProvider: AmazonIdentityProvider = {
        return AmazonIdentityProvider(regionType: .euWest1, identityPoolId: IdentityPoolId, useEnhancedFlow: true, identityProviderManager: nil)
    }()
    
    private lazy var credentialsProvider: AWSCognitoCredentialsProvider = {
        return AWSCognitoCredentialsProvider(regionType: .euWest1, identityProvider: self.identityProvider)
    }()
    
    private lazy var serviceConfiguration: AWSServiceConfiguration = {
        return AWSServiceConfiguration(region: .usWest2, credentialsProvider: self.credentialsProvider)
    }()
    
    /// Set the default service configuration
    init() {
        // Set configuration
        AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration
    }

   fun doUpload() { 
       // Your implementation here
   }
}
final class AmazonCognitoCredential {
    let token: String
    let identityId: String
    
    init(token: String, identityId: String) {
        self.token = token
        self.identityId = identityId
    }
}

@isadon
Copy link
Contributor

isadon commented Dec 31, 2016

First me let me get this out of the way:

The AWS SDK is huge, VERY HARD to follow (if you practically didn't write it), IS NOT very well documented and has many bugs (many which I have uncovered).

To the iOS AWS SDK Team:

  • PLEASE improve the DOCS.
  • Keep the Docs Updated (at the very least the html docs pertaining this issue are STILL outdated to this day)
  • Stop breaking the SDK on minor version bumps!
  • Provide Better Support! Seriously, why is it me that has to provide this post? This should've been posted a LONG time ago by someone on the iOS AWS SDK team. It shouldn't have to be us developers going in here spending ridiculous hours to try to "decipher your code" and provide each other documentation for this massive sdk!

With that being said, after finally deciding to unpin my AWS SDK from v2.3.6 and dedicating some time to getting the latest SDK version working heres what I found after spending nearly ~8+ hrs dissecting the AWS SDK - alot of which was harder to debug in XCode due to all the callbacks implemented in the code. Enjoy 🎉!

To provide federated identities with cognito at the most basic level what has to happen is that your backend has to make a call to GetOpenIdToken to get a Cognito token for your Cognito Identity Pool. This token is returned to your iOS app and it is this token that will be used to authenticate with AWS.

To Handle Developer Federated Identities in the iOS Client the following Must Be Done

1. Create a Custom Identity Provider that SubClasses AWSCognitoCredentialsProviderHelper
Any references to [WinkAPI sharedManager].session imply checking if your app has a valid user session. Replace with your own calls to validate your app session

//
// WinkIdentityProvider.h
// Wink
//
// Created by Adonis Peralta on 5/26/15.
// Copyright (c) 2015 TouchSix, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <AWSCore/AWSIdentityProvider.h>

@interface WinkIdentityProvider : AWSCognitoCredentialsProviderHelper

@end

//
// WinkIdentityProvider.m
// Wink
//
// Created by Adonis Peralta on 5/26/15.
// Copyright (c) 2015 TouchSix, Inc. All rights reserved.
//

#import <AWSCore/AWSCore.h>
#import "WinkAPI.h"
#import "WinkIdentityProvider.h"
#import "Session.h"
#import "AWSToken.h"
#import "Person.h"

@interface WinkIdentityProvider ()

@property (nonatomic, copy) NSString *awsToken;

@end

@implementation WinkIdentityProvider

- (AWSTask *)token
{
  return [[AWSTask taskWithResult:nil] continueWithBlock:^id _Nullable(AWSTask * _Nonnull task) {
    return [self refresh];
  }];
}

// call to backend, called through entry point
- (AWSTask *)refresh
{
  if (![WinkAPI sharedManager].session) return [AWSTask taskWithError:[NSError errorWithDomain:kWinkErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"unable to refresh AWS token, no valid WinkAPI session."}]];
  
  AWSTaskCompletionSource *task = [AWSTaskCompletionSource taskCompletionSource];
  
  [[WinkAPI sharedManager] refreshAWSTokenWithCompletion:^(AWSToken *token, NSError *error) {
    if (error) {
      [task setError:error];
      return;
    }

    self.identityId = token.identityId;    
    self.awsToken = token.uid;
    [task setResult:self.awsToken];
  }];
  return task.task;
}

// tells aws in its api call if your user is authenticated or not - this correlates with the authRoleArn and unAuthRoleArn parameters of your identity pool
- (BOOL)isAuthenticated
{
  if ([WinkAPI sharedManager].session) return YES;
  return NO;
}

// override of the clear method of the identity provider so you can clean up your custom identity provider
- (void)clear
{
  [super clear];
  self.awsToken = nil;
  self.identityId = nil;
}

@end

2. Initialize and setup your AWS Credentials in a class implementation of your choosing

// AWSHandlers.m

#import "WinkIdentityProvider.h"
#import "AWSCore.h"
#import "AWSS3.h"

@interface AWSHandlers ()

@property (nonatomic, strong) AWSCognitoCredentialsProvider *awsCredentialsProvider;
@property (nonatomic, strong) AWSS3TransferManager *s3TransferManager;

@end

@implementation AWSHandlers

- (void)setupAWS
{
  // instantiate our custom identity provider
  WinkIdentityProvider *winkProvider = [[WinkIdentityProvider alloc] initWithRegionType:AWSRegionUSEast1
                                                                         identityPoolId:@"identity-pool-id-here"
                                                                        useEnhancedFlow:YES
                                                                identityProviderManager:nil];

  // instantiate the credentials provider with our customer identity provider
  self.awsCredentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
                                                                         identityProvider:winkProvider];

  AWSServiceConfiguration *defaultAWSConfig = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1 credentialsProvider:self.awsCredentialsProvider];
  [[AWSServiceManager defaultServiceManager] setDefaultServiceConfiguration:defaultAWSConfig];
  
  // setup a s3 transfer manager
  self.s3TransferManager = [AWSS3TransferManager defaultS3TransferManager];
  
  // change the verbosity level of the aws sdk if you choose
  //[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
}

// call on app logout AND login! If not, cached credentials which may or may not be valid can be used during an app reinstall, or first loading of your app (not resuming from background) 
- (void)invalidateAWSCredentialsAndHandlers
{
  [self cancelAllCurrentS3Operations];
  [self.awsCredentialsProvider clearKeychain];
  self.awsCredentialsProvider = nil;
  self.s3TransferManager = nil;
}

Custom Identity Provider

1. Initialize your custom identity provider via initWithRegionType:identityPoolId. Do not use the other methods that specify a identity provider manager as an identity provider manager is for when not using developer provided identities.

2. If the token you provided is invalid or EXPIRED the AWS SDK will attempt to refresh your token (via getIdentityId method) ONE more time. After that IF IT FAILS again the call that triggered the getting of AWS credentials (for example downloading an AWS image) will fail.

3. The Custom Identity Provider (BOOL)isAuthenticated method is needed to tell AWS in its api call if your user is authenticated or not. This correlates with the authRoleArn and unAuthRoleArn.

4. Override the Custom Identity Provider -(void)clear method to do any clean up of your custom identity provider variables if necessary.

AWSCognitoCredentials Initialization Info

After initializing and seting up the aws cognito credentials as described above in step #3 here are some quirks to know of about the AWS SDK:

  • **After app user logout AND LOGIN MAKE SURE to call the clearKeychain method on your AWSCognitoCredentialsProvider instance (which you should have a previously stored reference to)

**If you do not clear the provider keychain on login, the AWS SDK will attempt to use the keychain stored credentials instead of calling your backend for a valid token again. Failing to do so has very bad implications:

  • The stored credentials may be already expired or invalid - in this case the any AWS calls you make will simply FAIL and NOT retry getting a good valid token from your backend - When using stored credentials AWS does not retry getting the token.
  • The stored token may be valid but for another user - this allows access to the aws sdk calls for a user who may not have valid credentials anymore.
  • The above implications can be seen particularly on app reinstalls (user deletes app, then reinstalls within the Cognito Token expiration)

Update: The above post has been updated to factor a misunderstanding about the IdentityProviderManager class. Initially I believed it was part of the developer identity provider flow but it is not. This simplifies many of the findings I had found previously in regards to the use of enhancedFlows vs non-enhanced throughout the authentication process.

@ivanfervar
Copy link

Hello,
To login with Developer Authenticated Identities in SDK AWS v2.4.16 I have a ILDeveloperAuthenticatedIdentityProvider (:AWSCognitoCredentialsProviderHelper
) where has implemented “(AWSTask<NSString *> *)token” among others like AWS Doc explain, but this “token” function never called.
Why could this be happening?

@isadon
Copy link
Contributor

isadon commented Jan 11, 2017 via email

@karthiksaligrama
Copy link
Contributor

@Donileo i don't suggest you use the approach that you suggested

- (AWSTask *)logins
{
  if (![WinkAPI sharedManager].session) {
    return [AWSTask taskWithError:[NSError errorWithDomain:kWinkErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Unable to get a proper logins map, no valid wink session found"}]];
  }
  
  // NOTE: here we are using the AWSIdentityProviderAmazonCognitoIdentity which is @"". 
  // NEVER send back the identity provider name as the key here. Thats only ever meant to be used in the 
  // generation of the cognito token in the backend.
  return [AWSTask taskWithResult:@{AWSIdentityProviderAmazonCognitoIdentity: [WinkAPI sharedManager].session.awsToken.uid}]; 
}

is more of an approach used in older release (2.3.6 and before). It doesnt apply and will not work well if you use a 2.4.0 or higher version of sdk. From 2.4.0 the logins method should always return a current token. If you cache the token then it will eventually expire and the sdk will only call logins when it is refreshing the credentials.

I suggest you implement it the way @behrooziAWS suggested a couple of comments before yours by just overriding the method - (AWSTask <NSString*>) token which will make call to your backend and fetch the current token. You can read more about it here

@isadon
Copy link
Contributor

isadon commented Jan 11, 2017

@karthiksaligrama the approach I put above is based on analyzing how the SDK currently works in 2.4.0. I am overriding the logins method in the IdentityProviderManager not the IdentityProvider. In the SDK, when using the enhanced flow (non-api) the SDK will call the IdentityProviderManager's login method after it refreshes the token via the (getIdentityId call of the identityProvider - making a backend call) from the IdentityProvider.

As far as what I'm noticing thats what I'm supposed to do. Get a valid token, store it in [WinkAPI sharedManager].session. When the token expires (an expiration error condition happens in the SDK) my Identity provider will refresh the token and my [WinkAPI sharedManager].session will have the new token. Then the logins method in my provider manager will return a valid token. I have not been seeing any issues whatsoever with my method.

Also, if you override the - (AWSTask <NSString*>) token that method doesn't even get called unless you are in the non-enhanced flow of the IdentityProvider which by default you are not. Keep in mind the IdentityProviderManager MUST be implemented and passed in when initializing your custom identity provider via initWithRegionType:identityPoolId:useEnhancedFlow:identityProviderManager:.

Please see the lines starting with 262 in AWSIdentityProvider.m:

if (self.identityProviderManager && self.useEnhancedFlow) {
        self.cachedLogins = nil;
        return [[self getIdentityId] continueWithSuccessBlock:^id _Nullable(AWSTask<NSString *> * _Nonnull task) {
            if(self.cachedLogins){
                return [AWSTask taskWithResult:self.cachedLogins];
            }
            else {
                return [self.identityProviderManager logins];
            }
        }];
    }

    return [[self token] continueWithSuccessBlock:^id _Nullable(AWSTask<NSString *> * _Nonnull task) {
        if (!task.result) {
            return [AWSTask taskWithResult:nil];
        }
        NSString *token = task.result;
        return [AWSTask taskWithResult:@{self.identityProviderName : token}];
    }];

@karthiksaligrama
Copy link
Contributor

@Donileo i get that. You really shouldnt be setting the identityProviderManager at all. You should just extend the AWSCognitoCredentialsProviderHelper and override the method - (AWSTask *)token. If you override the IdentityProviderManager and return a cached token then you will run into problems. The token that you get from Cognito GetOpentIdTokenForDeveloperIdentity is only valid for 15 mins. If you are caching anything to not require the user to provide the user name and password again you should be caching a refresh token against your backend and use that refresh token and get an new OpenIdToken by calling GetOpentIdTokenForDeveloperIdentity each time.

When ever you are using developer authenticated identities you need to use the constructor.

- (instancetype)initWithRegionType:(AWSRegionType)regionType
                  identityProvider:(id<AWSCognitoCredentialsProviderHelper>)identityProvider;

@isadon
Copy link
Contributor

isadon commented Jan 11, 2017

@karthiksaligrama what is the IdentityProviderManager for then? Yes, if you were to use that method above only and not provide an identityProviderManager then yea you should implement the tokens method in your identityProvider. As far as my code, I did implement an IdentityProviderManager and of course when you do you should implement the logins method in your provider manager. Can you please explain whats wrong with my method. I believe both are fine. Seems like you are implying the token returned by the IdentityProvider tokens method is not the same as the one returned in the logins method of the IdentityProviderManager. As far as my backend, yes my backend does call GetOpentIdTokenForDeveloperIdentity and provide my client code with a new AWS token essentially refreshing my token, it does it successfully when it expires. What triggers the call? The AWS SDK detects when the token has expired, and places my backend call to refresh/provide a new token by calling GetOpentIdTokenForDeveloperIdentity. Don't see anything wrong there.

@behrooziAWS
Copy link
Contributor

The IdentityProviderManager is for when you are not using developer provided identities. If you are using developer provider identities at all, then the logins method on AWSCognitoCredentialsProviderHelper serves the role as the IdentityProviderManager.

@isadon
Copy link
Contributor

isadon commented Jan 12, 2017

@behrooziAWS got it :). As far as my code it still works, just that my refresh token call should also be in the token method and theres no need for my Custom IdentityProviderManager. Thanks! I'll update the post I made up above.

@ghost
Copy link

ghost commented Jan 12, 2017

@behrooziAWS Where is this differentiation on AWSIdentityProviderManager and AWSCognitoCredentialsProviderHelper noted in the documentation?

Are there any code samples that demonstrate how to use either (or both) of them as intended?

@isadon
Copy link
Contributor

isadon commented Jan 12, 2017

@kdbertel @behrooziAWS also if the AWSIdentityProviderManager is not part of the developer provider identities flow at at all why is it being asked for on the initialization to the AWSCognitoCredentialsProviderHelper? Please see method:
initWithRegionType:identityPoolId:useEnhancedFlow:identityProviderManager:

@behrooziAWS
Copy link
Contributor

@Donileo It is not required. If you have an identity provider manager that implements all of your other non developer authenticated providers, you can call it in the logins method in your AWSCognitoCredentialsProviderHelper.

@behrooziAWS
Copy link
Contributor

To avoid further confusion, no one from AWS will continue to respond to this thread. There are several implementations in the comments that fail for certain edge cases, don't properly refresh tokens, are more complicated than necessary or are for unrelated issues. Too many people are arriving at this thread, not following the official guidance and ending up with issues. The official guidance is here: http://docs.aws.amazon.com/cognito/latest/developerguide/developer-authenticated-identities.html Please open a new issue if you have issues with the official guidance.

@roymckenzie
Copy link

roymckenzie commented Jan 12, 2017 via email

@isadon
Copy link
Contributor

isadon commented Jan 12, 2017

@behrooziAWS I'm not stating that it "is" required. Simply, if its not, why surface it up so high up to designated initializer of the AWSCognigoCredentialsProviderHelper. The fact that its there makes it seem that it is. The official docs? Let us all hope they are good now :/.

@drsromero
Copy link

@behrooziAWS sorry! but your comment is not serious:

Firstly, I know this link because in a issue ticket, a AWS support guy told my team we should try with @Donileo information. After your comment, that response mysteriously disappeared.

Secondly, Open new issue? we have opened dozens tickets about cognito and IOS, We always receive the same reponse, "are you use clearKeychain? are you instantiating 2 sync clients? ahh OK!!!! I have passed the information to the Cognito Team". I opened a ticket in October and the last weekend (3 months) the support team guy ask us again... "are you use clearKeychain?" OMG!!!!!

Do you understand why people are arriving at this thread?

@isadon
Copy link
Contributor

isadon commented Jan 19, 2017

@drsromero exactly. Theres a reason why this thread exists and is the amount of comments you see; horrible documentation from AWS. My original post detailing how to properly implement the auth flow was the best that I could implement based on what I knew, and what I discovered debugging the SDK. I also figured I'd try and help others given how crazy this issue had become. After they provided new info (that the manager isn't for dev authenticated flows) it changed a lot of the information I had posted and the post is now updated to match this simpler flow. But if the manager is used then all the info in my original post all applies again. Again its not a coincidence this thread is what it is.

Btw, looking at the docs @behrooziAWS posted it seems that the IdentityProviderManager is to be used during dev federated identities when you want to provide auth for the standard providers as well. He seemed to imply the manager is not part of the flow at all, which definitely contradicts the link he posted. Once you add the manager, like in this case, I'd say my original post applies again.

@AlexeyPoldeo
Copy link

God, after spending HOURS trying to solve "Invalid token" error I've noticed that using wrong initializer for AWSCognitoCredentialsProvider.

It should be initWithRegionType:identityProvider: (not initWithRegionType:identityPoolId:identityProviderManager: !!!).

Also, there is no any difference between [logins] and [token] overriding for developer identities.
[token] is just helper to automatically build login map like @{ self.identityProviderName : token }

And, yes, it's better to pass self.identityProviderName to get logins key for developer identities since it returns "cognito-identity.amazonaws.com"

Hope it helps someone in trouble...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question General question
Projects
None yet
Development

No branches or pull requests