aws / aws-sdk-net-extensions-cognito

An extension library to assist in the Amazon Cognito User Pools authentication process
Apache License 2.0
102 stars 49 forks source link

Custom Authentication Flow with SRP Password Verification #79

Closed efimenkop closed 2 years ago

efimenkop commented 2 years ago

The Question

I'm looking for a best way to implement a Custom Authentication Flow which consists of two steps:

  1. SRP Password Verification
  2. Custom challenge (SMS code verification)
public async Task AuthenticateWithCustomAuthAndSrpAsync(string poolId, string clientId, string userName, string password)
        {
            var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), FallbackRegionFactory.GetRegionEndpoint());
            var userPool = new CognitoUserPool(poolId, clientId, provider);
            var user = new CognitoUser(userName, clientId, userPool, provider);

            AuthFlowResponse authResponse = await user.StartWithCustomAuthAsync(new InitiateCustomAuthRequest
            {
                AuthParameters = new Dictionary<string, string>()
                {
                    {"USERNAME", userName},
                    {"SECRET_HASH", ComputeHash(userName)},
                    {"CHALLENGE_NAME", "CUSTOM_CHALLENGE"}
                },
                ClientMetadata = new Dictionary<string, string>()
            }).ConfigureAwait(false);

            var adminRespondToAuthChallengeAsync = await provider.AdminRespondToAuthChallengeAsync(
                new AdminRespondToAuthChallengeRequest
                {
                    ClientId = clientId,
                    UserPoolId = poolId,
                    ChallengeName = "PASSWORD_VERIFIER",
                    Session = authResponse.SessionID,
                    ChallengeResponses = new Dictionary<string, string>
                    {
                        {"USERNAME", userName},
                        {"CHALLENGE_NAME", "PASSWORD_VERIFIER"},
                        {"PASSWORD_CLAIM_SIGNATURE", ""}, // <== Is there a way to calculate it?
                        {"PASSWORD_CLAIM_SECRET_BLOCK", ""}, // <== Is there a way to calculate it?
                        {"TIMESTAMP", ""} // <== Is there a way to calculate it?
                    }
                });
        }

The problem is that SRP Password Verification requires next parameters: PASSWORD_CLAIM_SIGNATURE, PASSWORD_CLAIM_SECRET_BLOCK and TIMESTAMP, but seems like library calculates these properties internally and there is no way to get them from the caller code. Am I missing something?

ashishdhingra commented 2 years ago

Hi @efimenkop,

Good morning.

Thanks for posting guidance question.

For CognitoUser. StartWithSrpAuthAsync(), the logic seems to automatically create RespondToAuthChallengeRequest and responding to auth challenge. And you are right, for this scenarios, the library calculates the values for properties PASSWORD_CLAIM_SECRET_BLOCK and PASSWORD_CLAIM_SIGNATURE internally.

For your case, you could take some cue from CreateSrpPasswordVerifierAuthRequest on how the PASSWORD_CLAIM_SECRET_BLOCK and PASSWORD_CLAIM_SIGNATURE are calculated.

Thanks, Ashish

efimenkop commented 2 years ago

Hello, @ashishdhingra! Maybe it worth making CreateSrpPasswordVerifierAuthRequest public (or expose the logic for creating PASSWORD_CLAIM_SECRET_BLOCK and PASSWORD_CLAIM_SIGNATURE in some other way)? Otherwise I have to copy-paste big piece of library's logic.

ashishdhingra commented 2 years ago

@efimenkop Thanks for your reply. Please advise if it would be possible for you to test the extracted code for your scenario and if it works, may be submit a PR to refactor existing code as contribution.

github-actions[bot] commented 2 years ago

This issue has not recieved a response in 1 week. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled.

github-actions[bot] commented 2 years ago

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

ghost commented 1 year ago

This should not be closed @efimenkop. It is still a problem to implement Custom Flow with SRP. Even copy pasting a lot of the code still gives an error: Amazon.CognitoIdentityProvider.Model.NotAuthorizedException: "Incorrect username or password".

After 6 hours of debugging I can't find any mistakes. SRP normal flow is working as expected, custom + SRP is not.

It funny because it is even one of the scenarios described in the official documentation:

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html#Using-SRP-password-verification-in-custom-authentication-flow

But the SDK fails to use SRP on custom authentication flows.

I get:

image

konectech commented 1 year ago

I came here looking for a .NET solution to integrate the email MFA I setup from this guide

https://aws.amazon.com/blogs/mobile/extending-amazon-cognito-with-email-otp-for-2fa-using-amazon-ses/

Which is very similar to the OP.

I have a solution that integrates with the above custom authentication implementation if anyone is interested.

I included a new property IsCustomAuthFlow to class InitiateSrpAuthRequest:

    /// <summary>
    /// Class containing the necessary properities to initiate SRP authentication flow
    /// </summary>
    public class InitiateSrpAuthRequest
    {
        /// <summary>
        /// The password for the corresponding CognitoUser.
        /// </summary>
        public string Password { get; set; }
        /// <summary>
        /// The password for the device associated with the corresponding CognitoUser 
        /// </summary>
        public string DevicePass { get; set; }
        /// <summary>
        /// The device password verifier for the device associated with the corresponding CognitoUser
        /// </summary>
        public string DeviceVerifier { get; set; }
        /// <summary>
        /// The Device Key Group for the device associated with the corresponding CognitoUser
        /// </summary>
        public string DeviceGroupKey { get; set; }
        /// <summary>
        /// Use the custom auth flow with this SRP request
        /// </summary>
        public bool IsCustomAuthFlow { get; set; }
    }

I then modified the method StartWithSrpAuthAsync on class CognitoUser to set the AuthFlow to CUSTOM_AUTH and included an AuthParameter of CHALLENGE_NAME = SRP_A on the initiateRequest, when the new property IsCustomAuthFlow is true.

        /// <summary>
        /// Initiates the asynchronous SRP authentication flow
        /// </summary>
        /// <param name="srpRequest">InitiateSrpAuthRequest object containing the necessary parameters to
        /// create an InitiateAuthAsync API call for SRP authentication</param>
        /// <returns>Returns the AuthFlowResponse object that can be used to respond to the next challenge, 
        /// if one exists</returns>
        public virtual async Task<AuthFlowResponse> StartWithSrpAuthAsync(InitiateSrpAuthRequest srpRequest)
        {
            if (srpRequest == null || string.IsNullOrEmpty(srpRequest.Password))
            {
                throw new ArgumentNullException("Password required for authentication.", "srpRequest");
            }

            Tuple<BigInteger, BigInteger> tupleAa = AuthenticationHelper.CreateAaTuple();
            InitiateAuthRequest initiateRequest = CreateSrpAuthRequest(tupleAa);

            // change this to custom
            if (srpRequest.IsCustomAuthFlow)
            {
                initiateRequest.AuthFlow = AuthFlowType.CUSTOM_AUTH;
                initiateRequest.AuthParameters.Add("CHALLENGE_NAME", "SRP_A");
            }

            InitiateAuthResponse initiateResponse = await Provider.InitiateAuthAsync(initiateRequest).ConfigureAwait(false);
            UpdateUsernameAndSecretHash(initiateResponse.ChallengeParameters);

            RespondToAuthChallengeRequest challengeRequest =
                CreateSrpPasswordVerifierAuthRequest(initiateResponse, srpRequest.Password, tupleAa);

            bool challengeResponsesValid = challengeRequest != null && challengeRequest.ChallengeResponses != null;
            bool deviceKeyValid = Device != null && !string.IsNullOrEmpty(Device.DeviceKey);

            if (challengeResponsesValid && deviceKeyValid)
            {
                challengeRequest.ChallengeResponses[CognitoConstants.ChlgParamDeviceKey] = Device.DeviceKey;
            }

            RespondToAuthChallengeResponse verifierResponse =
                await Provider.RespondToAuthChallengeAsync(challengeRequest).ConfigureAwait(false);
            var isDeviceAuthRequest = verifierResponse.AuthenticationResult == null && (!string.IsNullOrEmpty(srpRequest.DeviceGroupKey)
                || !string.IsNullOrEmpty(srpRequest.DevicePass));
            #region Device-level authentication
            if (isDeviceAuthRequest)
            {
                if (string.IsNullOrEmpty(srpRequest.DeviceGroupKey) || string.IsNullOrEmpty(srpRequest.DevicePass))
                {
                    throw new ArgumentNullException("Device Group Key and Device Pass required for authentication.", "srpRequest");
                }

                #region Device SRP Auth
                var deviceAuthRequest = CreateDeviceSrpAuthRequest(verifierResponse, tupleAa);
                var deviceAuthResponse = await Provider.RespondToAuthChallengeAsync(deviceAuthRequest).ConfigureAwait(false); 
                #endregion

                #region Device Password Verifier
                var devicePasswordChallengeRequest = CreateDevicePasswordVerifierAuthRequest(deviceAuthResponse, srpRequest.DeviceGroupKey, srpRequest.DevicePass, tupleAa);
                verifierResponse = await Provider.RespondToAuthChallengeAsync(devicePasswordChallengeRequest).ConfigureAwait(false);
                #endregion

            }
            #endregion

            UpdateSessionIfAuthenticationComplete(verifierResponse.ChallengeName, verifierResponse.AuthenticationResult);

            return new AuthFlowResponse(verifierResponse.Session,
                verifierResponse.AuthenticationResult,
                verifierResponse.ChallengeName,
                verifierResponse.ChallengeParameters,
                new Dictionary<string, string>(verifierResponse.ResponseMetadata.Metadata));
        }

Inside your code you will need to respond to the custom auth with a response like the following:

var challengeResponses = new Dictionary<string, string>();
challengeResponses.Add("ANSWER", authenticationCode);
challengeResponses.Add("USERNAME", userName);
var request = new RespondToCustomChallengeRequest()
{                            
            ChallengeParameters = challengeResponses,
            SessionID = sessionID
};
await user.RespondToCustomAuthAsync(request).ConfigureAwait(false);