aws-amplify / aws-sdk-android

AWS SDK for Android. For more information, see our web site:
https://docs.amplify.aws
Other
1.03k stars 549 forks source link

Throwing `The security token included in the request is invalid exception` when using iot service to attach policy #3289

Closed ininmm closed 1 year ago

ininmm commented 1 year ago

State your question

I am using Custom Auth with Amplify (AuthFlowType.CUSTOM_AUTH_WITHOUT_SRP) to sign in and get a credential. And I need to set the credential to AWSIotClient sothat I can use MQTT service. I've followed the way that @ZubairAkber provided in the issue. Everything work fine in the beginning, however it started to throw exception if I keep calling AWSIotClient(newCredentialsProvider).attachPolicy(attachReq) after some hour later.

And the exception which crash at attachPolicy:

com.amazonaws.AmazonServiceException: The security token included in the request is invalid. (Service: AWSIot; Status Code: 403; Error Code: UnrecognizedClientException; Request ID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).

Which AWS Services are you utilizing?

AWS Custom Auth(with Amplify) AWS Iot Service with MQTT

Provide code snippets (if applicable)

Here is my code:

class CognitoAWSCredentialsProvider() : AWSCredentialsProvider {
    var profile = Profile()

    override fun getCredentials(): AWSCredentials {
        val latch = CountDownLatch(1)
        var sdkCredentials: AWSCredentials? = null

        try {
            Amplify.Auth.fetchAuthSession(
                { authSession ->
                    sdkCredentials = ((authSession as AWSCognitoAuthSession).awsCredentialsResult.value as? AWSTemporaryCredentials)?.let {
                        Timber.i("fetchSession sdkCredentials: awsSessionToken: ${it.sessionToken}, awsAccessKeyId: ${it.accessKeyId}, awsSecretKey: ${it.secretAccessKey}")
                        BasicAWSCredentials(
                            it.accessKeyId, it.secretAccessKey
                        )
                    }
                    Timber.i("sdkCredentials: ${sdkCredentials?.awsAccessKeyId}, ${sdkCredentials?.awsSecretKey}")
                    latch.countDown()
                },
                {
                    // can better handle this exception and pass it down to the throwing call below
                    latch.countDown()
                }
            )
        } catch (e: Exception) {
            Timber.e(e)
            val builder = AuthFetchSessionOptions.builder()
            builder.forceRefresh(true)
            Amplify.Auth.fetchAuthSession(
                builder.build(),
                { authSession ->
                    sdkCredentials = ((authSession as AWSCognitoAuthSession).awsCredentialsResult.value as? AWSTemporaryCredentials)?.let {
                        Timber.i("fetchSession sdkCredentials: awsSessionToken: ${it.sessionToken}, awsAccessKeyId: ${it.accessKeyId}, awsSecretKey: ${it.secretAccessKey}")
                        BasicAWSCredentials(
                            it.accessKeyId, it.secretAccessKey
                        )
                    }
                    Timber.i("sdkCredentials: ${sdkCredentials?.awsAccessKeyId}, ${sdkCredentials?.awsSecretKey}")
                    latch.countDown()
                },
                {
                    // can better handle this exception and pass it down to the throwing call below
                    latch.countDown()
                }
            )
        }

        // wait for fetchAuthSession to return
        latch.await()

        // return captured credentials or throw
        return sdkCredentials ?: throw IllegalStateException("Failed to get credentials")
    }

    override fun refresh() {
        // refresh
    }
}

    fun initIot(accessKey: String, secretKey: String, sessionToken: String) {
        credentialsProvider = CognitoAWSCredentialsProvider()
        if (::mqttManager.isInitialized) return
        Timber.i("[Remote] init Mqtt.")
        mqttManager = AWSIotMqttManager(parameter.identityId, EndPoint)
    }

    suspend fun attachPolicy() {
        val attachPolicyReq = AttachPolicyRequest()
        val serviceType = ServiceType.getServiceType(currentServiceType)
        attachPolicyReq.policyName = serviceType.policy
        Timber.i("attachPolicy id: ${parameter.identityId}")
        attachPolicyReq.target = parameter.identityId
        Timber.i("credentialsProvider.credentials: ${credentialsProvider.credentials.awsAccessKeyId}, ${credentialsProvider.credentials.awsSecretKey}")
        val iotAndroidClient = AWSIotClient(credentialsProvider.credentials)
        iotAndroidClient.setRegion(Region.getRegion(IotRegion)) // name of your IoT Region such as "us-east-1"
        iotAndroidClient.attachPolicy(attachPolicyReq)
    }

Environment(please complete the following information):

Device Information (please complete the following information):

If you need help with understanding how to implement something in particular then we suggest that you first look into our developer guide. You can also simplify your process of creating an application, as well as the associated backend setup by using the Amplify CLI.

tylerjroach commented 1 year ago

What is your Cognito access token expiration and refresh token expiration set to?

In the logs that you have captured, are you seeing the tokens refresh in fetchAuthSession calls?

I see that you are using Kotlin callback instead of coroutines for fetchAuthSession so that catch block will never run. You should add logging and possibly a fallback to the onError section of fetchAuthSession (although if a token is expired when fetchAuthSession is returned, it should refetch within the call itself).

ininmm commented 1 year ago

Hi @tylerjroach , I've edited my code:

fun getCredentials(): AWSCredentials {
        val latch = CountDownLatch(1)
        var sdkCredentials: AWSCredentials? = null

        Amplify.Auth.fetchAuthSession(
            { authSession ->
                sdkCredentials = ((authSession as AWSCognitoAuthSession).awsCredentialsResult.value as? AWSTemporaryCredentials)?.let {
                    Timber.i("fetchSession sdkCredentials: awsSessionToken: ${it.sessionToken}, awsAccessKeyId: ${it.accessKeyId}, awsSecretKey: ${it.secretAccessKey}")
                    Timber.i("expiration: ${it.expiration}, ${DateFormatUtils.getCustomDateFormatByTimestampMilliseconds(it.expiration.epochMilliseconds, DateFormatUtils.formatDateTimeFromNewRecord)}")
                    BasicAWSCredentials(
                        it.accessKeyId, it.secretAccessKey
                    )
                }
                Timber.i("sdkCredentials: ${sdkCredentials?.awsAccessKeyId}, ${sdkCredentials?.awsSecretKey}")
                latch.countDown()
            },
            {
                // can better handle this exception and pass it down to the throwing call below
                Timber.i("fetchSession error: ${it.stackTrace}")
                latch.countDown()
            }
        )

        // wait for fetchAuthSession to return
        latch.await()

        // return captured credentials or throw
        return sdkCredentials ?: throw IllegalStateException("Failed to get credentials")
    }

At the situation that I didn' t call refresh session and I take the tokens to attach policy, I've got below logs:

2023-05-17 15:08:43.540 AWSIotRemoteDataSource  com.example.ininmm                    I  fetchAWSSession: CognitoAuthSession(isSignIn=true)
2023-05-17 15:08:43.545 AWSIotRemoteDataSource  com.example.ininmm                    I  attachPolicy id: ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
2023-05-17 15:08:43.552 CognitoAWS...lsProvider com.example.ininmm                    I  fetchSession sdkCredentials: awsSessionToken: xxxxx, awsAccessKeyId:xxxxxx, awsSecretKey: xxxxxx
2023-05-17 15:08:43.555 CognitoAWS...lsProvider com.example.ininmm                    I  expiration: 2023-05-17T08:08:08Z, 2023/05/17 16:08:08
2023-05-17 15:08:43.556 CognitoAWS...lsProvider com.example.ininmm                    I  sdkCredentials:xxxxx, xxxxxx
2023-05-17 15:08:43.558 AWSIotRemoteDataSource  com.example.ininmm                    I  credentialsProvider.credentials:xxxxxx, xxxxxx
2023-05-17 15:08:43.624 AWSIotRepo...tachPolicy com.example.ininmm                    E  com.amazonaws.AmazonServiceException: The security token included in the request is invalid. (Service: AWSIot; Status Code: 403; Error Code: UnrecognizedClientException; Request ID: 49d323e8-ff80-4461-8f94-c12d80bb605f)

I'm sure that the tokens are not expired but I still got the exception, also if I force refresh session and got a new session that would be expired in a hour later then attach policy, I stiil got the exception too.

tylerjroach commented 1 year ago

It appears that you are passing the credentials directly into the AWSIotClient.

val iotAndroidClient = AWSIotClient(credentialsProvider.credentials)

Using this constructor does not allow refreshing credentials.

Instead, pass your CognitoAWSCredentialsProvider directly, so that the IOT SDK has access to call getCredentials if it detects expired credentials.

You referenced that you were doing this AWSIotClient(newCredentialsProvider) but I don't see it in the sample code.

Also, please see this update response to use BasicSessionCredentials if necessary: https://github.com/aws-amplify/amplify-android/issues/2400#issuecomment-1544284644

ininmm commented 1 year ago

Ok, now we can connect and publish MQTT as expected, I'll report back with any new issues that need an update.