API Gateway generated SDK for iOS (Objective-C) with Cognito User Pool for authorized users

974 views Asked by At

I have deployed an API using AWS API Gateway and I am trying to access it through an iOS device. Some endpoints support unauthorized users and I have no trouble accessing them but others don't and I can't manage to query them. I use the new Cognito User Pool feature for authentication which works fine when using a web application (or postman).

First, even though some of the endpoints are protected (like in the following picture of the console), when I deploy the API and generate the SDK for iOS (Objective-C), I can read in the README file: "All endpoints do not require authorization."

enter image description here

Then, when I run the following authentication code from AWS documentation, everything seems to work just fine:

AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionEUWest1 credentialsProvider:nil];
AWSCognitoIdentityUserPoolConfiguration *userPoolConfiguration = [[AWSCognitoIdentityUserPoolConfiguration alloc] initWithClientId:ClientId  clientSecret:nil poolId:UserPoolId];
[AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfiguration userPoolConfiguration:userPoolConfiguration forKey:@"UserPool"];
AWSCognitoIdentityUserPool *pool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:@"UserPool"];

AWSCognitoIdentityUser *user = [pool getUser];
[[user getSession:username password:password validationData:nil] continueWithBlock:^id(AWSTask<AWSCognitoIdentityUserSession *> *task) {
    if (task.error) {
        NSLog(@"Could not get user session. Error: %@", task.error);
    } else if (task.exception) {
        NSLog(@"Could not get user session. Exception: %@", task.exception);
    } else {
        NSLog(@"Successfully retrieved user session data");
        AWSCognitoIdentityUserSession *session = (AWSCognitoIdentityUserSession *) task.result;

        NSMutableString *poolId = [[NSMutableString alloc] initWithString:@"cognito-idp.eu-west-1.amazonaws.com/"];
        [poolId appendString:UserPoolId];
        NSString *tokenStr = [session.idToken tokenString];
        NSDictionary *tokens = [[NSDictionary alloc] initWithObjectsAndKeys:tokenStr, poolId, nil];



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

        AWSCognitoCredentialsProvider *credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionEUWest1 identityPoolId:IdentityPoolId identityProviderManager:idProvider];

        [credentialsProvider clearKeychain];
        [credentialsProvider clearCredentials];

        AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionEUWest1
                                                                                                          credentialsProvider:credentialsProvider];

        [[credentialsProvider getIdentityId] continueWithBlock:^id _Nullable(AWSTask<NSString *> * _Nonnull task) {
            if (task.error) {
                NSLog(@"Could not get identity id: %@", task.error);
            } else if (task.exception) {
                NSLog(@"Could not get identity id: %@", task.exception);
            } else {
                NSLog(@"Identity id: %@", task.result);
            }

            return nil;
         }];
    }

    return nil;
}];

I implemented CognitoPoolIdentityProvider as specified in this post.

@interface CognitoPoolIdentityProvider ()
@property (strong, nonatomic) NSDictionary *tokens;
@end

@implementation CognitoPoolIdentityProvider

    - (AWSTask<NSDictionary *> *)logins {
        return [AWSTask taskWithResult:self.tokens];
    }

    - (void)addTokens:(NSDictionary *)tokens {
        self.tokens = tokens;
    }

@end

I manage to get a proper token (tested using postman) and a user id:

2016-12-27 12:43:35.760 AskHub[26625:10037234] AWSiOSSDK v2.4.11 [Debug] AWSURLResponseSerialization.m line:63 | -[AWSJSONResponseSerializer responseObjectForResponse:originalRequest:currentRequest:data:error:] | Response body: {"IdentityId":"eu-west-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"} 2016-12-27 12:43:35.766 AskHub[26625:10037234] Identity id: eu-west-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

However, when I run the following code to hit a protected endpoint, CloudFront considers that I am not authenticated.

APIHealthClient *apiClient = [APIHealthClient defaultClient];
APIAskHubRequest *initReq = [[APIAskHubRequest alloc] init];
initReq.message = @"FIN";

NSLog(@"Sending initial request");
[[apiClient webhooksAskhubPost:initReq] continueWithBlock:^id(AWSTask *task) {
    if (task.error) {
        NSLog(@"Could not send initial request: %@", task.error);
    } else if (task.exception) {
        NSLog(@"Could not send initial request: %@", task.exception);
    } else {
        NSLog(@"Successfully sent initial request");
    }

    return nil;
}];

Reponse:

2016-12-27 12:56:25.247 AskHub[26784:10046562] Could not send initial request: Error Domain=com.amazonaws.AWSAPIGatewayErrorDomain Code=1 "(null)" UserInfo={HTTPBody={ message = Unauthorized; }, HTTPHeaderFields={type = immutable dict, count = 9, entries => 3 : Via = {contents = "X.X XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.cloudfront.net (CloudFront)"} 4 : x-amzn-ErrorType = {contents = "UnauthorizedException"} 5 : Content-Type = {contents = "application/json"} 6 : Content-Length = {contents = "27"} 7 : Connection = {contents = "keep-alive"} 8 : x-amzn-RequestId = {contents = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"} 9 : Date = {contents = "Tue, 27 Dec 2016 11:56:25 GMT"} 10 : X-Cache = {contents = "Error from cloudfront"} 11 : X-Amz-Cf-Id = {contents = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"} } }

Am I missing something here? Does the automatically generated SDK support User Pool authentication?

2

There are 2 answers

0
Hatim Khouzaimi On BEST ANSWER

I figured out that there is a confusion here between two types of authentication modes: using a credentials provider or just adding the JWT token to the "Authorization" header parameter (or your custom parameter name if you specified one when creating your authorizer).

The first method did not work for me, even though it is the only one that I have found in the documentation when it comes to using the iOS SDK. The second method works pretty fine, but to be able to use it without hacking into the SDK, don't forget to add the "Authorization" header when you specify your API in API Gateway.

14
Bruce0 On

Based upon the code I see, you are clearing credentials but never getting credentials. On quick review it looks like you are setting up the service configuration and authentication correctly, but you never call credentials.

If that was the case, even though your application would authenticate, the credentials provider would never go get the logins dictionary.

(You can verify this in the AWS Console, if you look at the identityId you are trying to use to access the resource, you can look at the "logins" count. If it is 0, you are not logged in.)

In order to be authenticated you need to have the credentials provider call your identityProviderManager "logins" method.

That call happens when you do "getCredentialsForIdentity" which is "credentials" in the SDK.

Typically the sequence is GetID followed by GetCredentialsForId. In the IOS SDK, the GetId happens with "getSession" (so you should have that), and the GetCredentials happens with "credentials"

(Note that there are different SDKs with different naming in each (Mobile Hub vs IOS SDK for example) but all that matters is that your credentials provider gets a login dictionary. With verbose logging you should actually see the "logins" dictionary logged, which will prove to you that a logins dictionary is being provided to the credentials provider).