Skip to content

Commit

Permalink
feat(lambda): preSignUp trigger support in signUp
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Nov 29, 2021
1 parent 96de301 commit af955a1
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 73 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ cognito-local how to connect to your local Lambda server:
| PostConfirmation | ConfirmForgotPassword ||
| PostConfirmation | ConfirmSignUp ||
| PreAuthentication | \* ||
| PreSignUp | \* ||
| PreSignUp | PreSignUp_AdminCreateUser ||
| PreSignUp | PreSignUp_ExternalProvider ||
| PreSignUp | PreSignUp_SignUp ||
| PreTokenGeneration | \* ||
| UserMigration | Authentication ||
| UserMigration | ForgotPassword ||
Expand Down Expand Up @@ -280,9 +282,11 @@ You can edit that `.cognito/config.json` and add any of the following settings:
| `LambdaClient.region` | `string` | `local` | |
| `TokenConfig.IssuerDomain` | `string` | `http://localhost:9229` | Issuer domain override |
| `TriggerFunctions` | `object` | `{}` | Trigger name to Function name mapping |
| `TriggerFunctions.CustomMessage` | `string` | | CustomMessage lambda name |
| `TriggerFunctions.PostConfirmation` | `string` | | PostConfirmation lambda name |
| `TriggerFunctions.UserMigration` | `string` | | UserMigration lambda name |
| `TriggerFunctions.CustomMessage` | `string` | | CustomMessage local lambda function name |
| `TriggerFunctions.PostAuthentication` | `string` | | PostAuthentication local lambda function name |
| `TriggerFunctions.PostConfirmation` | `string` | | PostConfirmation local lambda function name |
| `TriggerFunctions.PreSignUp` | `string` | | PostConfirmation local lambda function name |
| `TriggerFunctions.UserMigration` | `string` | | PreSignUp local lambda function name |
| `UserPoolDefaults` | `object` | | Default behaviour to use for the User Pool |
| `UserPoolDefaults.Id` | `string` | `local` | Default User Pool Id |
| `UserPoolDefaults.MfaConfiguration` | `string` | | MFA type |
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/mockTriggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const newMockTriggers = (): jest.Mocked<Triggers> => ({
enabled: jest.fn(),
postAuthentication: jest.fn(),
postConfirmation: jest.fn(),
preSignUp: jest.fn(),
userMigration: jest.fn(),
});
15 changes: 15 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ export class UnexpectedLambdaExceptionError extends CognitoError {
}
}

export class UserLambdaValidationError extends CognitoError {
public constructor(message?: string) {
super(
"UserLambdaValidationException",
message ?? "Lambda threw an exception"
);
}
}

export class InvalidLambdaResponseError extends CognitoError {
public constructor() {
super("InvalidLambdaResponseException", "Invalid Lambda response");
}
}

export class InvalidParameterError extends CognitoError {
public constructor(message = "Invalid parameter") {
super("InvalidParameterException", message);
Expand Down
132 changes: 129 additions & 3 deletions src/services/lambda.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { MockLogger } from "../__tests__/mockLogger";
import {
InvalidLambdaResponseError,
UserLambdaValidationError,
} from "../errors";
import { LambdaService } from "./lambda";
import * as AWS from "aws-sdk";
import { version } from "aws-sdk/package.json";
Expand Down Expand Up @@ -50,11 +54,11 @@ describe("Lambda function invoker", () => {
).rejects.toEqual(new Error("UserMigration trigger not configured"));
});

describe("when lambda successful", () => {
describe("when lambda is successful", () => {
it("returns string payload as json", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "response": "value" }',
Payload: '{ "response": { "ok": "value" } }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
Expand All @@ -78,7 +82,69 @@ describe("Lambda function invoker", () => {
validationData: undefined,
});

expect(result).toEqual("value");
expect(result).toEqual({ ok: "value" });
});

it("throws if an invalid payload is returned", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "respo...',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = new LambdaService(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient,
MockLogger
);

await expect(
lambda.invoke("UserMigration", {
clientId: "clientId",
clientMetadata: undefined,
password: "password",
triggerSource: "UserMigration_Authentication",
userAttributes: {},
username: "username",
userPoolId: "userPoolId",
validationData: undefined,
})
).rejects.toBeInstanceOf(InvalidLambdaResponseError);
});

it("throws if the function returns an error", async () => {
const response = Promise.resolve({
StatusCode: 500,
FunctionError: "Something bad happened",
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = new LambdaService(
{
UserMigration: "MyLambdaName",
},
mockLambdaClient,
MockLogger
);

await expect(
lambda.invoke("UserMigration", {
clientId: "clientId",
clientMetadata: undefined,
password: "password",
triggerSource: "UserMigration_Authentication",
userAttributes: {},
username: "username",
userPoolId: "userPoolId",
validationData: undefined,
})
).rejects.toEqual(
new UserLambdaValidationError("Something bad happened")
);
});

it("returns Buffer payload as json", async () => {
Expand Down Expand Up @@ -112,6 +178,66 @@ describe("Lambda function invoker", () => {
});
});

describe.each([
"PreSignUp_AdminCreateUser",
"PreSignUp_ExternalProvider",
"PreSignUp_SignUp",
] as const)("%s", (source) => {
it("invokes the lambda", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "some": "json" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = new LambdaService(
{
PreSignUp: "MyLambdaName",
},
mockLambdaClient,
MockLogger
);

await lambda.invoke("PreSignUp", {
clientId: "clientId",
clientMetadata: {
client: "metadata",
},
triggerSource: source,
username: "username",
userPoolId: "userPoolId",
userAttributes: {},
validationData: {
validation: "data",
},
});

expect(mockLambdaClient.invoke).toHaveBeenCalledWith({
FunctionName: "MyLambdaName",
InvocationType: "RequestResponse",
Payload: expect.jsonMatching({
version: 0,
callerContext: { awsSdkVersion: version, clientId: "clientId" },
region: "local",
userPoolId: "userPoolId",
triggerSource: source,
request: {
clientMetadata: {
client: "metadata",
},
userAttributes: {},
validationData: {
validation: "data",
},
},
response: {},
userName: "username",
}),
});
});
});

describe("UserMigration_Authentication", () => {
it("invokes the lambda", async () => {
const response = Promise.resolve({
Expand Down
43 changes: 37 additions & 6 deletions src/services/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { CognitoUserPoolEvent } from "aws-lambda";
import type { Lambda as LambdaClient } from "aws-sdk";
import { InvocationResponse } from "aws-sdk/clients/lambda";
import { UnexpectedLambdaExceptionError } from "../errors";
import {
InvalidLambdaResponseError,
UnexpectedLambdaExceptionError,
UserLambdaValidationError,
} from "../errors";
import { version as awsSdkVersion } from "aws-sdk/package.json";
import { Logger } from "../log";

Expand Down Expand Up @@ -33,6 +37,15 @@ interface UserMigrationEvent extends EventCommonParameters {
validationData: Record<string, string> | undefined;
}

interface PreSignUpEvent extends EventCommonParameters {
clientMetadata: Record<string, string> | undefined;
triggerSource:
| "PreSignUp_AdminCreateUser"
| "PreSignUp_ExternalProvider"
| "PreSignUp_SignUp";
validationData: Record<string, string> | undefined;
}

interface PostAuthenticationEvent extends EventCommonParameters {
clientMetadata: Record<string, string> | undefined;
triggerSource: "PostAuthentication_Authentication";
Expand All @@ -49,9 +62,10 @@ export type CognitoUserPoolResponse = CognitoUserPoolEvent["response"];

export interface FunctionConfig {
CustomMessage?: string;
UserMigration?: string;
PostAuthentication?: string;
PostConfirmation?: string;
PreSignUp?: string;
UserMigration?: string;
}

export interface Lambda {
Expand All @@ -64,6 +78,10 @@ export interface Lambda {
lambda: "UserMigration",
event: UserMigrationEvent
): Promise<CognitoUserPoolResponse>;
invoke(
lambda: "PreSignUp",
event: PreSignUpEvent
): Promise<CognitoUserPoolResponse>;
invoke(
lambda: "PostAuthentication",
event: PostAuthenticationEvent
Expand Down Expand Up @@ -97,9 +115,10 @@ export class LambdaService implements Lambda {
trigger: keyof FunctionConfig,
event:
| CustomMessageEvent
| UserMigrationEvent
| PostAuthenticationEvent
| PostConfirmationEvent
| PreSignUpEvent
| UserMigrationEvent
) {
const functionName = this.config[trigger];
if (!functionName) {
Expand Down Expand Up @@ -129,6 +148,13 @@ export class LambdaService implements Lambda {
lambdaEvent.request.clientMetadata = event.clientMetadata;
break;
}
case "PreSignUp_AdminCreateUser":
case "PreSignUp_ExternalProvider":
case "PreSignUp_SignUp": {
lambdaEvent.request.clientMetadata = event.clientMetadata;
lambdaEvent.request.validationData = event.validationData;
break;
}
case "UserMigration_Authentication": {
lambdaEvent.request.clientMetadata = event.clientMetadata;
lambdaEvent.request.password = event.password;
Expand Down Expand Up @@ -172,12 +198,17 @@ export class LambdaService implements Lambda {
`Lambda completed with StatusCode=${result.StatusCode} and FunctionError=${result.FunctionError}`
);
if (result.StatusCode === 200) {
const parsedPayload = JSON.parse(result.Payload as string);
try {
const parsedPayload = JSON.parse(result.Payload as string);

return parsedPayload.response as CognitoUserPoolResponse;
return parsedPayload.response as CognitoUserPoolResponse;
} catch (err) {
this.logger.error(err);
throw new InvalidLambdaResponseError();
}
} else {
this.logger.error(result.FunctionError);
throw new UnexpectedLambdaExceptionError();
throw new UserLambdaValidationError(result.FunctionError);
}
}
}
Loading

0 comments on commit af955a1

Please sign in to comment.