aws-amplify / aws-sdk-ios

AWS SDK for iOS. For more information, see our web site:
https://aws-amplify.github.io/docs
Other
1.68k stars 880 forks source link

Empty Refresh Token after an idle hour #645

Closed npalethorpe closed 6 years ago

npalethorpe commented 7 years ago

I've implemented AWS SDK Objective C into my project and all appears to be working correctly, however after an hour of non use, getSession will return an object back containing all but the Refresh Token (which is expired at this point according to the expirationTime property).

My setup does not use the delegate calls as it just doesn't fit into the flow of my app model - so am I missing something or is this a bug? I can see that getSession is refreshing the tokens whilst I am actively using my app, its just whilst I am idle for that hour period.

The result is that the user stays logged in but calls to the API Gateway and other items using the defaultServiceConfiguration will fail until I log the user out and get them to sign back in.

I haven't tried this with the Android Java sdk yet but I'm going to presume I'm going to run into the same issue there too!

Thanks for any help.

behrooziAWS commented 7 years ago

If you don't implement the delegate and the refresh token is expired, the SDK doesn't have any way of prompting the user for their username and password again and has no way of issuing new tokens. Did you set your refresh tokens to only be valid for 1 hour when you configured your User Pool in the console? If not, the SDK should be able to refresh without the username/password until the refresh token expires. If so, you will need to call getSession and provide the username/password again.

npalethorpe commented 7 years ago

Thanks for the quick response. Turns out I got my information a little wrong - the times the refresh code comes back as empty it does appear to pick itself back up and continues working. However I left the app unopened (note that I am using the iOS simulator) for likely 17+ hours and when I came back I made the usual getSession call which returns all three tokens however the API gateway just no longer acknowledges the credentials until I log out and log back in.

The API returns an error message of:

@"User: arn:aws:sts::47*********9:assumed-role/Cognito_MyAppUsersUnauth_Role/CognitoIdentityCredentials is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:us-east-1:********6789:91qkshfe93/prod/POST/getcc"

As I mention; if I sign out and back in again then it all springs back into life. The User Pool is setup with a refresh token that expires after 30 days.

I'm hoping someones going to just point out something stupid I'm doing. Below is the code I'm running on app start up to pick up an existing authenticated user and set things back up again:

AWSCognitoIdentityUserPool *pool;
  AWSCognitoIdentityUser *user;
  AWSServiceConfiguration *serviceConfiguration;
  AWSCognitoCredentialsProvider *credentialsProvider;

  // Setup the pool
  AWSCognitoIdentityUserPoolConfiguration *configuration = [[AWSCognitoIdentityUserPoolConfiguration alloc]
      initWithClientId:CognitoIdentityUserPoolAppClientId
      clientSecret:CognitoIdentityUserPoolAppClientSecret
      poolId:CognitoIdentityUserPoolId];

  // The Credentials Provider holds our identity pool which allows access to AWS resources
  credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:CognitoIdentityUserPoolRegion identityPoolId:CognitoIdentityPoolId];

  // We can then set this as the default configuration for all AWS SDKs
  serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:CognitoIdentityUserPoolRegion credentialsProvider:credentialsProvider];

  [AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfiguration userPoolConfiguration:configuration forKey:USER_POOL_NAME];

  // Get the pool object now
  pool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:USER_POOL_NAME];

  // Get the user from the pool
  user = [pool currentUser];

  // Make a call to get hold of the users session
  [[user getSession] continueWithBlock:^id(AWSTask<AWSCognitoIdentityUserSession *> *task) {
    if (task.error) {
      NSLog(@"Could not get user session. Error: %@", task.error);

      callback(@[@"", @(CognitoResponseNo)]);

    } else {
      NSLog(@"Successfully retrieved user session data");

      // Get the session object from our result
      AWSCognitoIdentityUserSession *session = (AWSCognitoIdentityUserSession *) task.result;

     // Build a token string
      NSString *key = [NSString stringWithFormat:@"cognito-idp.%@.amazonaws.com/%@", CognitoIdentityUserPoolRegionString, CognitoIdentityUserPoolId];
      NSString *tokenStr = [session.idToken tokenString];
      NSDictionary *tokens = [[NSDictionary alloc] initWithObjectsAndKeys:tokenStr, key,  nil];

      CognitoPoolIdentityProvider *idProvider = [[CognitoPoolIdentityProvider alloc] init];
      [idProvider addTokens:tokens];

      AWSCognitoCredentialsProvider *creds = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:CognitoIdentityUserPoolRegion identityPoolId:CognitoIdentityPoolId identityProviderManager:idProvider];

      AWSServiceConfiguration *serviceConfig = [[AWSServiceConfiguration alloc] initWithRegion:CognitoIdentityUserPoolRegion credentialsProvider:creds];

      // This sets the default service configuration to allow the API gateway access to user authentication
      AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = serviceConfig;

      // Register the pool
      [AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfig userPoolConfiguration:configuration forKey:USER_POOL_NAME];

      [[creds getIdentityId] continueWithBlock:^id _Nullable(AWSTask<NSString *> * _Nonnull task) {
        if (task.error) {
          callback(@[[NSString stringWithFormat:@"Could not get identity id: %@", task.error], @(CognitoResponseNo)]);
        } else {
          callback(@[@"", @(CognitoResponseYes)]);
        }
        return nil;
      }];

    }
    return nil;
  }];

Thanks for any help or advice - I'm new to Objective C and finding it a little overwhelming also understanding the correct authentication flow for Cognito!

rmartell commented 7 years ago

I have this same issue, and I believe that I've identified two different problems in here:

These are in getSession() when it is trying to refresh:

Issue 1 When it actually calls this:

return [[self.pool.client initiateAuth:request] continueWithBlock:^id _Nullable(AWSTask<AWSCognitoIdentityProviderInitiateAuthResponse *> * _Nonnull task) {

in AWSCognitoIdentityUser.m, it fails. This is because it signs the request, and the current access token is invalid (expiredToken). The refresh does work if you nil out the requestInterceptors for this call (which you have to do in the debugger - they are set in assignProperties in AWSNetworking.m, from the configuration).

IF you nil the requestInterceptors array out, the initiateAuth succeeds, and it updates the accessToken, refreshToken, and idToken..

Issue 2 When it does return the expiredToken code, it looks like it's trying to test for that:

return [[self.pool.client initiateAuth:request] continueWithBlock:^id _Nullable(AWSTask<AWSCognitoIdentityProviderInitiateAuthResponse *> * _Nonnull task) {
  if(task.error){
    //If this token is no longer valid, fall back on interactive auth.
    if(task.error.code == AWSCognitoIdentityProviderErrorNotAuthorized) {
      return [self interactiveAuth];
    } else {
      return task;
    }
}

But the error that is being returned is:

2017/05/03 23:01:38:092  Unable to refresh. Error is [Error Domain=com.amazonaws.AWSCognitoIdentityErrorDomain Code=8 "(null)" UserInfo={message=Invalid login token. Token expired: 1493855918 >= 1493850102, __type=NotAuthorizedException}]

And AWSCognitoIdentityProviderErrorNotAuthorized is ~16, not 8. So it won't call the interactiveAuth like it should.

(edited to remove remove 2nd issue, which was mine, and add clarification)

rmartell commented 7 years ago

FYI-

I fixed Issue 1, but it's not in a method that can be submitted as a patch. Basically, I exposed client (from AWSCognitoIdentityUserPool) and configuration (from AWSCognitoIdentityProvider). Then I head patched the signature code to explicitly not sign if the request is AWSCognitoIdentityProviderService.InitiateAuth.

The problem is that the requestInterceptors isn't exposed at a level that will let me to do the above in a clean manner, and there is no way to override them on a per-request basis.

This now refreshes properly.

I suspect Issue 2 is a case that happens if the refresh token has expired, and it might be the legitimate error condition in that case.

If an AWS engineer would suggest a method to patch this, I'll definitely do the work here. My possible thoughts:

1) Make AWSSignatureV4Signer check to see if X-Amz-Target is AWSCognitoIdentityProviderService.InitiateAuth, and if so, not sign. (Simplest, but ugly)

2) Add a boolean value to AWSNetworkingRequest to sign or not; default is true. This seems like it would change a lot of things. I don't like this one, because it implies knowing what the requestInterceptors are.

3) Provide a method to override requestInterceptors on a AWSNetworkingRequest basis. There appears to be precedent for this in assignProperties in AWSNetworkingRequest. Something like:

if(!self.requestInterceptors && configuration.requestInterceptors) {
    self.requestInterceptors = configuration.requestInterceptors;
}

Then, in this one call, you would explicitly set the requestInterceptors to not include the signing request. This is probably the cleanest implementation.

behrooziAWS commented 7 years ago

Hi @rmartell, @npalethorpe

Sorry for the delay, I missed the updates to this issue. It appears the reason that both your issues are happening are because you are specifying a credentialsProvider in the AWSServiceConfiguration for your AWSCognitoIdentityUserPool object. This is causing the requests to be signed, and to make calls to Cognito Identity using an expired token during the refresh. Although you need a AWSServiceConfiguration like this for most services, AWSCognitoIdentityUserPool is different. You should have a separate AWSServiceConfiguration specifically for your AWSCognitoIdentityUserPool object with a nil credentials provider as all the calls are unauthenticated or authenticated with an access token, not SigV4 credentials.

Here is an example:

AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1 credentialsProvider:nil];

More details on the correct configuration here:

http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html

And here:

http://docs.aws.amazon.com/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-ios-sdk.html

stale[bot] commented 6 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] commented 6 years ago

This issue has been automatically closed because of inactivity. Please open a new issue if are still encountering problems.