aws-sdk-cpp icon indicating copy to clipboard operation
aws-sdk-cpp copied to clipboard

cognito-idp: "SECRET_HASH was not received" with USER_SRP_AUTH

Open phyordia opened this issue 1 year ago • 1 comments

Describe the bug

Hello! I'm trying to authenticate a user using CognitoIdentityProviderClient.

TL;DR: Using USER_SRP_AUTH flow and a correct secret_hash, I get a response saying SECRET_HASH was not sent.

Here's the relevant portion of the code:

Aws::Map<Aws::String, Aws::String> authParameters;
authParameters["USERNAME"] = username.c_str();
// authParameters["PASSWORD"] = password.c_str(); // Used to test with USER_PASSWORD_AUTH below

authParameters["SECRET_HASH"] = "some_secret_hash";
authParameters["SRP_A"] = srp.A();

Aws::CognitoIdentityProvider::CognitoIdentityProviderClient cipClient(clientConfig );

Aws::CognitoIdentityProvider::Model::InitiateAuthRequest authRequest;
authRequest.SetClientId( m_clientID.c_str() );
// authRequest.SetAuthFlow(Aws::CognitoIdentityProvider::Model::AuthFlowType::USER_PASSWORD_AUTH );
authRequest.SetAuthFlow(Aws::CognitoIdentityProvider::Model::AuthFlowType::USER_SRP_AUTH );

authRequest.SetAuthParameters( authParameters );
Aws::Map<Aws::String, Aws::String> __authParameters = authRequest.GetAuthParameters();
// check if the correct value is in the map. It is.

Aws::CognitoIdentityProvider::Model::InitiateAuthOutcome authResult = cipClient.InitiateAuth( authRequest );

Then I get: "NotAuthorizedException: Client <XXXXXXX> is configured with secret but SECRET_HASH was not received"

  • I have tested all the credentials (user, password, pool Id, app ID, secret_hash, SRP_A, same flow type, etc...) with both Python's boto3 and requests and it works fine both ways (i get tokens and challange).

  • Strangely, in the c++ version above:

    • Using USER_PASSWORD_AUTH flow instead (and provide a password in the authParameters), I don't get the error of "SECRET_HASH was not received"
    • Using USER_SRP_AUTH and authParameters["SECRET_HASH"] = "some_INCORRECT_secret_hash", I get an error saying the hash was not correct (but it was, apparently, received)

From what I have read in several StackOverflow that SRP doesn't work with apps with secrets, but those threads seem outdated, and the python test seems to disprove that?

Could you please advise? Is this a limitation of the c++ sdk or is this a bug?

Many thanks in advance!

Regression Issue

  • [ ] Select this option if this issue appears to be a regression.

Expected Behavior

Expect to receive either a success response or an invalid credentials error, but not a "not sent" error.

Current Behavior

See description of the bug

Reproduction Steps

See description of the bug

Possible Solution

No response

Additional Information/Context

No response

AWS CPP SDK version used

1.11.483

Compiler and Version used

clang-1600.0.26.6

Operating System and version

macOS 15.2

phyordia avatar Jan 14 '25 21:01 phyordia

Hello, I tried to replicate the issue you described but I couldn't replicate it. Here is the test code you can try . It works for me.

#include <gtest/gtest.h>
#include <aws/testing/AwsTestHelpers.h>
#include <aws/testing/MemoryTesting.h>
#include <algorithm>
#include <thread>

#include <aws/cognito-idp/CognitoIdentityProviderClient.h>
#include <aws/cognito-idp/CognitoIdentityProviderErrors.h>
#include <aws/cognito-idp/model/AuthFlowType.h>
#include <aws/cognito-idp/model/ExplicitAuthFlowsType.h>
#include <aws/cognito-idp/model/InitiateAuthRequest.h>
#include <aws/cognito-idp/model/SignUpRequest.h>
#include <aws/cognito-idp/model/CreateUserPoolClientRequest.h>
#include <aws/cognito-idp/model/DeleteUserPoolClientRequest.h>
#include <aws/cognito-idp/model/CreateUserPoolRequest.h>
#include <aws/cognito-idp/model/DeleteUserPoolRequest.h>
#include <aws/cognito-idp/model/AdminCreateUserRequest.h>
#include <aws/cognito-idp/model/AdminDeleteUserRequest.h>
#include <aws/cognito-idp/model/MessageActionType.h>
#include <aws/cognito-idp/CognitoIdentityProviderServiceClientModel.h>

#include <aws/core/client/CoreErrors.h>
#include <aws/core/utils/json/JsonSerializer.h>
#include <aws/core/utils/Outcome.h>
#include <aws/testing/TestingEnvironment.h>
#include <aws/core/platform/Environment.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/bn.h>


using namespace Aws::CognitoIdentityProvider;
using namespace Aws::CognitoIdentityProvider::Model;
using namespace Aws::Client;
using namespace Aws::Region;

namespace
{
static const char* ALLOCATION_TAG = "IdentityProviderOperationTest";


class IdentityProviderOperationTest : public ::testing::Test
{
public:
    IdentityProviderOperationTest() :
        client(nullptr)
    {}

    std::shared_ptr<Aws::CognitoIdentityProvider::CognitoIdentityProviderClient> client;
    Aws::String testTrace;

protected:

    const Aws::String m_clientName{"testClient"};
    Aws::String m_userPoolId;
    Aws::String m_clientId;
    const Aws::String m_username{"testUser"};
    Aws::String m_clientSecret;

    void SetUp()
    {
        Aws::Client::ClientConfiguration config;
        config.region = TEST_REGION;

        client = Aws::MakeShared<Aws::CognitoIdentityProvider::CognitoIdentityProviderClient>(ALLOCATION_TAG, config);

        //make user pool
        m_userPoolId = createUserPool("TestPool");

        ASSERT_TRUE(!m_userPoolId.empty());

        //make client
        if(!m_userPoolId.empty())
        {
            auto ret = createPoolClient(m_userPoolId, m_clientName);
            m_clientId = ret.first;
            m_clientSecret = ret.second;
        }

        ASSERT_TRUE(!m_clientId.empty());
        ASSERT_TRUE(!m_clientSecret.empty());

        //create user
        if(!m_userPoolId.empty() && !m_clientId.empty())
        {
            ASSERT_TRUE(createUser(m_username, "Password123!", m_userPoolId));
        }

    }

    void TearDown()
    {

        //delete user
        if(!m_userPoolId.empty())
        {
            ASSERT_TRUE(deleteUser(m_username, m_userPoolId));
        }

        // delete client
        if(!m_userPoolId.empty() && !m_clientId.empty())
        {
            ASSERT_TRUE(deletePoolClient(m_userPoolId, m_clientId));
        }

        // delete user pool
        if(!m_userPoolId.empty())
        {
            ASSERT_TRUE(deleteUserPool(m_userPoolId));
        }


        if (::testing::Test::HasFailure())
        {
            std::cout << "Test traces: " << testTrace << "\n";
        }
        testTrace.erase();
    }


    Aws::String createUserPool(Aws::String poolName)
    {
        if(poolName.empty())
        {
            return {};
        }
        Aws::CognitoIdentityProvider::Model::CreateUserPoolRequest userPoolRequest;
        userPoolRequest.SetPoolName(poolName);

        CreateUserPoolOutcome userPoolOutcome = client->CreateUserPool(userPoolRequest);
        //ASSERT_TRUE(userPoolOutcome.IsSuccess());
        if (!userPoolOutcome.IsSuccess()) {
            return {};
        }

        auto poolId = userPoolOutcome.GetResult().GetUserPool().GetId();
        std::cout << "Created User Pool with ID: " << poolId << " for pool name="<<poolName<<std::endl;
        return poolId;
    }

    bool deleteUserPool(const Aws::String& poolId)
    {
        if (poolId.empty())
        {
            return false;
        }
        Aws::CognitoIdentityProvider::Model::DeleteUserPoolRequest deletePoolRequest;
        deletePoolRequest.SetUserPoolId(poolId);

        auto deletePoolOutcome = client->DeleteUserPool(deletePoolRequest);
        //ASSERT_TRUE(deletePoolOutcome.IsSuccess());
        if (!deletePoolOutcome.IsSuccess()) {
            std::cerr << "Failed to delete User Pool: " << deletePoolOutcome.GetError().GetMessage() << std::endl;
        }
        else
        {
            std::cout << "Deleted User Pool with ID: " << poolId << std::endl;
        }
        return deletePoolOutcome.IsSuccess();
    }

    bool createUser(const Aws::String& username, const Aws::String& password, const Aws::String& userPoolId)
    {
        if (userPoolId.empty() || username.empty() || password.empty())
        {
            return false;
        }
        
        Aws::CognitoIdentityProvider::Model::AdminCreateUserRequest createUserRequest;
        createUserRequest.SetUserPoolId(userPoolId);
        createUserRequest.SetUsername(username);

        Aws::CognitoIdentityProvider::Model::AttributeType emailAttribute;
        emailAttribute.SetName("email");
        emailAttribute.SetValue("[email protected]");
        createUserRequest.AddUserAttributes(emailAttribute);

        // Set a temporary password
        createUserRequest.SetTemporaryPassword(password);

        // Suppress sending an email to the user
        createUserRequest.SetMessageAction(Aws::CognitoIdentityProvider::Model::MessageActionType::SUPPRESS);

        auto outcome = client->AdminCreateUser(createUserRequest);
        //ASSERT_TRUE(outcome.IsSuccess());
        if (outcome.IsSuccess()) {
            std::cout << "User created successfully: " << outcome.GetResult().GetUser().GetUsername() << std::endl;
        } else {
            std::cerr << "Failed to create user: " << outcome.GetError().GetMessage() << std::endl;
        }

        return outcome.IsSuccess();
    }

    bool deleteUser(const Aws::String& username, const Aws::String& userPoolId)
    {
        if (username.empty() || userPoolId.empty())
        {
            return false;
        }
        
        Aws::CognitoIdentityProvider::Model::AdminDeleteUserRequest deleteUserRequest;
        deleteUserRequest.SetUserPoolId(userPoolId);
        deleteUserRequest.SetUsername(username);
        auto deleteUserOutcome = client->AdminDeleteUser(deleteUserRequest);
        //ASSERT_TRUE(deleteUserOutcome.IsSuccess());
        if (!deleteUserOutcome.IsSuccess()) {
            std::cerr << "Failed to delete user: " << deleteUserOutcome.GetError().GetMessage() << std::endl;
        }
        else
        {
            std::cout << "Deleted test user: " << username << std::endl;
        }
        return deleteUserOutcome.IsSuccess();
    }


    std::pair<Aws::String, Aws::String> createPoolClient(const Aws::String& userPoolId, const Aws::String& clientName)
    {
        if(userPoolId.empty() || clientName.empty())
        {
            return {};
        }

        Aws::CognitoIdentityProvider::Model::CreateUserPoolClientRequest request;
        request.SetUserPoolId(userPoolId); 
        request.SetClientName(clientName);  

        request.SetGenerateSecret(true);
        Aws::Vector<Aws::CognitoIdentityProvider::Model::ExplicitAuthFlowsType> authFlows{Aws::CognitoIdentityProvider::Model::ExplicitAuthFlowsType::ALLOW_USER_SRP_AUTH, Aws::CognitoIdentityProvider::Model::ExplicitAuthFlowsType::ALLOW_REFRESH_TOKEN_AUTH};
        request.SetExplicitAuthFlows(authFlows);

        Aws::CognitoIdentityProvider::Model::CreateUserPoolClientOutcome outcome = client->CreateUserPoolClient(request);
        //ASSERT_TRUE(outcome.IsSuccess());
        Aws::String clientId, clientSecret;
        if (outcome.IsSuccess())
        {
            clientId = outcome.GetResult().GetUserPoolClient().GetClientId();
            clientSecret = outcome.GetResult().GetUserPoolClient().GetClientSecret();
            std::cout << "User Pool Client created successfully." << std::endl;
            std::cout << "Client ID: " << clientId << std::endl;
        }
        return {clientId,clientSecret};
    }

    bool deletePoolClient(const Aws::String& userPoolId, const Aws::String& clientId)
    {
        // Set up the request for deleting the User Pool Client
        Aws::CognitoIdentityProvider::Model::DeleteUserPoolClientRequest request;
        request.SetUserPoolId(userPoolId);
        request.SetClientId(clientId);

        // Make the call to delete the user pool client
        auto outcome = client->DeleteUserPoolClient(request);
        if (outcome.IsSuccess())
        {
            std::cout << "User Pool Client deleted successfully." << std::endl;
        }
        //ASSERT_TRUE(outcome.IsSuccess());
        return outcome.IsSuccess();
    }

};


Aws::String ComputeSecretHash(const Aws::String &userPoolClientId, const Aws::String &clientSecret, const Aws::String &userName)
{
    const auto message = userName + userPoolClientId;
    unsigned char hash[EVP_MAX_MD_SIZE];
    unsigned int len = 0;

    HMAC(EVP_sha256(),
    clientSecret.c_str(), clientSecret.length(),
    (unsigned char*)message.c_str(), message.length(),
    hash, &len);
    
    // Base64 encode the hash
    char encoded[128];
    EVP_EncodeBlock((unsigned char*)encoded, hash, len);

    return std::string(encoded);
}
// https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
const char* initN = 
    "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
    "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
    "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
    "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
    "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
    "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
    "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
    "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
    "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
    "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
    "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
    "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
    "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
    "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
    "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
    "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF";
// https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#
const char* generator = "2";


class SRPClient {
    public:
    SRPClient(const std::string& N_hex, const std::string& g):N(BN_new()), g(BN_new()), a(nullptr), A(BN_new()), ctx(BN_CTX_new())
    {
        if (!BN_hex2bn(&N, N_hex.c_str())) {
            std::cout<<"Failed to load N"<<std::endl;
        }
    
        if (!BN_dec2bn(&this->g, g.c_str())) {
            std::cout<<"Failed to load g"<<std::endl;
        }
    
        generatePrivateKey(256);
    }
    ~SRPClient()
    {
        BN_free(N);
        BN_free(g);
        BN_free(a);
        BN_free(A);
        BN_CTX_free(ctx);
    }

    
    private:
    BIGNUM* N{nullptr};
    BIGNUM* g{nullptr};
    BIGNUM* a{nullptr};
    BIGNUM* A{nullptr};
    BN_CTX* ctx;

    void generatePrivateKey(int bits)
    {
        a = BN_new();
        if (!BN_rand(a, bits, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY)) {
            std::cout<<"Failed to generate private key"<<std::endl;
        }
    }

    public:
    std::string calculateA() const
    {
        if (!BN_mod_exp(A, g, a, N, ctx)) {
            std::cout<<"Failed to compute A = g^a % N"<<std::endl;
            return {};
        }
    
        char* A_hex = BN_bn2hex(A);
        std::string A_str(A_hex);
        OPENSSL_free(A_hex);
    
        return A_str;
    }
};



TEST_F(IdentityProviderOperationTest, testSecret)
{
    SRPClient srpclient(initN, generator);
    Aws::Map<Aws::String, Aws::String> authParameters;
    authParameters["USERNAME"] = m_username;
    authParameters["SECRET_HASH"] = ComputeSecretHash(m_clientId, m_clientSecret, m_username);
    auto srpa = srpclient.calculateA();
    ASSERT_TRUE(!srpa.empty());
    authParameters["SRP_A"] = srpa;  

    Aws::CognitoIdentityProvider::Model::InitiateAuthRequest authRequest;
    authRequest.SetClientId(m_clientId);
    authRequest.SetAuthFlow(Aws::CognitoIdentityProvider::Model::AuthFlowType::USER_SRP_AUTH);
    authRequest.SetAuthParameters(authParameters);
    Aws::CognitoIdentityProvider::Model::InitiateAuthOutcome authResult = client->InitiateAuth( authRequest );
    EXPECT_TRUE(authResult.IsSuccess());
}

}

sbera87 avatar Mar 13 '25 20:03 sbera87