simonmcallister0210 / cognito-srp-helper

A helper for SRP authentication in AWS Cognito
Apache License 2.0
9 stars 0 forks source link

Feature request: add support to `DEVICE_SRP_AUTH` and `DEVICE_PASSWORD_VERIFIER` challanges #34

Open renatoargh opened 3 months ago

renatoargh commented 3 months ago

Hey! πŸ‘‹

I am currently working with Cognito's remember devices feature so I would like to know whether it would be possible to add support for the DEVICE_SRP_AUTH and DEVICE_PASSWORD_VERIFIER challenges which are necessary to authenticate remembered devices.

My current (working) code is this;

const email = 'potato@example.com'
const password = 'potato'
const cognitoConfig = { poolId: 'my-pool-id', clientId: 'my-client-id' }

const srpSession = createSrpSession('', '', cognitoConfig.poolId);
const authParameters: Record<string, string> = {
  CHALLENGE_NAME: 'SRP_A',
  SRP_A: srpSession.largeA,
  USERNAME: email,
};

const deviceKey = getStringItem(LocalStorageKey.DEVICE_KEY) || '';
if (deviceKey) {
  authParameters.DEVICE_KEY = deviceKey; // <--- we are making use of remember device feature 
}

const initAuthCommand = new InitiateAuthCommand(
  wrapInitiateAuth(srpSession, {
    ClientId: cognitoConfig.clientId,
    AuthFlow: AuthFlowType.USER_SRP_AUTH,
    AuthParameters: authParameters,
  }),
);

const initAuthResponse = await client.send(initAuthCommand);
const userId = initAuthResponse.ChallengeParameters?.USER_ID_FOR_SRP || '';
srpSession.username = userId;
srpSession.passwordHash = createPasswordHash(userId, password, cognitoConfig.poolId);

const signedSrpSession = signSrpSession(srpSession, initAuthResponse);
const challangeResponses: Record<string, string> = {
  USERNAME: email,
};

if (deviceKey) {
  challangeResponses.DEVICE_KEY = deviceKey; // <--- we are making use of remember device feature 
}

const responseToAuthChallangeCommand = new RespondToAuthChallengeCommand(
  wrapAuthChallenge(signedSrpSession, {
    ClientId: cognitoConfig.clientId,
    ChallengeName: ChallengeNameType.PASSWORD_VERIFIER,
    ChallengeResponses: challangeResponses,
  }),
);

const authChallangeResponse = await client.send(responseToAuthChallangeCommand);

// All good and nice at this point! βœ…

The code above works fine! The problem is that when I have a deviceKey on local storage I then get two more challenges after this point, that are DEVICE_SRP_AUTH and DEVICE_PASSWORD_VERIFIER. I have found instructions here: https://repost.aws/knowledge-center/cognito-user-pool-remembered-devices#:~:text=Call%20RespondToAuthChallenge%20for%20DEVICE_SRP_AUTH but all seems super cryptic to me and I was unable to adapt cognito-srp-helper to work with the device challenges.

Would it be possible to extend this lib to add support for this feature or maybe tell me what steps to follow in order to make it work with the current state of the library?

More references here: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html#user-pools-remembered-devices-signing-in-with-a-device

thank you a lot, great work guys!!!!

simonmcallister0210 commented 3 months ago

Hey @renatoargh good to see you again!

I feel like this should be possible by extending the library. I'll see if I can prototype a solution. I'm in the middle of moving house right now, but I should be able to carve out some time to do this soon

renatoargh commented 3 months ago

Thank you a lot, Simon! Take your time and no rush.. please finish moving houses before paying attention to this! I would love trying to contribute a PR but all seems super cryptic to me. And if you enable sponsoring on this repo I will make sure to send you a few 🍻 !

simonmcallister0210 commented 2 months ago

I've pushed a branch that's WIP

Still trying to figure out the response to DEVICE_PASSWORD_VERIFIER. Getting NotAuthorizedException: Incorrect username or password. so I must've done something wrong in one of the steps? I'll keep looking into it this week

simonmcallister0210 commented 2 months ago

Here's the code I'm using to prototype the fix. Should be able to run it on the new branch. It'll create a new user each time, auto-confirm them, setup MFA, remember their device, etc.

const {
  createSrpSession,
  signSrpSession,
  wrapAuthChallenge,
  wrapInitiateAuth,
  createSecretHash,
  createDeviceVerifier,
  signSrpSessionWithDevice,
} = require("../cognito-srp-helper");
const {
  CognitoIdentityProviderClient,
  InitiateAuthCommand,
  RespondToAuthChallengeCommand,
  AssociateSoftwareTokenCommand,
  SetUserMFAPreferenceCommand,
  VerifySoftwareTokenCommand,
  SignUpCommand,
  ConfirmDeviceCommand,
  UpdateDeviceStatusCommand,
} = require("@aws-sdk/client-cognito-identity-provider");
const { TOTP } = require("totp-generator");
const { faker } = require("@faker-js/faker");

const wait = async (time) => new Promise((resolve) => setTimeout(resolve, time - new Date().getTime()));

(async () => {
  // ---------- Setup credentials for new user ----------

  const username = faker.internet.userName();
  const password = "Qwerty1!";
  const poolId = "eu-west-2_ebRTcgfiK";
  const clientId = "1eci0qkm70jpfov0uo2j1ejep";
  const secretId = "1op7af116gm42riug0brsfku3fr1tl1jn5f54lernp5q1d5mksbv";
  const cognitoIdentityProviderClient = new CognitoIdentityProviderClient({
    region: "eu-west-2",
  });

  console.log("credentials:");
  console.log({ username, password });

  // ---------- Signup with new user ----------

  const secretHash = createSecretHash(username, clientId, secretId);

  const signupRes = await cognitoIdentityProviderClient.send(
    // There's a pre-signup trigger to auto-confirm new users, so no need to Confirm post signup
    new SignUpCommand({
      ClientId: clientId,
      Username: username,
      Password: password,
      SecretHash: secretHash,
    }),
  );

  console.log("signupRes:");
  console.log(signupRes);

  // ---------- Signin 1. initiate signin attempt ----------

  const srpSession1 = createSrpSession(username, password, poolId, false);

  const initiateAuthRes1 = await cognitoIdentityProviderClient
    .send(
      new InitiateAuthCommand(
        wrapInitiateAuth(srpSession1, {
          ClientId: clientId,
          AuthFlow: "USER_SRP_AUTH",
          AuthParameters: {
            CHALLENGE_NAME: "SRP_A",
            SECRET_HASH: secretHash,
            USERNAME: username,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("initiateAuthRes1:");
  console.log(initiateAuthRes1);

  // ---------- Signin 1. respond to PASSWORD_VERIFIER challenge ----------

  const signedSrpSession1 = signSrpSession(srpSession1, initiateAuthRes1);

  const respondToAuthChallengeRes1a = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSession1, {
          ClientId: clientId,
          ChallengeName: "PASSWORD_VERIFIER",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            USERNAME: username,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes1a:");
  console.log(respondToAuthChallengeRes1a);

  // ---------- Associate a TOTP token with the user ----------

  const { AccessToken } = respondToAuthChallengeRes1a.AuthenticationResult;

  const associateSoftwareTokenRes = await cognitoIdentityProviderClient
    .send(
      new AssociateSoftwareTokenCommand({
        AccessToken,
      }),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("associateSoftwareTokenRes:");
  console.log(associateSoftwareTokenRes);

  // ---------- Verify the TOTP token with the user ----------

  const { SecretCode } = associateSoftwareTokenRes;
  const { otp: otp1, expires: expires1 } = TOTP.generate(SecretCode);

  const verifySoftwareTokenRes = await cognitoIdentityProviderClient
    .send(
      new VerifySoftwareTokenCommand({
        AccessToken,
        UserCode: otp1,
      }),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("verifySoftwareTokenRes:");
  console.log(verifySoftwareTokenRes);

  // ---------- Set MFA preference to TOTP ----------

  const setUserMFAPreferenceRes = await cognitoIdentityProviderClient
    .send(
      new SetUserMFAPreferenceCommand({
        AccessToken,
        SoftwareTokenMfaSettings: {
          // won't work unless we associate and verify TOTP token with user
          Enabled: true,
          PreferredMfa: true,
        },
      }),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("setUserMFAPreferenceRes:");
  console.log(setUserMFAPreferenceRes);

  // ---------- Wait for a new OTP to generate ----------

  console.log("waiting for new OTP . . .");
  await wait(expires1);

  // ---------- Signin 2. initiate signin attempt ----------

  const srpSession2 = createSrpSession(username, password, poolId, false);

  const initiateAuthRes2 = await cognitoIdentityProviderClient
    .send(
      new InitiateAuthCommand(
        wrapInitiateAuth(srpSession2, {
          ClientId: clientId,
          AuthFlow: "USER_SRP_AUTH",
          AuthParameters: {
            CHALLENGE_NAME: "SRP_A",
            SECRET_HASH: secretHash,
            USERNAME: username,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("initiateAuthRes2:");
  console.log(initiateAuthRes2);

  // ---------- Signin 2. respond to PASSWORD_VERIFIER challenge ----------

  const signedSrpSession2 = signSrpSession(srpSession2, initiateAuthRes2);

  const respondToAuthChallengeRes2a = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSession2, {
          ClientId: clientId,
          ChallengeName: "PASSWORD_VERIFIER",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            USERNAME: username,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes2a:");
  console.log(respondToAuthChallengeRes2a);

  // ---------- Signin 2. respond to SOFTWARE_TOKEN_MFA challenge ----------

  const { otp: otp2 } = TOTP.generate(SecretCode);
  const { Session: Session2a } = respondToAuthChallengeRes2a;

  const respondToAuthChallengeRes2b = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSession2, {
          ClientId: clientId,
          ChallengeName: "SOFTWARE_TOKEN_MFA",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            SOFTWARE_TOKEN_MFA_CODE: otp2,
            USERNAME: username,
          },
          Session: Session2a,
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes2b:");
  console.log(respondToAuthChallengeRes2b);

  // ---------- Confirm the device (for tracking) ----------

  const { DeviceGroupKey, DeviceKey } = respondToAuthChallengeRes2b.AuthenticationResult.NewDeviceMetadata;
  const DeviceSecretVerifierConfig = createDeviceVerifier(username, DeviceGroupKey);

  const confirmDeviceRes = await cognitoIdentityProviderClient
    .send(
      new ConfirmDeviceCommand({
        AccessToken,
        DeviceKey,
        DeviceName: "example-friendly-name", // usually this is set a User-Agent
        DeviceSecretVerifierConfig,
      }),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("confirmDeviceRes:");
  console.log(confirmDeviceRes);

  // ---------- Remember the device (for easier logins) ----------

  const updateDeviceStatusRes = await cognitoIdentityProviderClient
    .send(
      new UpdateDeviceStatusCommand({
        AccessToken,
        DeviceKey,
        DeviceRememberedStatus: "remembered",
      }),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("updateDeviceStatusRes:");
  console.log(updateDeviceStatusRes);

  // ---------- Signin 3. initiate signin attempt ----------

  const srpSession3 = createSrpSession(username, password, poolId, false);

  const initiateAuthRes3 = await cognitoIdentityProviderClient
    .send(
      new InitiateAuthCommand(
        wrapInitiateAuth(srpSession3, {
          ClientId: clientId,
          AuthFlow: "USER_SRP_AUTH",
          AuthParameters: {
            CHALLENGE_NAME: "SRP_A",
            SECRET_HASH: secretHash,
            USERNAME: username,
            DEVICE_KEY: DeviceKey,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("initiateAuthRes3:");
  console.log(initiateAuthRes3);

  // ---------- Signin 3. respond to PASSWORD_VERIFIER challenge ----------

  const signedSrpSession3 = signSrpSession(srpSession3, initiateAuthRes3);

  const respondToAuthChallengeRes3a = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSession3, {
          ClientId: clientId,
          ChallengeName: "PASSWORD_VERIFIER",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            USERNAME: username,
            DEVICE_KEY: DeviceKey,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes3a:");
  console.log(respondToAuthChallengeRes3a);

  // ---------- Signin 3. respond to DEVICE_SRP_AUTH challenge ----------

  const respondToAuthChallengeRes3b = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSession3, {
          ClientId: clientId,
          ChallengeName: "DEVICE_SRP_AUTH",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            USERNAME: username,
            DEVICE_KEY: DeviceKey,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes3b:");
  console.log(respondToAuthChallengeRes3b);

  // ---------- Signin 3. respond to DEVICE_PASSWORD_VERIFIER challenge ----------

  // `NotAuthorizedException: Incorrect username or password.`
  // Must be doing something wrong around here . . .

  const signedSrpSessionWithDevice3 = signSrpSessionWithDevice(
    signedSrpSession3,
    respondToAuthChallengeRes3b,
    DeviceGroupKey,
    DeviceSecretVerifierConfig.RandomPassword,
  );

  const respondToAuthChallengeRes3c = await cognitoIdentityProviderClient
    .send(
      new RespondToAuthChallengeCommand(
        wrapAuthChallenge(signedSrpSessionWithDevice3, {
          ClientId: clientId,
          ChallengeName: "DEVICE_PASSWORD_VERIFIER",
          ChallengeResponses: {
            SECRET_HASH: secretHash,
            USERNAME: username,
            DEVICE_KEY: respondToAuthChallengeRes3b.ChallengeParameters.DEVICE_KEY,
          },
        }),
      ),
    )
    .catch((err) => {
      console.error(err);
      throw err;
    });

  console.log("respondToAuthChallengeRes3c:");
  console.log(respondToAuthChallengeRes3c);
})();
renatoargh commented 2 months ago

Hey @simonmcallister0210 thanks for your update! I will start checking it today!

spatel2693 commented 2 weeks ago

Hey @renatoargh Were you able to figure out why you were getting // NotAuthorizedException: Incorrect username or password. ?

I am facing the same issue.

renatoargh commented 2 weeks ago

Hey @spatel2693 we had a big detour on the project and I will be back to it next Monday. So far no progress but if I figure something out I will post it here

spatel2693 commented 2 days ago

Hey @renatoargh, @simonmcallister0210 do you have any updates on this? I'm stuck on this error.

simonmcallister0210 commented 1 day ago

Hey. I had a look at this a month ago and couldn't make any progress.. but I think it's just a case of getting the algorithm correct. I must be making a mistake in the calculation somewhere. I'll have another go over the next few days

spatel2693 commented 4 hours ago

Hey. I had a look at this a month ago and couldn't make any progress.. but I think it's just a case of getting the algorithm correct. I must be making a mistake in the calculation somewhere. I'll have another go over the next few days

Thank you, @simonmcallister0210. I really appreciate this. If you enable sponsoring on this repo, I will make sure to send you a few 🍻 !

simonmcallister0210 commented 1 hour ago

Have a working demo on the linked branch. Just need to polish it off, write tests, update docs, etc.