Boto3 `initiate_auth` Raises `NotAuthorizedException` for Valid Refresh Tokens

1.1k Views Asked by At

We have secured our Chalice endpoints with a Cognito authorizer and are able to access it by passing a valid ID Token in the Authorization header.

Below is our code for securing an endpoint:

authorizer = CognitoUserPoolAuthorizer(
    'USER_POOL_NAME', provider_arns=['USER_POOL_ARN'])

@app.route('/ping', methods=['GET'], authorizer=authorizer)
def ping() -> str:
    return 'The site is up!'

Below is how we get ID Tokens given an Email and Password that has been signed up and confirmed in our User Pool:

_USER_POOL_ID: Final[str] = ''
_USER_POOL_REGION: Final[str] = 'ap-southeast-1'
_APP_CLIENT_ID: Final[str] = ''
_APP_CLIENT_SECRET: Final[str] = ''

_USERNAME_PASSWORD_AUTH_FLOW = 'USER_PASSWORD_AUTH'

_aws_client = boto3.client('cognito-idp', config=Config(region_name=_USER_POOL_REGION))


def _get_secret_hash(username, cognito_client_id, cognito_secret) -> str:
    msg = username + cognito_client_id

    dig = hmac.new(str(cognito_secret).encode('utf-8'),
                   msg=str(msg).encode('utf-8'),
                   digestmod=hashlib.sha256).digest()

    return base64.b64encode(dig).decode()


@app.route('/authenticate', methods=['POST'], content_types=['application/json', 'text/plain'])
def authenticate():
    # Callers will pass an email and password to this endpoint.
    return _aws_client.initiate_auth(ClientId=_APP_CLIENT_ID,
                                     AuthFlow=_USERNAME_PASSWORD_AUTH_FLOW,
                                     AuthParameters={
                                         'USERNAME': 'email',
                                         'SECRET_HASH': _get_secret_hash('email',
                                                                         _APP_CLIENT_ID,
                                                                         _APP_CLIENT_SECRET),
                                         'PASSWORD': 'password'
                                     })

Below is the format of the response from initiate_auth's documentation:

{
    'ChallengeName': 'SMS_MFA'|'SOFTWARE_TOKEN_MFA'|'SELECT_MFA_TYPE'|'MFA_SETUP'|'PASSWORD_VERIFIER'|'CUSTOM_CHALLENGE'|'DEVICE_SRP_AUTH'|'DEVICE_PASSWORD_VERIFIER'|'ADMIN_NO_SRP_AUTH'|'NEW_PASSWORD_REQUIRED',
    'Session': 'string',
    'ChallengeParameters': {
        'string': 'string'
    },
    'AuthenticationResult': {
        'AccessToken': 'string',
        'ExpiresIn': 123,
        'TokenType': 'string',
        'RefreshToken': 'string',
        'IdToken': 'string',
        'NewDeviceMetadata': {
            'DeviceKey': 'string',
            'DeviceGroupKey': 'string'
        }
    }
}

We use AuthenticationResult.IdToken as the value of our Authorization headers.

Our issue is when we try to get new ID Tokens via AuthenticationResult.RefreshToken, we are getting NotAuthorizedException.

We have configured our User Pool to NOT remember User Devices, so we assume we do not have to pass a DEVICE_KEY when using Refresh Tokens. We have also configured our App Client so Refresh Tokens last 180 days while ID and Access Tokens only last 1 hour.

We followed this tutorial and below is our code:

_REFRESH_TOKEN_AUTH_FLOW = 'REFRESH_TOKEN_AUTH'

@app.route('/refreshTokens', methods=['POST'], content_types=['application/json', 'text/plain'])
def refresh_tokens():
    # Callers will pass an email and refresh token to this endpoint.
    return _aws_client.initiate_auth(ClientId=_APP_CLIENT_ID,
                                     AuthFlow=_REFRESH_TOKEN_AUTH_FLOW,
                                     AuthParameters={
                                         'USERNAME': 'email',
                                         'SECRET_HASH': _getSecretHash(email,
                                                                       _APP_CLIENT_ID,
                                                                       _APP_CLIENT_SECRET),
                                         'REFRESH_TOKEN': 'refresh token'
                                     })

What puzzles us is that the Refresh Token should be valid because it is from the same response where we got a valid ID Token from.

Is there something we are doing wrong?

1

There are 1 best solutions below

0
On BEST ANSWER

It turns out that we cannot use the user's Email (something about the '@' character causing problems).

The solution is to also pass the ID Token so we can extract the user's Cognito Username (which is some kind of UUID) and use that instead of the email.

Also, apparently, this issue has been posted here before.