How to create a SECRET_HASH for AWS Cognito using boto3?
Asked Answered
N

2

12

I want to create/calculate a SECRET_HASH for AWS Cognito using boto3 and python. This will be incorporated in to my fork of warrant.

I configured my cognito app client to use an app client secret. However, this broke the following code.

def renew_access_token(self):
    """
    Sets a new access token on the User using the refresh token.

    NOTE:
    Does not work if "App client secret" is enabled. 'SECRET_HASH' is needed in AuthParameters.
    'SECRET_HASH' requires HMAC calculations.

    Does not work if "Device Tracking" is turned on.
    https://mcmap.net/q/259646/-cognito-user-pool-how-to-refresh-access-token-using-refresh-token

    'DEVICE_KEY' is needed in AuthParameters. See AuthParameters section.
    https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html
    """
    refresh_response = self.client.initiate_auth(
        ClientId=self.client_id,
        AuthFlow='REFRESH_TOKEN',
        AuthParameters={
            'REFRESH_TOKEN': self.refresh_token
            # 'SECRET_HASH': How to generate this?
        },
    )

    self._set_attributes(
        refresh_response,
        {
            'access_token': refresh_response['AuthenticationResult']['AccessToken'],
            'id_token': refresh_response['AuthenticationResult']['IdToken'],
            'token_type': refresh_response['AuthenticationResult']['TokenType']
        }
    )

When I run this I receive the following exception:

botocore.errorfactory.NotAuthorizedException: 
An error occurred (NotAuthorizedException) when calling the InitiateAuth operation: 
Unable to verify secret hash for client <client id echoed here>.

This answer informed me that a SECRET_HASH is required to use the cognito client secret.

The aws API reference docs AuthParameters section states the following:

For REFRESH_TOKEN_AUTH/REFRESH_TOKEN: USERNAME (required), SECRET_HASH (required if the app client is configured with a client secret), REFRESH_TOKEN (required), DEVICE_KEY

The boto3 docs state that a SECRET_HASH is

A keyed-hash message authentication code (HMAC) calculated using the secret key of a user pool client and username plus the client ID in the message.

The docs explain what is needed, but not how to achieve this.

Natalyanataniel answered 29/5, 2017 at 14:5 Comment(0)
T
13

The below get_secret_hash method is a solution that I wrote in Python for a Cognito User Pool implementation, with example usage:

import boto3
import botocore
import hmac
import hashlib
import base64


class Cognito:
    client_id = app.config.get('AWS_CLIENT_ID')
    user_pool_id = app.config.get('AWS_USER_POOL_ID')
    identity_pool_id = app.config.get('AWS_IDENTITY_POOL_ID')
    client_secret = app.config.get('AWS_APP_CLIENT_SECRET')
    # Public Keys used to verify tokens returned by Cognito:
    # http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-identity-user-pools-using-id-and-access-tokens-in-web-api
    id_token_public_key = app.config.get('JWT_ID_TOKEN_PUB_KEY')
    access_token_public_key = app.config.get('JWT_ACCESS_TOKEN_PUB_KEY')

    def __get_client(self):
        return boto3.client('cognito-idp')

    def get_secret_hash(self, username):
        # A keyed-hash message authentication code (HMAC) calculated using
        # the secret key of a user pool client and username plus the client
        # ID in the message.
        message = username + self.client_id
        dig = hmac.new(self.client_secret, msg=message.encode('UTF-8'),
                       digestmod=hashlib.sha256).digest()
        return base64.b64encode(dig).decode()

    # REQUIRES that `ADMIN_NO_SRP_AUTH` be enabled on Client App for User Pool
    def login_user(self, username_or_alias, password):
        try:
            return self.__get_client().admin_initiate_auth(
                UserPoolId=self.user_pool_id,
                ClientId=self.client_id,
                AuthFlow='ADMIN_NO_SRP_AUTH',
                AuthParameters={
                    'USERNAME': username_or_alias,
                    'PASSWORD': password,
                    'SECRET_HASH': self.get_secret_hash(username_or_alias)
                }
            )
        except botocore.exceptions.ClientError as e:
            return e.response
Taeniafuge answered 29/5, 2017 at 14:38 Comment(6)
does the SECRET_HASH calculation always use the username as input, for every case where the API needs one, even the REFRESH_TOKEN_AUTH flow, where a username is not part of the AuthParameters?Mccarthyism
Good stuff. I just got this error "TypeError: key: expected bytes or bytearray, but got 'str' " at the dig=hmac.new(...) line. The solution was to do a simple bytearray(self.client_secret, "utf-8") at the first parameter of hmac.newHyperbole
@Mccarthyism I'd also like to knowWinner
@RonyTesler I found the answer to be to use the username for all uses of SECRET_HASH in the authentication processes' parameters, but when doing REFRESH_TOKEN_AUTH the user's UUID from the authentication was needed, along with the REFRESH_TOKEN.Mccarthyism
@Mccarthyism what is the user UUID? Anyway, the username is part of the jwt token, so you should have the refresh token and the expired jwt token, so you should have access to the username.Winner
@RonyTesler when you authenticate a user, you supply their username and password and SECRET_HASH generated from their username. The AWS response includes the access token as 'AccessToken', the refresh token as 'RefreshToken', and the ID token as 'IdToken'; once authenticated, get user info with their username which responds with their UUID in the 'Username' field. When refreshing the access token, the RefreshToken and UUID are used so that the username and password are not needed again, and gives a new AccessToken and IdToken. IdToken is used in requests along with AccessToken.Mccarthyism
S
2

I also got a TypeError when I tried the above solution. Here is the solution that worked for me:

import hmac
import hashlib
import base64

# Function used to calculate SecretHash value for a given client
def calculateSecretHash(client_id, client_secret, username):
    key = bytes(client_secret, 'utf-8')
    message = bytes(f'{username}{client_id}', 'utf-8')
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

# Usage example
calculateSecretHash(client_id, client_secret, username)

Stumpf answered 25/6, 2022 at 17:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.