Skip to content

Commit

Permalink
feat(api): respondToAuthChallenge supports NEW_PASSWORD_REQUIRED
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Nov 25, 2021
1 parent 5e5aa36 commit 6a75fea
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 63 deletions.
64 changes: 64 additions & 0 deletions integration-tests/aws-sdk/respondToAuthChallenge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { UUID } from "../../src/__tests__/patterns";
import { attributeValue } from "../../src/services/userPoolClient";
import { withCognitoSdk } from "./setup";

describe(
"CognitoIdentityServiceProvider.respondToAuthChallenge",
withCognitoSdk((Cognito) => {
it("handles NEW_PASSWORD_REQUIRED challenge", async () => {
const client = Cognito();

const upc = await client
.createUserPoolClient({
UserPoolId: "test",
ClientName: "test",
})
.promise();

const createUserResponse = await client
.adminCreateUser({
TemporaryPassword: "def",
UserAttributes: [{ Name: "email", Value: "[email protected]" }],
Username: "abc",
UserPoolId: "test",
})
.promise();
const userSub = attributeValue(
"sub",
createUserResponse.User?.Attributes
);

const initiateAuthResponse = await client
.initiateAuth({
ClientId: upc.UserPoolClient?.ClientId!,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: "abc",
PASSWORD: "def",
},
})
.promise();

const response = await client
.respondToAuthChallenge({
ChallengeName: "NEW_PASSWORD_REQUIRED",
ClientId: upc.UserPoolClient?.ClientId!,
Session: initiateAuthResponse.Session,
ChallengeResponses: {
USERNAME: "abc",
NEW_PASSWORD: "new_password",
},
})
.promise();

expect(response).toEqual({
AuthenticationResult: {
AccessToken: expect.any(String),
IdToken: expect.any(String),
RefreshToken: expect.any(String),
},
ChallengeParameters: {},
});
});
})
);
271 changes: 217 additions & 54 deletions src/targets/respondToAuthChallenge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { advanceTo } from "jest-date-mock";
import jwt from "jsonwebtoken";
import { ClockFake } from "../__tests__/clockFake";
import { MockUserPoolClient } from "../__tests__/mockUserPoolClient";
Expand All @@ -14,25 +13,26 @@ import {
RespondToAuthChallenge,
RespondToAuthChallengeTarget,
} from "./respondToAuthChallenge";
import { User } from "../services/userPoolClient";

const currentDate = new Date();

describe("RespondToAuthChallenge target", () => {
let respondToAuthChallenge: RespondToAuthChallengeTarget;
let mockCognitoClient: jest.Mocked<CognitoClient>;
let now: Date;
let clock: ClockFake;

beforeEach(() => {
now = new Date(2020, 1, 2, 3, 4, 5);
advanceTo(now);

mockCognitoClient = {
getAppClient: jest.fn(),
getUserPool: jest.fn().mockResolvedValue(MockUserPoolClient),
getUserPoolForClientId: jest.fn().mockResolvedValue(MockUserPoolClient),
};

clock = new ClockFake(currentDate);
respondToAuthChallenge = RespondToAuthChallenge({
cognitoClient: mockCognitoClient,
clock: new ClockFake(now),
clock,
});
});

Expand Down Expand Up @@ -65,34 +65,227 @@ describe("RespondToAuthChallenge target", () => {
);
});

describe("when code matches", () => {
it("generates tokens", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue({
Attributes: [
{ Name: "sub", Value: "0000-0000" },
{ Name: "email", Value: "[email protected]" },
],
it("throws if ChallengeResponses.USERNAME is missing", async () => {
await expect(
respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {},
})
).rejects.toEqual(
new InvalidParameterError("Missing required parameter USERNAME")
);
});

it("throws if Session is missing", async () => {
// we don't actually do anything with the session right now, but we still want to
// replicate Cognito's behaviour if you don't provide it
await expect(
respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {
USERNAME: "abc",
},
})
).rejects.toEqual(
new InvalidParameterError("Missing required parameter Session")
);
});

describe("ChallengeName=SMS_MFA", () => {
const user: User = {
Attributes: [
{ Name: "sub", Value: "0000-0000" },
{ Name: "email", Value: "[email protected]" },
],
UserStatus: "CONFIRMED",
Password: "hunter2",
Username: "0000-0000",
Enabled: true,
UserCreateDate: currentDate.getTime(),
UserLastModifiedDate: currentDate.getTime(),
MFACode: "1234",
};
describe("when code matches", () => {
it("updates the user and removes the MFACode", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

const newDate = clock.advanceBy(1200);

await respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {
USERNAME: "0000-0000",
SMS_MFA_CODE: "1234",
},
Session: "Session",
});

expect(MockUserPoolClient.saveUser).toHaveBeenCalledWith({
...user,
MFACode: undefined,
UserLastModifiedDate: newDate.getTime(),
});
});

it("generates tokens", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

const output = await respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {
USERNAME: "0000-0000",
SMS_MFA_CODE: "1234",
},
Session: "Session",
});

expect(output).toBeDefined();

// access token
expect(output.AuthenticationResult?.AccessToken).toBeDefined();
const decodedAccessToken = jwt.decode(
output.AuthenticationResult?.AccessToken ?? ""
);
expect(decodedAccessToken).toMatchObject({
client_id: "clientId",
iss: "http://localhost:9229/test",
sub: "0000-0000",
token_use: "access",
username: "0000-0000",
event_id: expect.stringMatching(UUID),
scope: "aws.cognito.signin.user.admin", // TODO: scopes
auth_time: Math.floor(clock.get().getTime() / 1000),
jti: expect.stringMatching(UUID),
});
expect(
jwt.verify(
output.AuthenticationResult?.AccessToken ?? "",
PublicKey.pem,
{
algorithms: ["RS256"],
}
)
).toBeTruthy();

// id token
expect(output.AuthenticationResult?.IdToken).toBeDefined();
const decodedIdToken = jwt.decode(
output.AuthenticationResult?.IdToken ?? ""
);
expect(decodedIdToken).toMatchObject({
aud: "clientId",
iss: "http://localhost:9229/test",
sub: "0000-0000",
token_use: "id",
"cognito:username": "0000-0000",
email_verified: true,
event_id: expect.stringMatching(UUID),
auth_time: Math.floor(clock.get().getTime() / 1000),
email: "[email protected]",
});
expect(
jwt.verify(
output.AuthenticationResult?.IdToken ?? "",
PublicKey.pem,
{
algorithms: ["RS256"],
}
)
).toBeTruthy();
});
});

describe("when code is incorrect", () => {
it("throws an error", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

await expect(
respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {
USERNAME: "0000-0000",
SMS_MFA_CODE: "4321",
},
Session: "Session",
})
).rejects.toBeInstanceOf(CodeMismatchError);
});
});
});

describe("ChallengeName=NEW_PASSWORD_REQUIRED", () => {
const user: User = {
Attributes: [
{ Name: "sub", Value: "0000-0000" },
{ Name: "email", Value: "[email protected]" },
],
UserStatus: "FORCE_CHANGE_PASSWORD",
Password: "hunter2",
Username: "0000-0000",
Enabled: true,
UserCreateDate: currentDate.getTime(),
UserLastModifiedDate: currentDate.getTime(),
};

it("throws if NEW_PASSWORD missing", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

await expect(
respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "NEW_PASSWORD_REQUIRED",
ChallengeResponses: {
USERNAME: "0000-0000",
},
Session: "session",
})
).rejects.toEqual(
new InvalidParameterError("Missing required parameter NEW_PASSWORD")
);
});

it("updates the user's password and status", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

const newDate = clock.advanceBy(1200);

await respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "NEW_PASSWORD_REQUIRED",
ChallengeResponses: {
USERNAME: "0000-0000",
NEW_PASSWORD: "foo",
},
Session: "Session",
});

expect(MockUserPoolClient.saveUser).toHaveBeenCalledWith({
...user,
Password: "foo",
UserLastModifiedDate: newDate.getTime(),
UserStatus: "CONFIRMED",
Password: "hunter2",
Username: "0000-0000",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
MFACode: "1234",
});
});

it("generates tokens", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue(user);

const output = await respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeName: "NEW_PASSWORD_REQUIRED",
ChallengeResponses: {
USERNAME: "0000-0000",
SMS_MFA_CODE: "1234",
NEW_PASSWORD: "foo",
},
Session: "Session",
});

expect(output).toBeDefined();
expect(output.Session).toBe("Session");

// access token
expect(output.AuthenticationResult?.AccessToken).toBeDefined();
Expand All @@ -107,7 +300,7 @@ describe("RespondToAuthChallenge target", () => {
username: "0000-0000",
event_id: expect.stringMatching(UUID),
scope: "aws.cognito.signin.user.admin", // TODO: scopes
auth_time: Math.floor(now.getTime() / 1000),
auth_time: Math.floor(clock.get().getTime() / 1000),
jti: expect.stringMatching(UUID),
});
expect(
Expand All @@ -133,7 +326,7 @@ describe("RespondToAuthChallenge target", () => {
"cognito:username": "0000-0000",
email_verified: true,
event_id: expect.stringMatching(UUID),
auth_time: Math.floor(now.getTime() / 1000),
auth_time: Math.floor(clock.get().getTime() / 1000),
email: "[email protected]",
});
expect(
Expand All @@ -143,34 +336,4 @@ describe("RespondToAuthChallenge target", () => {
).toBeTruthy();
});
});

describe("when code is incorrect", () => {
it("throws an error", async () => {
MockUserPoolClient.getUserByUsername.mockResolvedValue({
Attributes: [
{ Name: "sub", Value: "0000-0000" },
{ Name: "email", Value: "[email protected]" },
],
UserStatus: "CONFIRMED",
Password: "hunter2",
Username: "0000-0000",
Enabled: true,
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
MFACode: "1234",
});

await expect(
respondToAuthChallenge({
ClientId: "clientId",
ChallengeName: "SMS_MFA",
ChallengeResponses: {
USERNAME: "0000-0000",
SMS_MFA_CODE: "4321",
},
Session: "Session",
})
).rejects.toBeInstanceOf(CodeMismatchError);
});
});
});
Loading

0 comments on commit 6a75fea

Please sign in to comment.