skip to Main Content

I’m using JS aws-sdk to implement remember device option for users who want to skip MFA in my backend Node.js Lambda API. I followed the official AWS blog and code from AWS cognito archives from package amazon-cognito-identity-js but still I was getting "Incorrect Username or Password" error on responding to DEVICE_PASSWORD_VERIFIER.
I have used "User opt-in" method in cognito pool settings for remember device so I have to ask user whether to remember device or not.
I have used certain functions directly from amazon-cognito-identity-js which can be just replicated but I preferred to do it that way to make sure it is the way as intended by AWS. These are the following steps to recreate the error:

  1. Confirming the device
import AuthenticationHelper from 'amazon-cognito-identity-js/src/AuthenticationHelper.js';
import { Buffer } from 'buffer';
import { util } from 'aws-sdk/global';
import DateHelper from 'amazon-cognito-identity-js/src/DateHelper.js';

.
.
// I'm performing confirm device during MFA, it can be done in normal Auth too
cognitoAdminUser.adminRespondToAuthChallenge({
      UserPoolId: user_pool_id,
      ClientId: client_id,
      ChallengeName: 'SOFTWARE_TOKEN_MFA',
      Session: Session,
      ChallengeResponses: {
        SOFTWARE_TOKEN_MFA_CODE: body.mfaCode,
        USERNAME: body.user_name
      },
   }, async (err, data) => {
        .
        .

        const helperObj = new AuthenticationHelper(user_pool_id.split('_')[1]);
        // using generateHashDevice from package function to get password verifier and random password
        helperObj.generateHashDevice(data.AuthenticationResult.NewDeviceMetadata.DeviceGroupKey, data.AuthenticationResult.NewDeviceMetadata.DeviceKey, (err, result) => {
             if (err) {
                console.log({err});
             } else {
                console.log({result});
             }
        })
        const randomPassword = helperObj.getRandomPassword();
        await new Promise((res, rej) => {
           cognitoAdminUser.confirmDevice({
               AccessToken: data.AuthenticationResult.AccessToken,
               DeviceKey: data.AuthenticationResult.NewDeviceMetadata.DeviceKey,
               DeviceName: userPoolLookup.email,
               DeviceSecretVerifierConfig: {
                  PasswordVerifier: Buffer.from(helperObj.getVerifierDevices(), 'hex').toString('base64'),
                  Salt: Buffer.from(helperObj.getSaltDevices(), 'hex').toString('base64')
           }
        }, async (err, result) => {
             .
             .
             // returning all tokens, deviceKey, deviceGroupKey and random password to store in client browser
             // device parameters would be used in next authentication to fetch tokens
           }
  1. Authenticating the next time using adminInitiateAuth an respoding to DEVICE_SRP_AUTH and DEVICE_PASSWORD_VERIFIER challenges
await new Promise((res, rej) => {
     const params = {
        USERNAME: body.user_name,
        PASSWORD: body.password,
        DEVICE_KEY: body.deviceKey
     }
     cognitoAdminUser.adminInitiateAuth({
        AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
        ClientId: client_id,
        UserPoolId: user_pool_id,
        AuthParameters: params
        async (error, data) => {
            if (error) {
                rej(error);
            } else {
                if (data.ChallengeName === 'DEVICE_SRP_AUTH') {                              
                     await new Promise((res1, rej1) => {
                         const deviceParams = {
                             DEVICE_KEY: body.deviceKey,
                             USERNAME: body.user_name,
                         }
                         helperObj.getLargeAValue((err, aValue) => {
                             if (err) {
                                 rej1(err);
                             }
                             deviceParams.SRP_A = aValue.toString(16);
                        });
                        cognitoAdminUser.adminRespondToAuthChallenge({
                             UserPoolId: userPoolLookup.user_pool_id,
                             ClientId: userPoolLookup.client_id,
                             ChallengeName: 'DEVICE_SRP_AUTH',
                             Session: data.Session,
                             ChallengeResponses: deviceParams,
                        }, async (err, data1) => {
                               if (err) {
                                   rej1(err);
                               } else {
                                   if (data1.ChallengeName === 'DEVICE_PASSWORD_VERIFIER') {
                                       function deviceChallenge(result, password, deviceGroupKey, deviceKey, userName) {
                                           let hkdf;
                                           const dateHelper = new DateHelper();
                                           const time = dateHelper.getNowString();
                                           const secretBlock = result.SECRET_BLOCK;
                                           const hkdf = helperObj.getPasswordAuthenticationKey(deviceGroupKey, deviceKey, password, result.SRP_B, result.SALT);
                                                                                        
                                                                                        
                                           const signature = util.crypto.hmac(hkdf, util.buffer.concat([
                                               Buffer.from(deviceGroupKey, 'utf8'),
                                               Buffer.from(deviceKey, 'utf8'),
                                               Buffer.from(secretBlock, 'base64'),
                                               Buffer.from(time, 'utf8')
                                           ]), 'base64', 'sha256');                            
                                           return {
                                                    TIMESTAMP: time,
                                                    USERNAME: userName,
                                                    PASSWORD_CLAIM_SECRET_BLOCK: result.SECRET_BLOCK,
                                                    PASSWORD_CLAIM_SIGNATURE: signature,
                                                    DEVICE_KEY: deviceKey
                                                  }
                                           }
                                           await new Promise((res2, rej2) => {
                                               const verifierParams =  deviceChallenge(data1.ChallengeParameters, user.devicePassword, user.deviceGroupKey, user.deviceKey, data1.ChallengeParameters.USERNAME);
                                               cognitoAdminUser.adminRespondToAuthChallenge({
                                                   UserPoolId: userPoolLookup.user_pool_id,
                                                   ClientId: userPoolLookup.client_id,
                                                   ChallengeName: 'DEVICE_PASSWORD_VERIFIER',
                                                   Session: data.Session,
                                                   ChallengeResponses: verifierParams,
                                               }, async (err, data2) => {
                                                    // deriving tokens from `data2`
                                                  }
                                               })

Even after using functions directly given by helper functions from the package, I think the the hkdf derived is wrong. I have tried using functions built based of these docs but with the same result.

2

Answers


  1. Chosen as BEST ANSWER

    As I understood the problem is with the amazon-cognito-identity-js AuthenticationHelper.js function called getPasswordAuthenticationKey as it is not designed for the purpose to respond to DEVICE_PASSWORD_VERIFIER. So instead of changing the function within the package itself I just simply replicated only the getPasswordAuthenticationKey function and followed a bit of blog which also is wrong as it is saying to do SHA256_HASH(DeviceGroupKey + username + ":" + RANDOM_PASSWORD) instead of SHA256_HMAC(K_USER, DeviceGroupKey + DeviceKey + PASSWORD_CLAIM_SECRET_BLOCK + TIMESTAMP). Here is the only portion of code where I recreated that function in my code along with it's usage:

    1. Defining getPasswordAuthenticationKey function
     function getPasswordAuthenticationKey(deviceGroupKey, deviceKey, devicePassword, serverBValue, salt, callback) {
         if (serverBValue.mod(helperObj.N).equals(BigInteger.ZERO)) {
            throw new Error('B cannot be zero.');
         }
         helperObj.UValue = helperObj.calculateU(helperObj.largeAValue, serverBValue);
         if (helperObj.UValue.equals(BigInteger.ZERO)) {
            throw new Error('U cannot be zero.')
         }
         const usernamePassword = `${deviceGroupKey}${deviceKey}:${devicePassword}`;
         const usernamePasswordHash = helperObj.hash(usernamePassword);
         const xValue = new BigInteger(
         helperObj.hexHash(helperObj.padHex(salt) + usernamePasswordHash),16);
         helperObj.calculateS(xValue, serverBValue, (err, sValue) => {
            if (err) {
               callback(err, null);
            }
            const hkdf = helperObj.computehkdf(
               Buffer.from(helperObj.padHex(sValue), 'hex'),
               Buffer.from(helperObj.padHex(helperObj.UValue), 'hex')
            );
            callback(null, hkdf);                                                                                        
          });                                                            
    }
    
    1. Usage in the function above in question
     function deviceChallenge(result, password, deviceGroupKey, deviceKey, userName) {
     .
     .
     // instead of this
     // const hkdf = helperObj.getPasswordAuthenticationKey(deviceGroupKey, deviceKey, password, result.SRP_B, result.SALT);
     // use above given function                                                                               
     getPasswordAuthenticationKey(deviceGroupKey, deviceKey, password, new BigInteger(result.SRP_B, '16'), new BigInteger(result.SALT, '16'), (err, data) => {
        if (err) {
           rej1(err);
        } else {
           hkdf = data;
        }
     })
     .
     .
     .
    }
    
    

  2. You must provide a SECRET_HASH parameter in all challenge responses to an app client that has a client secret

    To compute the secret hash
    https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash

    For further reading be sure to use these update docs.
    https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-cognito-identity-provider/Interface/RespondToAuthChallengeRequest/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search