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?