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:
- 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
}
- Authenticating the next time using
adminInitiateAuth
an respoding toDEVICE_SRP_AUTH
andDEVICE_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
As I understood the problem is with the amazon-cognito-identity-js
AuthenticationHelper.js
function calledgetPasswordAuthenticationKey
as it is not designed for the purpose to respond toDEVICE_PASSWORD_VERIFIER
. So instead of changing the function within the package itself I just simply replicated only thegetPasswordAuthenticationKey
function and followed a bit of blog which also is wrong as it is saying to doSHA256_HASH(DeviceGroupKey + username + ":" + RANDOM_PASSWORD)
instead ofSHA256_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:getPasswordAuthenticationKey
functionYou 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/