Closed renatoargh closed 2 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
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 π» !
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
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);
})();
Hey @simonmcallister0210 thanks for your update! I will start checking it today!
Hey @renatoargh Were you able to figure out why you were getting // NotAuthorizedException: Incorrect username or password.
?
I am facing the same issue.
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
Hey @renatoargh, @simonmcallister0210 do you have any updates on this? I'm stuck on this error.
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
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 π» !
Have a working demo on the linked branch. Just need to polish it off, write tests, update docs, etc.
Have a working demo on the linked branch. Just need to polish it off, write tests, update docs, etc.
Thank you so much, @simonmcallister0210 , for a quick turnaround. Really appreciate it. I'll give it a try and let you know.
Made a rod for my own back writing so many unit tests, not sure how useful most of those tests are tbh... just need to finish the integration test cases and documentation then we should be good to go
Hello again!
I've returned to the project where this feature would be extremely helpful. I attempted to test the code in this branch, but unfortunately, I ran into some issues. Would it be possible for you to publish the contents of this branch as a beta tagged release on npm?
Thanks so much for your help! It looks like the device support is in place, and I suspect the issues might be on my end.
Sure. I've published 2.3.0-beta-v1. If all goes well on the beta release I'll merge this branch in next week / week after. Let me know if there's any issues π
One thing to be aware of: the request wrapper functions (wrapInitiateAuth
and wrapAuthChallenge
) don't use USER_ID_FOR_SRP
. You would need to add it yourself like so:
const USER_ID_FOR_SRP = initiateAuthRes2.ChallengeParameters?.USER_ID_FOR_SRP;
// . . .
new RespondToAuthChallengeCommand(
wrapAuthChallenge(signedSrpSession, {
ClientId: clientId,
ChallengeName: "SOFTWARE_TOKEN_MFA",
ChallengeResponses: {
SECRET_HASH: secretHash,
SOFTWARE_TOKEN_MFA_CODE: otp,
USERNAME: USER_ID_FOR_SRP, // <-- here
},
Session: session,
}),
,
The integration tests are a good reference if you need it. In a future release I'll just attach USER_ID_FOR_SRP
to the SRP session
Hey, thanks for your quick response! I am testing it now (successfully installed the beta version from npm) and I am now adapting my code because currently, I am still using the workaround from https://github.com/simonmcallister0210/cognito-srp-helper/issues/26
A quick question; what exactly is the secretId, as mentioned here (in my case I am not generating client secrets):
When you enable secrets you will get that secret ID for the app. Then you'd need to call that function to calculate the value for SECRET_HASH, which you need to pass to all your Cognito API requests. Since you dont have a secret you won't need to provide a SECRET_HASH
Hey @simonmcallister0210, thank you so much. I can confirm that I am finally able to make the entire flow work! π₯³ I would like to send you a few π»s, make sure to enable sponsoring
It's ok, I'm just happy people are using the library π thanks for your patience
I'll wait for another week in case there are any issues, then I'll release the beta fix in 2.3.0
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
andDEVICE_PASSWORD_VERIFIER
challenges which are necessary to authenticate remembered devices.My current (working) code is this;
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 areDEVICE_SRP_AUTH
andDEVICE_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 adaptcognito-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!!!!