Aws-sdk-ios: federatedSignIn and unauthorized requests

Created on 16 Oct 2019  路  2Comments  路  Source: aws-amplify/aws-sdk-ios

Hello, I am trying to implement federated signIn using AppSync. I create everything as described in documentation, but for some unknown reasons I still can't send requests with app sync.

I create stack using snippet:

let cacheConfiguration = try? AWSAppSyncCacheConfiguration()

let appSyncServiceConfig = try? AWSAppSyncServiceConfig()
let appSyncConfig = try? AWSAppSyncClientConfiguration(
            appSyncServiceConfig: AWSAppSyncServiceConfig(),
            userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
            cacheConfiguration: cacheConfiguration)

appSyncClient = try? AWSAppSyncClient(appSyncConfig: appSyncConfig!)

using standard implementation of MyCognitoUserPoolsAuthProvider:

class MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProviderAsync {
    func getLatestAuthToken(_ callback: @escaping (String?, Error?) -> Void) {
        AWSMobileClient.sharedInstance().getTokens { (tokens, error) in
            if error != nil {
                callback(nil, error)
            } else {
                callback(tokens?.idToken?.tokenString, nil)
            }
        }
    }
}

Next, I register: (result.token and result.identityId are provided by my backend)

 AWSMobileClient.sharedInstance().federatedSignIn(
                providerName: IdentityProvider.developer.rawValue,
                token: result.token,
                federatedSignInOptions: FederatedSignInOptions(cognitoIdentityId: result.identityId)) { (userState, error) in
                    if let error = error as? AWSMobileClientError {
                        print(error.localizedDescription)
                    }
                    if let userState = userState {
                        print("Status: \(userState.rawValue)")
                    }
            }

which results with userState == signedIn. Every app sync request is finishing with unauthorized error. I've created breakpoint inside MyCognitoUserPoolsAuthProvider, where I saw that AWSMobileClient.sharedInstance().getTokens always results with empty token and error. I've also tried to use alternative implementation of this class. It is returning token created by my backend, but I also get unauthorized error:

class MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProviderAsync {
    func getLatestAuthToken(_ callback: @escaping (String?, Error?) -> Void) {
        AWSMobileClient.sharedInstance().logins().continueWith { (result) -> Any? in
            callback(result.result?["cognito-identity.amazonaws.com"] as? String, nil)
            return nil
        }
    }
}

I currently have no idea what I'm doing wrong and how can I fix my problem. Can anybody help me solve it?

Which AWS Services are you utilizing?
AWSMobileClient, AWSAppSync

Environment(please complete the following information):

  • AWSAppSync [2.14.2]
  • AWSMobileClient [2.10.2]
  • Dependency Manager: Cocoapods
  • Swift Version: 5.1

Device Information (please complete the following information):

  • Device: [iPhone XR Simulator]
  • iOS Version: [iOS 13]
  • Specific to simulators: no
appsync closing-soon-if-no-response mobile client question

Most helpful comment

@bananita

Sorry for the trouble you're having with this--unfortunately, Auth is a very confusing issue at the best of times and Cognito is a pretty complicated system.

tl;dr: If you use Cognito User Pools and Hosted UI, everything should "just work" for your use case. Otherwise, you can use a custom UI and invoke showSignIn with an identityProvider in your HostedUIOptions.

Discussion

It sounds like you're using UserPools as your primary Authorization method for your AppSync installation, based on the fact that your code snippet shows you configuring with userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider.

In addition, you're using the federatedSignIn API to try to Authenticate into your UserPools, using your own custom UI. I believe you're making the reasonable assumption that if one of your users signs into your UserPool via, for example, Facebook, then you'll be able to use UserPools to authenticate the user for your AppSync API.

Unfortunately, that's not quite how Cognito User Pools works with federation. You'll have to use a different approach.

In the interest of laying some groundwork for potential future documentation updates, I'm going to go into more detail than is strictly warranted by your question, and perhaps cover some ground you're already familiar with.


Part 1: Authentication

Step 1 is to authenticate the user--that is, provide some assurance that a given user is who they say they are, (for example, based on their ability to give a username and password). Cognito User Pools is one way to do this: it allows you to set up a user store--a directory of users and passwords that represent known users in your system.

Other ways exist: for example, you could use Facebook to vouch for a user via Federated sign in. At the end of that process, Cognito would know that Facebook recognizes the user. However, neither the AWSMobileClient drop in UI, nor the AWSMobileClient.signIn method, will create a User Pool record for the user with this method.

This last piece is critical. Currently, the only way to get a User Pool record from a Federated Sign In flow is to use Cognito's Hosted UI. HostedUI has the necessary setups in place to talk to Federated providers to set up a User Pool record. The AWSMobileClient.federatedSignIn method is only used to federate into Cognito Identity Pools (see below), not for User Pools. And neither the no-option flavor of AWSMobileClient.showSignIn() (which displays our drop-in UI) nor the AWSMobileClient.signIn method create a User Pools user.

What you want is to use HostedUI, by invoking AWSMobileClient.showSignIn(hostedUIOptions:). Despite the name, using HostedUI does give you some flexibility to use your own UI, as we'll discuss below.

Part 2: Authorization

AppSync requires some kind of credentials to authorize that a user has permissions to access your API. The four authorization modes AppSync recognizes are:

  1. API Key: An API Key static string that is passed as part of the HTTP request header.
  2. User Pools: JWT Tokens provided by Cognito User Pools
  3. IAM: AWS Credentials provided by Cognito Identity Pools via an IAM assumed role
  4. OIDC: JWT Tokens provided by some third-party OIDC-compliant provider

Each of these methods have some prerequisites:

  • API Key requires very little setup--just pasting a static string in your awsconfiguration.json file. However, it is also the least secure, and only suitable for unauthenticated APIs.
  • User Pools: In order to get JWT tokens to authorize an AppSync API call, your user has to have a full record in Cognito User Pools. As mentioned above, only sign ups through Cognito Hosted UI currently have that.
  • IAM: This is the most flexible authorization mode, as it uses Cognito Identity Pools to assign a user to either an "authorized" or "unauthorized" IAM role. Behind the scenes, the SDK assumes the appropriate role, and sends temporary credentials to AppSync to authorize an API call. Cognito Identity Pools and IAM roles are also suitable for calling other AWS services like S3 and Pinpoint, and also work with social providers like Facebook.
  • OIDC: This requires you an OIDC compliant provider to respond to user login requests, and respond to service authorization requests.

Options for Federating into User Pools

Your best option is to use AWSMobileClient.showSignIn(navigationController:signInUIOptions:hostedUIOptions:_:), and pass a HostedUIOptions object to control the behavior. You can do this one of two ways:

Option 1: Full Hosted UI experience

This is the easiest, as it uses Cognito's HostedUI web page to provide a secure login experience for your users.

let hostedUIOptions = HostedUIOptions(
    /* Configure as appropriate, but do not specify `identityProvider` */
)

// Present the Hosted UI sign in.
AWSMobileClient.default().showSignIn(navigationController: navigationController) { userState, error in
    if let error = error as? AWSMobileClientError {
        print(error.localizedDescription)
        return
    }
    guard let userState = userState else {
        print("userState unexpectedly nil")
        return
    }
    print(userState)
}

Option 2: Custom UI using Hosted UI for federation

As described here

@IBOutlet @objc func didTapSignInWithFacebook(){
    let hostedUIOptions = HostedUIOptions(scopes: ["openid", "email"], identityProvider: "Facebook")
    AWSMobileClient.default().showSignIn(navigationController: self.navigationController!,
                                         hostedUIOptions: hostedUIOptions) { userState, error in
        if let error = error as? AWSMobileClientError {
            print(error.localizedDescription)
            return
        }
        guard let userState = userState else {
            print("userState unexpectedly nil")
            return
        }
        print(userState)
    }

}

Finally, I'm tagging @kneekey23 and @undefobj to clarify/correct anything I've missed above.

Hope this helps.

All 2 comments

@bananita

Sorry for the trouble you're having with this--unfortunately, Auth is a very confusing issue at the best of times and Cognito is a pretty complicated system.

tl;dr: If you use Cognito User Pools and Hosted UI, everything should "just work" for your use case. Otherwise, you can use a custom UI and invoke showSignIn with an identityProvider in your HostedUIOptions.

Discussion

It sounds like you're using UserPools as your primary Authorization method for your AppSync installation, based on the fact that your code snippet shows you configuring with userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider.

In addition, you're using the federatedSignIn API to try to Authenticate into your UserPools, using your own custom UI. I believe you're making the reasonable assumption that if one of your users signs into your UserPool via, for example, Facebook, then you'll be able to use UserPools to authenticate the user for your AppSync API.

Unfortunately, that's not quite how Cognito User Pools works with federation. You'll have to use a different approach.

In the interest of laying some groundwork for potential future documentation updates, I'm going to go into more detail than is strictly warranted by your question, and perhaps cover some ground you're already familiar with.


Part 1: Authentication

Step 1 is to authenticate the user--that is, provide some assurance that a given user is who they say they are, (for example, based on their ability to give a username and password). Cognito User Pools is one way to do this: it allows you to set up a user store--a directory of users and passwords that represent known users in your system.

Other ways exist: for example, you could use Facebook to vouch for a user via Federated sign in. At the end of that process, Cognito would know that Facebook recognizes the user. However, neither the AWSMobileClient drop in UI, nor the AWSMobileClient.signIn method, will create a User Pool record for the user with this method.

This last piece is critical. Currently, the only way to get a User Pool record from a Federated Sign In flow is to use Cognito's Hosted UI. HostedUI has the necessary setups in place to talk to Federated providers to set up a User Pool record. The AWSMobileClient.federatedSignIn method is only used to federate into Cognito Identity Pools (see below), not for User Pools. And neither the no-option flavor of AWSMobileClient.showSignIn() (which displays our drop-in UI) nor the AWSMobileClient.signIn method create a User Pools user.

What you want is to use HostedUI, by invoking AWSMobileClient.showSignIn(hostedUIOptions:). Despite the name, using HostedUI does give you some flexibility to use your own UI, as we'll discuss below.

Part 2: Authorization

AppSync requires some kind of credentials to authorize that a user has permissions to access your API. The four authorization modes AppSync recognizes are:

  1. API Key: An API Key static string that is passed as part of the HTTP request header.
  2. User Pools: JWT Tokens provided by Cognito User Pools
  3. IAM: AWS Credentials provided by Cognito Identity Pools via an IAM assumed role
  4. OIDC: JWT Tokens provided by some third-party OIDC-compliant provider

Each of these methods have some prerequisites:

  • API Key requires very little setup--just pasting a static string in your awsconfiguration.json file. However, it is also the least secure, and only suitable for unauthenticated APIs.
  • User Pools: In order to get JWT tokens to authorize an AppSync API call, your user has to have a full record in Cognito User Pools. As mentioned above, only sign ups through Cognito Hosted UI currently have that.
  • IAM: This is the most flexible authorization mode, as it uses Cognito Identity Pools to assign a user to either an "authorized" or "unauthorized" IAM role. Behind the scenes, the SDK assumes the appropriate role, and sends temporary credentials to AppSync to authorize an API call. Cognito Identity Pools and IAM roles are also suitable for calling other AWS services like S3 and Pinpoint, and also work with social providers like Facebook.
  • OIDC: This requires you an OIDC compliant provider to respond to user login requests, and respond to service authorization requests.

Options for Federating into User Pools

Your best option is to use AWSMobileClient.showSignIn(navigationController:signInUIOptions:hostedUIOptions:_:), and pass a HostedUIOptions object to control the behavior. You can do this one of two ways:

Option 1: Full Hosted UI experience

This is the easiest, as it uses Cognito's HostedUI web page to provide a secure login experience for your users.

let hostedUIOptions = HostedUIOptions(
    /* Configure as appropriate, but do not specify `identityProvider` */
)

// Present the Hosted UI sign in.
AWSMobileClient.default().showSignIn(navigationController: navigationController) { userState, error in
    if let error = error as? AWSMobileClientError {
        print(error.localizedDescription)
        return
    }
    guard let userState = userState else {
        print("userState unexpectedly nil")
        return
    }
    print(userState)
}

Option 2: Custom UI using Hosted UI for federation

As described here

@IBOutlet @objc func didTapSignInWithFacebook(){
    let hostedUIOptions = HostedUIOptions(scopes: ["openid", "email"], identityProvider: "Facebook")
    AWSMobileClient.default().showSignIn(navigationController: self.navigationController!,
                                         hostedUIOptions: hostedUIOptions) { userState, error in
        if let error = error as? AWSMobileClientError {
            print(error.localizedDescription)
            return
        }
        guard let userState = userState else {
            print("userState unexpectedly nil")
            return
        }
        print(userState)
    }

}

Finally, I'm tagging @kneekey23 and @undefobj to clarify/correct anything I've missed above.

Hope this helps.

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bradgmueller picture bradgmueller  路  5Comments

cornr picture cornr  路  4Comments

dougboberg picture dougboberg  路  5Comments

premiumbosslimited picture premiumbosslimited  路  3Comments

pawlowskialex picture pawlowskialex  路  4Comments