Skip to content

Commit

Permalink
feat(api): getUserAttributeVerificationCode full support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Dec 10, 2021
1 parent 320dd17 commit 90726a4
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
| GetSigningCertificate ||
| GetUICustomization ||
| GetUser ||
| GetUserAttributeVerificationCode | |
| GetUserAttributeVerificationCode | |
| GetUserPoolMfaConfig ||
| GlobalSignOut ||
| InitiateAuth | 🕒 (partial support) |
Expand Down
71 changes: 71 additions & 0 deletions integration-tests/aws-sdk/getUserAttributeVerificationCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { UUID } from "../../src/__tests__/patterns";
import { TestContext } from "../../src/__tests__/testContext";
import { withCognitoSdk } from "./setup";
import { User } from "../../src/services/userPoolService";

describe(
"CognitoIdentityServiceProvider.getUserAttributeVerificationCode",
withCognitoSdk((Cognito, DataStoreFactory) => {
it("sends a verification code for a user's attribute", async () => {
const client = Cognito();

const pool = await client
.createUserPool({
PoolName: "test",
AutoVerifiedAttributes: ["email"],
})
.promise();
const userPoolId = pool.UserPool?.Id as string;

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

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

await client
.adminConfirmSignUp({
UserPoolId: userPoolId,
Username: "abc",
})
.promise();

// login as the user
const initiateAuthResponse = await client
.initiateAuth({
AuthFlow: "USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: "abc",
PASSWORD: "def",
},
ClientId: upc.UserPoolClient?.ClientId as string,
})
.promise();

await client
.getUserAttributeVerificationCode({
AccessToken: initiateAuthResponse.AuthenticationResult
?.AccessToken as string,
AttributeName: "email",
})
.promise();

// get the user's code -- this is very nasty
const ds = await DataStoreFactory().create(TestContext, userPoolId, {});
const storedUser = (await ds.get(TestContext, ["Users", "abc"])) as User;

expect(storedUser.AttributeVerificationCode).toMatch(/^\d{4}$/);
});
})
);
134 changes: 134 additions & 0 deletions src/targets/getUserAttributeVerificationCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import jwt from "jsonwebtoken";
import * as uuid from "uuid";
import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockMessages } from "../__tests__/mockMessages";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { TestContext } from "../__tests__/testContext";
import { InvalidParameterError, UserNotFoundError } from "../errors";
import PrivateKey from "../keys/cognitoLocal.private.json";
import { Messages, UserPoolService } from "../services";
import { attribute, attributeValue } from "../services/userPoolService";
import {
GetUserAttributeVerificationCode,
GetUserAttributeVerificationCodeTarget,
} from "./getUserAttributeVerificationCode";
import * as TDB from "../__tests__/testDataBuilder";

const validToken = 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",
}
);

describe("GetUserAttributeVerificationCode target", () => {
let getUserAttributeVerificationCode: GetUserAttributeVerificationCodeTarget;
let mockUserPoolService: jest.Mocked<UserPoolService>;
let mockMessages: jest.Mocked<Messages>;

beforeEach(() => {
mockUserPoolService = newMockUserPoolService({
Id: "test",
AutoVerifiedAttributes: ["email"],
});
mockMessages = newMockMessages();
getUserAttributeVerificationCode = GetUserAttributeVerificationCode({
cognito: newMockCognitoService(mockUserPoolService),
messages: mockMessages,
otp: () => "1234",
});
});

it("throws if token isn't valid", async () => {
await expect(
getUserAttributeVerificationCode(TestContext, {
AccessToken: "blah",
AttributeName: "email",
})
).rejects.toBeInstanceOf(InvalidParameterError);
});

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

await expect(
getUserAttributeVerificationCode(TestContext, {
AccessToken: validToken,
AttributeName: "email",
})
).rejects.toEqual(new UserNotFoundError());
});

it("throws if the user doesn't have a valid way to contact them", async () => {
const user = TDB.user({
Attributes: [],
});

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await expect(
getUserAttributeVerificationCode(TestContext, {
ClientMetadata: {
client: "metadata",
},
AccessToken: validToken,
AttributeName: "email",
})
).rejects.toEqual(
new InvalidParameterError(
"User has no attribute matching desired auto verified attributes"
)
);
});

it("delivers a OTP code to the user", async () => {
const user = TDB.user({
Attributes: [attribute("email", "[email protected]")],
});

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await getUserAttributeVerificationCode(TestContext, {
ClientMetadata: {
client: "metadata",
},
AccessToken: validToken,
AttributeName: "email",
});

expect(mockMessages.deliver).toHaveBeenCalledWith(
TestContext,
"VerifyUserAttribute",
null,
"test",
user,
"1234",
{ client: "metadata" },
{
AttributeName: "email",
DeliveryMedium: "EMAIL",
Destination: attributeValue("email", user.Attributes),
}
);

expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(
TestContext,
expect.objectContaining({
AttributeVerificationCode: "1234",
})
);
});
});
93 changes: 93 additions & 0 deletions src/targets/getUserAttributeVerificationCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
GetUserAttributeVerificationCodeRequest,
GetUserAttributeVerificationCodeResponse,
} from "aws-sdk/clients/cognitoidentityserviceprovider";
import jwt from "jsonwebtoken";
import { Messages, Services, UserPoolService } from "../services";
import { InvalidParameterError, UserNotFoundError } from "../errors";
import { selectAppropriateDeliveryMethod } from "../services/messageDelivery/deliveryMethod";
import { Token } from "../services/tokenGenerator";
import { User } from "../services/userPoolService";
import { Context, Target } from "./router";

const sendAttributeVerificationCode = async (
ctx: Context,
userPool: UserPoolService,
user: User,
messages: Messages,
req: GetUserAttributeVerificationCodeRequest,
code: string
) => {
const deliveryDetails = selectAppropriateDeliveryMethod(
userPool.config.AutoVerifiedAttributes ?? [],
user
);
if (!deliveryDetails) {
// TODO: I don't know what the real error message should be for this
throw new InvalidParameterError(
"User has no attribute matching desired auto verified attributes"
);
}

await messages.deliver(
ctx,
"VerifyUserAttribute",
null,
userPool.config.Id,
user,
code,
req.ClientMetadata,
deliveryDetails
);
};

export type GetUserAttributeVerificationCodeTarget = Target<
GetUserAttributeVerificationCodeRequest,
GetUserAttributeVerificationCodeResponse
>;

type GetUserAttributeVerificationCodeServices = Pick<
Services,
"cognito" | "otp" | "messages"
>;

export const GetUserAttributeVerificationCode =
({
cognito,
otp,
messages,
}: GetUserAttributeVerificationCodeServices): GetUserAttributeVerificationCodeTarget =>
async (ctx, req) => {
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,
decodedToken.client_id
);
const user = await userPool.getUserByUsername(ctx, decodedToken.sub);
if (!user) {
throw new UserNotFoundError();
}

const code = otp();

await userPool.saveUser(ctx, {
...user,
AttributeVerificationCode: code,
});

await sendAttributeVerificationCode(
ctx,
userPool,
user,
messages,
req,
code
);

return {};
};
2 changes: 2 additions & 0 deletions src/targets/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DeleteUser } from "./deleteUser";
import { DescribeUserPoolClient } from "./describeUserPoolClient";
import { ForgotPassword } from "./forgotPassword";
import { ChangePassword } from "./changePassword";
import { GetUserAttributeVerificationCode } from "./getUserAttributeVerificationCode";
import { InitiateAuth } from "./initiateAuth";
import { ListGroups } from "./listGroups";
import { ListUserPools } from "./listUserPools";
Expand Down Expand Up @@ -45,6 +46,7 @@ export const Targets = {
DescribeUserPoolClient,
ForgotPassword,
GetUser,
GetUserAttributeVerificationCode,
InitiateAuth,
ListGroups,
ListUsers,
Expand Down

0 comments on commit 90726a4

Please sign in to comment.