1

I am trying to get a CUSTOM_AUTH flow with AWS Cognito in the following setup:

  • Angular TS client app
  • .NET 3.1 WebAPI

Login is handled serverside, UserName + Password check works correctly. Business requires to add email based MFA. I struggle to understand how to implement CUSTOM_AUTH flow from Server-side. All samples and documentation are restricted to Client flow, which includes SRP values.

The desired flow would be:

  1. User enters username + password
  2. API send them to Cognito, which triggers the CUSTOM AUTH Flow
  3. Custom flow triggers, code being generated and an email sent out to the user email address with a generated code
  4. Client app renders an input field for the code generated
  5. user submits the code received via email
  6. API calls Cognito with the generated code
  7. Cognito confirms if the provided code match the generated one and generates the AuthToken to be used

The current login flow looks as:

  1. user inputs username and password in the Angular App

  2. inputs are sent to the backend API

  3. Backend Initiates the authentication flow with Cognito through SDK

    public async Task<AdminInitiateAuthResponse> AuthenticateUser(string username, string password) {
        var authRequest = new AdminInitiateAuthRequest
        {
            UserPoolId = this._poolId,
            ClientId = this._clientId,
            AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH
        };
        authRequest.AuthParameters.Add("USERNAME", username);
        authRequest.AuthParameters.Add("PASSWORD", password);
    
        return await this._client.AdminInitiateAuthAsync(authRequest);
    }
    

Cognito validates the username and password. From the return value we generate a token.

public CognitoUserDto CreateUserToken(AuthenticationResultType authRes) {
    var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
    var awsIdToken = tokenHandler.ReadJwtToken(authRes.IdToken);

    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(claims),
        Expires = DateTime.UtcNow.AddHours(1),
        SigningCredentials = creds
    };

    return new CognitoUserDto
    {
        Email = awsIdToken.Claims.Single(e => e.Type == "email").Value,
        Token = CreateToken(awsIdToken, authRes.AccessToken)
    }; 
}


public string CreateToken(JwtSecurityToken awsToken, string accessToken) {
    var claims = new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Email, awsToken.Claims.Single(e => e.Type == "email").Value),
        new Claim("accessToken", accessToken)
    };

    var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(claims),
        Expires = DateTime.UtcNow.AddHours(1),
        SigningCredentials = creds
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);

    return tokenHandler.WriteToken(token);
}

As for Custom Auth mode with email the sample here was followed (even though its an app flow). Since I failed to find samples/documentation with server side custom auth flow attempts are pretty much restricted to trial and error.

If possible I would like to avoid the need to refactor the whole process to client side flow.

What has been done so far:

  • Replaced AuthFlow in AuthenticateUser method to AuthFlowType.CUSTOM_AUTH
  • User Pool signin experience is set to Optional MFA
  • Created the lambda triggers in AWS Cognito

Issues faced:

  • keeping the ADMIN_NO_SRP_AUTH mode does not trigger the CUSTOM AUTH flow. (somewhat expected)

  • changing the AuthFlow to CUSTOM_AUTH throws an exception. First sentence in AWS Docs state here states "The request for this Lambda trigger contains session", yet the Lambda expression stops with an exception that session is null. Yet, this may be right, since according to step 1 one shall include value for SRP_A, which is kinda obscure how, given no SRP is available.

  • reworked the defaultAuthChallenge Lambda expression to handle if session is null. In this case Lambda was executed without exception, however challengeName and session are not returned, so kinda blocked how to call AdminRespondToAuthChallengeAsync() method

     public async Task<AdminRespondToAuthChallengeResponse> RespondToCognitoChallenge(string token, string session) {
         return await this._client.AdminRespondToAuthChallengeAsync(new AdminRespondToAuthChallengeRequest() {
             ChallengeName = ChallengeNameType.CUSTOM_CHALLENGE,
             CientId = this._clientId,
             UserPoolId = this._poolId,
             ChallengeResponses = new Dicitonary<string, string> {{ "token", token }},
             Session = session
         });
     }
    

The reworked defaultAuthChallenge Lambda look like:

export const handler = async (event) => {
if (event.request?.session === null || event.request.session.length === 0) {
    //SRP_A is the first challenge, this will be implemented by cognito. Set next challenge as PASSWORD_VERIFIER.
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'PASSWORD_VERIFIER';
    
} else if (event.request.session && event.request.session.length === 1
    && event.request.session[1].challengeName === 'PASSWORD_VERIFIER'
    && event.request.session[1].challengeResult === true) {
    //If password verification is successful then set next challenge as CUSTOM_CHALLENGE.
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
    
} else if (event.request.session && event.request.session.length >= 5
    && event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE'
    && event.request.session.slice(-1)[0].challengeResult === false) {
    //The user has exhausted 3 attempts to enter correct otp.
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
    
} else if (event.request.session  && event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE'
    && event.request.session.slice(-1)[0].challengeResult === true) {
    //User entered the correct OTP. Issue tokens.
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
    
} else {
    //user did not provide a correct answer yet.
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
}

return event;

};

Your kind help is apprecaited!

Neophyte
  • 132
  • 1
  • 8
  • some references I have checked mostly in SO Missing required parameter SRP_A ==> https://stackoverflow.com/questions/41115639/aws-cognito-user-authentication-missing-required-parameter-srp-a Handling session == null case: ==> https://stackoverflow.com/a/55824970/9993775 https://stackoverflow.com/questions/67752552/cognito-custom-auth-trigger-not-getting-session-from-cognito – Neophyte Jan 31 '23 at 17:45

0 Answers0