Skip to content

Commit

Permalink
fix(api): finish implementation of changePassword
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Jan 15, 2022
1 parent cff0d2d commit f649bfa
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
| AdminUpdateUserAttributes ||
| AdminUserGlobalSignOut ||
| AssociateSoftwareToken ||
| ChangePassword | 🕒 (partial support) |
| ChangePassword | |
| ConfirmDevice ||
| ConfirmForgotPassword | 🕒 (partial support) |
| ConfirmSignUp | 🕒 (partial support) |
Expand Down
87 changes: 87 additions & 0 deletions integration-tests/aws-sdk/changePassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { withCognitoSdk } from "./setup";

describe(
"CognitoIdentityServiceProvider.changePassword",
withCognitoSdk((Cognito) => {
it("deletes the current user", async () => {
const client = Cognito();

// create the user pool client
const upc = await client
.createUserPoolClient({
UserPoolId: "test",
ClientName: "test",
})
.promise();

// create a user
await client
.adminCreateUser({
DesiredDeliveryMediums: ["EMAIL"],
TemporaryPassword: "def",
UserAttributes: [{ Name: "email", Value: "[email protected]" }],
Username: "abc",
UserPoolId: "test",
})
.promise();

await client
.adminSetUserPassword({
Password: "firstPassword",
Permanent: true,
Username: "abc",
UserPoolId: "test",
})
.promise();

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

// delete the user with their token
await client
.changePassword({
AccessToken: initAuthResponse.AuthenticationResult?.AccessToken!,
PreviousPassword: "firstPassword",
ProposedPassword: "secondPassword",
})
.promise();

// (fail to) login with the old password
await expect(
client
.initiateAuth({
ClientId: upc.UserPoolClient?.ClientId!,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: "abc",
PASSWORD: "firstPassword",
},
})
.promise()
).rejects.toBeDefined();

// login with the new password
const initAuthResponse2nd = await client
.initiateAuth({
ClientId: upc.UserPoolClient?.ClientId!,
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: "abc",
PASSWORD: "secondPassword",
},
})
.promise();

expect(initAuthResponse2nd).toBeDefined();
});
})
);
148 changes: 148 additions & 0 deletions src/targets/changePassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import jwt from "jsonwebtoken";
import * as uuid from "uuid";
import { ClockFake } from "../__tests__/clockFake";
import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { TestContext } from "../__tests__/testContext";
import * as TDB from "../__tests__/testDataBuilder";
import {
InvalidParameterError,
InvalidPasswordError,
NotAuthorizedError,
} from "../errors";
import PrivateKey from "../keys/cognitoLocal.private.json";
import { UserPoolService } from "../services";
import { ChangePassword, ChangePasswordTarget } from "./changePassword";

const currentDate = new Date();

describe("ChangePassword target", () => {
let changePassword: ChangePasswordTarget;
let mockUserPoolService: jest.Mocked<UserPoolService>;

beforeEach(() => {
mockUserPoolService = newMockUserPoolService();
changePassword = ChangePassword({
cognito: newMockCognitoService(mockUserPoolService),
clock: new ClockFake(currentDate),
});
});

it("throws if token isn't valid", async () => {
await expect(
changePassword(TestContext, {
AccessToken: "blah",
PreviousPassword: "abc",
ProposedPassword: "def",
})
).rejects.toBeInstanceOf(InvalidParameterError);

expect(mockUserPoolService.saveUser).not.toHaveBeenCalled();
});

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

await expect(
changePassword(TestContext, {
AccessToken: jwt.sign(
{
sub: "0000-0000",
event_id: "0",
token_use: "access",
scope: "aws.cognito.signin.user.admin",
auth_time: new Date(),
jti: uuid.v4(),
client_id: "test",
username: "0000-0000",
},
PrivateKey.pem,
{
algorithm: "RS256",
issuer: `http://localhost:9229/test`,
expiresIn: "24h",
keyid: "CognitoLocal",
}
),
PreviousPassword: "abc",
ProposedPassword: "def",
})
).rejects.toEqual(new NotAuthorizedError());

expect(mockUserPoolService.saveUser).not.toHaveBeenCalled();
});

it("throws if previous password doesn't match", async () => {
const user = TDB.user({
Password: "previous-password",
});

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await expect(
changePassword(TestContext, {
AccessToken: jwt.sign(
{
sub: "0000-0000",
event_id: "0",
token_use: "access",
scope: "aws.cognito.signin.user.admin",
auth_time: new Date(),
jti: uuid.v4(),
client_id: "test",
username: "0000-0000",
},
PrivateKey.pem,
{
algorithm: "RS256",
issuer: `http://localhost:9229/test`,
expiresIn: "24h",
keyid: "CognitoLocal",
}
),
PreviousPassword: "abc",
ProposedPassword: "def",
})
).rejects.toEqual(new InvalidPasswordError());

expect(mockUserPoolService.saveUser).not.toHaveBeenCalled();
});

it("updates the user's password if the previous password matches", async () => {
const user = TDB.user({
Password: "previous-password",
});

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await changePassword(TestContext, {
AccessToken: jwt.sign(
{
sub: "0000-0000",
event_id: "0",
token_use: "access",
scope: "aws.cognito.signin.user.admin",
auth_time: new Date(),
jti: uuid.v4(),
client_id: "test",
username: "0000-0000",
},
PrivateKey.pem,
{
algorithm: "RS256",
issuer: `http://localhost:9229/test`,
expiresIn: "24h",
keyid: "CognitoLocal",
}
),
PreviousPassword: "previous-password",
ProposedPassword: "new-password",
});

expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(TestContext, {
...user,
Password: "new-password",
UserLastModifiedDate: currentDate,
});
});
});
28 changes: 22 additions & 6 deletions src/targets/changePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,43 @@ import {
} from "aws-sdk/clients/cognitoidentityserviceprovider";
import jwt from "jsonwebtoken";
import { Services } from "../services";
import { NotAuthorizedError } from "../errors";
import {
InvalidParameterError,
InvalidPasswordError,
NotAuthorizedError,
} from "../errors";
import { Token } from "../services/tokenGenerator";
import { Target } from "./router";

export type ChangePasswordTarget = Target<
ChangePasswordRequest,
ChangePasswordResponse
>;

type ChangePasswordServices = Pick<Services, "cognito" | "clock">;

export const ChangePassword =
({ cognito, clock }: Services): ChangePasswordTarget =>
({ cognito, clock }: ChangePasswordServices): ChangePasswordTarget =>
async (ctx, req) => {
const claims = jwt.decode(req.AccessToken) as any;
const decodedToken = jwt.decode(req.AccessToken) as Token | null;
if (!decodedToken) {
ctx.logger.info("Unable to decode token");
throw new InvalidParameterError();
}

const userPool = await cognito.getUserPoolForClientId(
ctx,
claims.client_id
decodedToken.client_id
);
const user = await userPool.getUserByUsername(ctx, claims.username);
const user = await userPool.getUserByUsername(ctx, decodedToken.username);
if (!user) {
throw new NotAuthorizedError();
}
// TODO: Should check previous password.

if (req.PreviousPassword !== user.Password) {
throw new InvalidPasswordError();
}

await userPool.saveUser(ctx, {
...user,
Password: req.ProposedPassword,
Expand Down

0 comments on commit f649bfa

Please sign in to comment.