Search code examples
boto3aws-sts

How can you reuse AWS STS AssumeRole MFA TokenCode values?


I am using the boto3 library to iterate over many AWS accounts using AssumeRole. The policy requires MFA. The boiler-plate code looks like this (redacted):

session = boto3.Session(
     aws_access_key_id=#####,
     aws_secret_access_key=#####
     )

sts_client = session.client('sts')
assumed_role = sts_client.assume_role(
     RoleArn=f'arn:aws:iam::#####:role/#####',
     RoleSessionName=f'#####',
     SerialNumber='arn:aws:iam::#####:mfa/#####',
     TokenCode=mfa_code
     )

credentials = assumed_role['Credentials']
assumed_session = boto3.Session(
     aws_access_key_id=credentials['AccessKeyId'],
     aws_secret_access_key=credentials['SecretAccessKey'],
     aws_session_token=credentials['SessionToken']
)

This code is executed for each account in a list of many accounts and I'm trying to get a session into each of them.

I find that if I do not pass a unique value for mfa_code each time that I get exception Failed to authenticate into account: An error occurred (AccessDenied) when calling the AssumeRole operation: MultiFactorAuthentication failed with invalid MFA one time pass code.

This essentially forces me to wait 30 seconds between each account.

Is there a way to associate my MFA code with the base session and then obtain the assumed role sessions all leveraging that single MFA code? Obviously the AWS console doesn't prompt for a new MFA code every time I assume a role, only when I log in.

Through trial and error I did find that I can use the same code until I get a successful assumed_role. For example, if there is an account where the role isn't configured I get the exception Failed to authenticate into account: An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::#####:user/##### is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::#####:role/#####. If that happens then I can use the same mfa_code value for another account until it succeeds, and then I cannot use it again.

There was a passing comment back in 2022 that made reference to the issue but with no solution, see this answer.


Solution

  • After an extremely productive call with AWS technical support I have the solution. The code was so close and the answer is in how the MFA information is associated with the original session. In my code I'm passing the MFA information to the assume_role call, which allows its use for just that call and then invalidates the MFA code.

    So, without further introduction, is the code that works.

    # The following is done once and includes providing MFA
    session = boto3.Session(
        aws_access_key_id=#####,
        aws_secret_access_key=#####
    )
    
    base_client = session.client('sts').get_session_token(
         SerialNumber='arn:aws:iam::#####:mfa/#####',
         TokenCode=mfa_code
    )
    
    base_credentials = base_client['Credentials']
    base_session = boto3.Session(
         aws_access_key_id=base_credentials['AccessKeyId'],
         aws_secret_access_key=base_credentials['SecretAccessKey'],
         aws_session_token=base_credentials['SessionToken']
    )
    
    # The following is repeated for each account/role assumption
    sts_client = base_session.client('sts')
    assumed_role = sts_client.assume_role(
         RoleArn=f'arn:aws:iam::#####:role/#####',
         RoleSessionName=f'#####'
    )
    
    assumed_credentials = assumed_role['Credentials']
    assumed_session = boto3.Session(
         aws_access_key_id=assumed_credentials['AccessKeyId'],
         aws_secret_access_key=assumed_credentials['SecretAccessKey'],
         aws_session_token=assumed_credentials['SessionToken']
    )
    

    The solution is the call to get_session_token with the MFA-related parameters which then creates the base_session that then can be used with assume_role.

    Thanks again to the extremely helpful AWS technical support engineer who had this working in under 10 minutes.