Skip to content

Commit

Permalink
feat: forgot password flow
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Apr 12, 2020
1 parent 44e3cf0 commit 6bd0b42
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ The goal for this project is to be _Good Enough_ for local development use, and

- [x] Sign Up
- [x] Confirm Sign Up
- [x] Initiate Auth/Login
- [x] Initiate Auth (Login)
- [x] Forgot Password

## Installation

Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export class NotAuthorizedError extends CognitoError {
}
}

export class UserNotFoundError extends CognitoError {
public constructor() {
super("UserNotFoundException", "User not found");
}
}

export class UsernameExistsError extends CognitoError {
public constructor() {
super("UsernameExistsException", "User already exists");
Expand Down
111 changes: 111 additions & 0 deletions src/targets/forgotPassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { advanceTo } from "jest-date-mock";
import { UserNotFoundError } from "../errors";
import { CodeDelivery, UserPool } from "../services";
import { ForgotPassword, ForgotPasswordTarget } from "./forgotPassword";

describe("ForgotPassword target", () => {
let forgotPassword: ForgotPasswordTarget;
let mockDataStore: jest.Mocked<UserPool>;
let mockCodeDelivery: jest.Mock;
let now: Date;

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

mockDataStore = {
getUserByUsername: jest.fn(),
saveUser: jest.fn(),
};
mockCodeDelivery = jest.fn();

forgotPassword = ForgotPassword({
storage: mockDataStore as UserPool,
codeDelivery: mockCodeDelivery as CodeDelivery,
});
});

it("throws if user doesn't exist", async () => {
mockDataStore.getUserByUsername.mockResolvedValue(null);

await expect(
forgotPassword({
ClientId: "clientId",
Username: "0000-0000",
})
).rejects.toBeInstanceOf(UserNotFoundError);
});

it("sends a confirmation code to the user's email address", async () => {
mockDataStore.getUserByUsername.mockResolvedValue({
Attributes: [{ Name: "email", Value: "[email protected]" }],
Enabled: true,
Password: "hunter2",
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
UserStatus: "CONFIRMED",
Username: "0000-0000",
});
mockCodeDelivery.mockResolvedValue("1234");

const result = await forgotPassword({
ClientId: "clientId",
Username: "0000-0000",
});

expect(mockCodeDelivery).toHaveBeenCalledWith(
{
Attributes: [{ Name: "email", Value: "[email protected]" }],
Enabled: true,
Password: "hunter2",
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
UserStatus: "CONFIRMED",
Username: "0000-0000",
},
{
AttributeName: "email",
DeliveryMedium: "EMAIL",
Destination: "[email protected]",
}
);

expect(result).toEqual({
CodeDeliveryDetails: {
AttributeName: "email",
DeliveryMedium: "EMAIL",
Destination: "[email protected]",
},
});
});

it("saves the confirmation code on the user for comparison when confirming", async () => {
mockDataStore.getUserByUsername.mockResolvedValue({
Attributes: [{ Name: "email", Value: "[email protected]" }],
Enabled: true,
Password: "hunter2",
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
UserStatus: "CONFIRMED",
Username: "0000-0000",
});
mockCodeDelivery.mockResolvedValue("1234");

await forgotPassword({
ClientId: "clientId",
Username: "0000-0000",
});

expect(mockDataStore.saveUser).toHaveBeenCalledWith({
Attributes: [{ Name: "email", Value: "[email protected]" }],
ConfirmationCode: "1234",
Enabled: true,
Password: "hunter2",
UserCreateDate: now.getTime(),
UserLastModifiedDate: now.getTime(),
// TODO: validate whether an already confirmed user should stay confirmed when password reset starts?
UserStatus: "CONFIRMED",
Username: "0000-0000",
});
});
});
45 changes: 45 additions & 0 deletions src/targets/forgotPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { UserNotFoundError } from "../errors";
import { Services } from "../services";
import { DeliveryDetails } from "../services/codeDelivery/codeDelivery";

interface Input {
ClientId: string;
Username: string;
}

interface Output {
CodeDeliveryDetails: DeliveryDetails;
}

export type ForgotPasswordTarget = (body: Input) => Promise<Output>;

export const ForgotPassword = ({
storage,
codeDelivery,
}: Services): ForgotPasswordTarget => async (body) => {
const user = await storage.getUserByUsername(body.Username);

if (!user) {
throw new UserNotFoundError();
}

const deliveryDetails: DeliveryDetails = {
AttributeName: "email",
DeliveryMedium: "EMAIL",
Destination: user.Attributes.filter((x) => x.Name === "email").map(
(x) => x.Value
)[0],
};

const code = await codeDelivery(user, deliveryDetails);

await storage.saveUser({
...user,
UserLastModifiedDate: new Date().getTime(),
ConfirmationCode: code,
});

return {
CodeDeliveryDetails: deliveryDetails,
};
};
2 changes: 2 additions & 0 deletions src/targets/router.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Services } from "../services";
import { UnsupportedError } from "../errors";
import { ConfirmSignUp } from "./confirmSignUp";
import { ForgotPassword } from "./ForgotPassword";
import { InitiateAuth } from "./initiateAuth";
import { SignUp } from "./signUp";

export const Targets = {
ConfirmSignUp,
ForgotPassword,
InitiateAuth,
SignUp,
};
Expand Down

0 comments on commit 6bd0b42

Please sign in to comment.