Skip to content

Commit

Permalink
feat: Add support for custom Lambda function email senders in Auth co…
Browse files Browse the repository at this point in the history
…nstruct (#2087)

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* chore: add changeset

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: clearing the .amplify/generated/env/ before synthesis

* fix: Clear generated env directory before shim generation

* fix: Clear generated env directory before shim generation

* fix: Clear generated env directory before shim generation

* fix: Clear generated env directory before shim generation

* chore: add changeset

* fix: Clear generated env directory before shim generation

* fix: Clear generated env directory before shim generation

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* feat: adding custom lambda function trigger for email

* cleanup

* chore: removed unwanted changesets

* chore: preparing for merge

* chore: preparing for merge

* chore: preparing for merge

* feat: added a functionality to translate auth-props for custom fucntion email

* chore: added changeset

* chore: added new API

* feat: added a functionality to translate auth-props for custom fucntion email

* feat: added a test case to cover custom function in backend-auth

* feat: added a test case to cover custom function in backend-auth

* feat: added a test case to cover custom function in backend-auth

* feat: added a test case to cover custom function in backend-auth

* feat: added a test case to cover custom function in backend-auth

* feat: narrowed down the permission by updating the conditions

* Refactored the code

* Refactored the code

* added a test case for checking lambdaTrigger={} empty condition

* added a test case for checking lambdaTrigger={} empty condition

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* Merge Branch

* fixed the code to use addTrigger instead of manually setting up permissions

* added KMS Key for customEmailSender

* changed KMS key to not read-only

* changed the test case to include lambdaArn

* add a test case validation for KMS key

* fixed the code to use addTrigger instead of manually setting up permissions

* added KMS Key for customEmailSender

* changed KMS key to not read-only

* changed the test case to include lambdaArn

* add a test case validation for KMS key

* detect transform errors with multiple errors (#2102)

* detect transform errors with multiple errors

* new method of getting multiple transform errors

* Add minify option to defineFunction (#2093)

* Add minify option to defineFunction

* Add unit tests and e2e tests when set minify option to false

* Add changeset

* Update API.md

* add bundling options

* Update .changeset/pink-rockets-dance.md

* use optional chaining

* include funcNoMinify into function.ts

---------

Co-authored-by: Kamil Sobol <[email protected]>

* upgrade constructs (#2103)

* Remove deprecated messages field from event (#2106)

* detect generic CFN stack creation errors (#2108)

* Fix cdk tests when new dependencies are shipped to npm. (#2107)

* Fix cdk tests when new dependencies are shipped to npm.

* try this

* try this

* try this

* try this

* API changes

* Update API changes

* Added kmsKeyArn for custom user KMS keys

* chore: added changesets and updated API's

* chore: added changesets

* Added integration tests for customEmailSender

* updated the API files to reflect master

* feat: added customSenderEmail with types and added exceptions to eslint dict

* chore: Updated API

* chore: Updated API

* chore: updated API

* Delete packages/ai-constructs/API.md

* chore: Updated API

* chore: delete unused file

* chore: update changeset

* chore: update changeset

* chore: Updated API and changeSets

* chore: Updated the API from main

* API updates to resolve conflicting naming

* Updated the types in backend-auth

* chore: Updated changesets

* Added custom Email handler function and refactored the types of auth-construct

* chore: updated API

---------

Co-authored-by: Roshane Pascual <[email protected]>
Co-authored-by: MURAKAMI Masahiko <[email protected]>
Co-authored-by: Kamil Sobol <[email protected]>
Co-authored-by: Kamil Sobol <[email protected]>
  • Loading branch information
5 people authored Oct 31, 2024
1 parent 4418a51 commit 11d62fe
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 26 deletions.
8 changes: 8 additions & 0 deletions .changeset/curvy-pans-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@aws-amplify/auth-construct': minor
'@aws-amplify/backend-auth': minor
'@aws-amplify/backend': minor
'@aws-amplify/integration-tests': minor
---

Add support for custom Lambda function email senders in Auth construct
3 changes: 3 additions & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"datasync",
"debounce",
"declarator",
"decrypt",
"deployer",
"deprecations",
"deprecator",
Expand Down Expand Up @@ -146,6 +147,7 @@
"sigint",
"signout",
"signup",
"SKey",
"sms",
"stderr",
"stdin",
Expand All @@ -159,6 +161,7 @@
"synthing",
"testname",
"testnamebucket",
"testuser",
"timestamps",
"tmpdir",
"todos",
Expand Down
9 changes: 8 additions & 1 deletion packages/auth-construct/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AuthResources } from '@aws-amplify/plugin-types';
import { aws_cognito } from 'aws-cdk-lib';
import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types';
import { Construct } from 'constructs';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { NumberAttributeConstraints } from 'aws-cdk-lib/aws-cognito';
import { ResourceProvider } from '@aws-amplify/plugin-types';
import { SecretValue } from 'aws-cdk-lib';
Expand Down Expand Up @@ -47,7 +48,7 @@ export type AuthProps = {
externalProviders?: ExternalProviderOptions;
};
senders?: {
email: Pick<UserPoolSESOptions, 'fromEmail' | 'fromName' | 'replyTo'>;
email: Pick<UserPoolSESOptions, 'fromEmail' | 'fromName' | 'replyTo'> | CustomEmailSender;
};
userAttributes?: UserAttributes;
multifactor?: MFA;
Expand Down Expand Up @@ -84,6 +85,12 @@ export type CustomAttributeString = CustomAttributeBase & StringAttributeConstra
dataType: 'String';
};

// @public
export type CustomEmailSender = {
handler: IFunction;
kmsKeyArn?: string;
};

// @public
export type EmailLogin = true | EmailLoginSettings;

Expand Down
72 changes: 59 additions & 13 deletions packages/auth-construct/src/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
UserPoolIdentityProviderOidc,
UserPoolIdentityProviderSaml,
UserPoolIdentityProviderSamlMetadataType,
UserPoolOperation,
UserPoolProps,
} from 'aws-cdk-lib/aws-cognito';
import { FederatedPrincipal, Role } from 'aws-cdk-lib/aws-iam';
Expand All @@ -51,6 +52,7 @@ import {
StackMetadataBackendOutputStorageStrategy,
} from '@aws-amplify/backend-output-storage';
import * as path from 'path';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';

type DefaultRoles = { auth: Role; unAuth: Role };
type IdentityProviderSetupResult = {
Expand Down Expand Up @@ -130,6 +132,11 @@ export class AmplifyAuth
role: Role;
};
} = {};
/**
* The KMS key used for encrypting custom email sender data.
* This is only set when using a custom email sender.
*/
private customEmailSenderKMSkey: IKey | undefined;

/**
* Create a new Auth construct with AuthProps.
Expand All @@ -141,24 +148,39 @@ export class AmplifyAuth
props: AuthProps = DEFAULTS.IF_NO_PROPS_PROVIDED
) {
super(scope, id);

this.name = props.name ?? '';
this.domainPrefix = props.loginWith.externalProviders?.domainPrefix;

// UserPool
this.computedUserPoolProps = this.getUserPoolProps(props);

this.userPool = new cognito.UserPool(
this,
`${this.name}UserPool`,
this.computedUserPoolProps
);
/**
* Configure custom email sender for Cognito User Pool
* Grant necessary permissions for Lambda function to decrypt emails
* and allow Cognito to invoke the Lambda function
*/
if (
props.senders?.email &&
'handler' in props.senders.email &&
this.customEmailSenderKMSkey
) {
this.customEmailSenderKMSkey.grantDecrypt(props.senders.email.handler);
this.customEmailSenderKMSkey.grantEncrypt(props.senders.email.handler);
this.userPool.addTrigger(
UserPoolOperation.of('customEmailSender'),
props.senders.email.handler
);
}

// UserPool - External Providers (Oauth, SAML, OIDC) and User Pool Domain
this.providerSetupResult = this.setupExternalProviders(
this.userPool,
props.loginWith
);

// UserPool Client
const userPoolClient = new cognito.UserPoolClient(
this,
Expand Down Expand Up @@ -478,7 +500,30 @@ export class AmplifyAuth
},
{ standardAttributes: {}, customAttributes: {} }
);

/**
* Handle KMS key for custom email sender
* If a custom email sender is provided, we either use the provided KMS key ARN
* or create a new KMS key if one is not provided.
*/
if (props.senders?.email && 'handler' in props.senders.email) {
if (props.senders.email.kmsKeyArn) {
// Use the provided KMS key ARN
this.customEmailSenderKMSkey = Key.fromKeyArn(
this,
`${this.name}CustomSenderKey`,
props.senders.email.kmsKeyArn
);
} else {
// Create a new KMS key if not provided
this.customEmailSenderKMSkey = new Key(
props.senders.email.handler.stack,
`${this.name}CustomSenderKey`,
{
enableKeyRotation: true,
}
);
}
}
const userPoolProps: UserPoolProps = {
signInCaseSensitive: DEFAULTS.SIGN_IN_CASE_SENSITIVE,
signInAliases: {
Expand All @@ -503,15 +548,15 @@ export class AmplifyAuth
customAttributes: {
...customAttributes,
},
email: props.senders
? cognito.UserPoolEmail.withSES({
fromEmail: props.senders.email.fromEmail,
fromName: props.senders.email.fromName,
replyTo: props.senders.email.replyTo,
sesRegion: Stack.of(this).region,
})
: undefined,

email:
props.senders && 'fromEmail' in props.senders.email
? cognito.UserPoolEmail.withSES({
fromEmail: props.senders.email.fromEmail,
fromName: props.senders.email.fromName,
replyTo: props.senders.email.replyTo,
sesRegion: Stack.of(this).region,
})
: undefined,
selfSignUpEnabled: DEFAULTS.ALLOW_SELF_SIGN_UP,
mfa: mfaMode,
mfaMessage: this.getMFAMessage(props.multifactor),
Expand All @@ -528,6 +573,7 @@ export class AmplifyAuth
props.loginWith.email?.userInvitation
)
: undefined,
customSenderKmsKey: this.customEmailSenderKMSkey,
};
return userPoolProps;
};
Expand Down
1 change: 1 addition & 0 deletions packages/auth-construct/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {
CustomAttributeBoolean,
CustomAttributeDateTime,
CustomAttributeBase,
CustomEmailSender,
} from './types.js';
export { AmplifyAuth } from './construct.js';
export { triggerEvents } from './trigger_events.js';
17 changes: 15 additions & 2 deletions packages/auth-construct/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UserPoolIdentityProviderSamlMetadata,
UserPoolSESOptions,
} from 'aws-cdk-lib/aws-cognito';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
export type VerificationEmailWithLink = {
/**
* The type of verification. Must be one of "CODE" or "LINK".
Expand Down Expand Up @@ -380,6 +381,14 @@ export type CustomAttribute =
export type UserAttributes = StandardAttributes &
Record<`custom:${string}`, CustomAttribute>;

/**
* CustomEmailSender type for configuring a custom Lambda function for email sending
*/
export type CustomEmailSender = {
handler: IFunction;
kmsKeyArn?: string;
};

/**
* Input props for the AmplifyAuth construct
*/
Expand Down Expand Up @@ -417,11 +426,15 @@ export type AuthProps = {
*/
senders?: {
/**
* Configure Cognito to send emails from SES
* Configure Cognito to send emails from SES or a custom message trigger
* SES configurations enable the use of customized email sender addresses and names
* Custom message triggers enable the use of third-party email providers when sending email notifications to users
* @see https://docs.amplify.aws/react/build-a-backend/auth/moving-to-production/#email
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html
*/
email: Pick<UserPoolSESOptions, 'fromEmail' | 'fromName' | 'replyTo'>;
email:
| Pick<UserPoolSESOptions, 'fromEmail' | 'fromName' | 'replyTo'>
| CustomEmailSender;
};
/**
* The set of attributes that are required for every user in the user pool. Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html
Expand Down
14 changes: 13 additions & 1 deletion packages/backend-auth/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```ts

import { AmazonProviderProps } from '@aws-amplify/auth-construct';
import { AmplifyFunction } from '@aws-amplify/plugin-types';
import { AppleProviderProps } from '@aws-amplify/auth-construct';
import { AuthProps } from '@aws-amplify/auth-construct';
import { AuthResources } from '@aws-amplify/plugin-types';
Expand All @@ -16,12 +17,14 @@ import { ExternalProviderOptions } from '@aws-amplify/auth-construct';
import { FacebookProviderProps } from '@aws-amplify/auth-construct';
import { FunctionResources } from '@aws-amplify/plugin-types';
import { GoogleProviderProps } from '@aws-amplify/auth-construct';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { OidcProviderProps } from '@aws-amplify/auth-construct';
import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types';
import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types';
import { ResourceProvider } from '@aws-amplify/plugin-types';
import { StackProvider } from '@aws-amplify/plugin-types';
import { TriggerEvent } from '@aws-amplify/auth-construct';
import { UserPoolSESOptions } from 'aws-cdk-lib/aws-cognito';

// @public
export type ActionIam = 'addUserToGroup' | 'createGroup' | 'createUser' | 'deleteGroup' | 'deleteUser' | 'deleteUserAttributes' | 'disableUser' | 'enableUser' | 'forgetDevice' | 'getDevice' | 'getGroup' | 'getUser' | 'listUsers' | 'listUsersInGroup' | 'listGroups' | 'listDevices' | 'listGroupsForUser' | 'removeUserFromGroup' | 'resetUserPassword' | 'setUserMfaPreference' | 'setUserPassword' | 'setUserSettings' | 'updateDeviceStatus' | 'updateGroup' | 'updateUserAttributes';
Expand All @@ -36,10 +39,13 @@ export type AmazonProviderFactoryProps = Omit<AmazonProviderProps, 'clientId' |
};

// @public (undocumented)
export type AmplifyAuthProps = Expand<Omit<AuthProps, 'outputStorageStrategy' | 'loginWith'> & {
export type AmplifyAuthProps = Expand<Omit<AuthProps, 'outputStorageStrategy' | 'loginWith' | 'senders'> & {
loginWith: Expand<AuthLoginWithFactoryProps>;
triggers?: Partial<Record<TriggerEvent, ConstructFactory<ResourceProvider<FunctionResources>>>>;
access?: AuthAccessGenerator;
senders?: {
email: Pick<UserPoolSESOptions, 'fromEmail' | 'fromName' | 'replyTo'> | CustomEmailSender;
};
}>;

// @public
Expand Down Expand Up @@ -80,6 +86,12 @@ export type AuthLoginWithFactoryProps = Omit<AuthProps['loginWith'], 'externalPr
// @public (undocumented)
export type BackendAuth = ResourceProvider<AuthResources> & ResourceAccessAcceptorFactory<AuthRoleName | string> & StackProvider;

// @public
export type CustomEmailSender = {
handler: ConstructFactory<AmplifyFunction> | IFunction;
kmsKeyArn?: string;
};

// @public
export const defineAuth: (props: AmplifyAuthProps) => ConstructFactory<BackendAuth>;

Expand Down
Loading

0 comments on commit 11d62fe

Please sign in to comment.