packages/blueprints/gen-ai-chatbot/static-assets/chatbot-genai-cdk/lib/constructs/auth.ts (256 lines of code) (raw):

import { CfnOutput, Duration, Stack, CustomResource } from "aws-cdk-lib"; import { ProviderAttribute, UserPool, UserPoolClient, UserPoolOperation, UserPoolIdentityProviderGoogle, CfnUserPoolGroup, UserPoolIdentityProviderOidc, } from "aws-cdk-lib/aws-cognito"; import * as iam from "aws-cdk-lib/aws-iam"; import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import { Runtime, Code, SingletonFunction } from "aws-cdk-lib/aws-lambda"; import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"; import { Construct } from "constructs"; import * as path from "path"; import * as fs from "fs"; import { Idp, TIdentityProvider } from "../utils/identity-provider"; export interface AuthProps { readonly origin: string; readonly userPoolDomainPrefixKey: string; readonly idp: Idp; readonly allowedSignUpEmailDomains: string[]; readonly autoJoinUserGroups: string[]; readonly selfSignUpEnabled: boolean; } export class Auth extends Construct { readonly userPool: UserPool; readonly client: UserPoolClient; constructor(scope: Construct, id: string, props: AuthProps) { super(scope, id); const userPool = new UserPool(this, "UserPool", { passwordPolicy: { requireUppercase: true, requireSymbols: true, requireDigits: true, minLength: 8, }, // Disable id selfSignUpEnabled is given as false or if selfSignUpEnabled is true and idp is provided selfSignUpEnabled: props.selfSignUpEnabled && !props.idp.isExist(), signInAliases: { username: false, email: true, }, }); const clientProps = (() => { const defaultProps = { idTokenValidity: Duration.days(1), authFlows: { userPassword: true, userSrp: true, }, }; if (!props.idp.isExist()) return defaultProps; return { ...defaultProps, oAuth: { callbackUrls: [props.origin], logoutUrls: [props.origin], }, supportedIdentityProviders: [ ...props.idp.getSupportedIndetityProviders(), ], }; })(); const client = userPool.addClient(`Client`, clientProps); const configureProvider = ( provider: TIdentityProvider, userPool: UserPool, client: UserPoolClient ) => { const secret = secretsmanager.Secret.fromSecretNameV2( this, "Secret", provider.secretName ); const clientId = secret .secretValueFromJson("clientId") .unsafeUnwrap() .toString(); const clientSecret = secret.secretValueFromJson("clientSecret"); switch (provider.service) { // Currently only Google and custom OIDC are supported case "google": { const googleProvider = new UserPoolIdentityProviderGoogle( this, "GoogleProvider", { userPool, clientId, clientSecretValue: clientSecret, scopes: ["openid", "email"], attributeMapping: { email: ProviderAttribute.GOOGLE_EMAIL, }, } ); client.node.addDependency(googleProvider); break; } case "oidc": { const issuerUrl = secret .secretValueFromJson("issuerUrl") .unsafeUnwrap() .toString(); const oidcProvider = new UserPoolIdentityProviderOidc( this, "OidcProvider", { name: provider.serviceName, userPool, clientId, clientSecret: clientSecret.unsafeUnwrap().toString(), issuerUrl, attributeMapping: { // This is an example of mapping the email attribute. // Replace this with the actual idp attribute key. email: ProviderAttribute.other("EMAIL"), }, scopes: ["openid", "email"], } ); client.node.addDependency(oidcProvider); break; } } }; if (props.idp.isExist()) { for (const provider of props.idp.getProviders()) { configureProvider(provider, userPool, client); } userPool.addDomain("UserPool", { cognitoDomain: { domainPrefix: props.userPoolDomainPrefixKey, }, }); } if (props.allowedSignUpEmailDomains.length >= 1) { const checkEmailDomainFunction = new PythonFunction( this, "CheckEmailDomain", { runtime: Runtime.PYTHON_3_12, index: "check_email_domain.py", entry: path.join( __dirname, "../../../backend/auth/check_email_domain" ), timeout: Duration.minutes(1), environment: { ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR: JSON.stringify( props.allowedSignUpEmailDomains ), }, } ); userPool.addTrigger( UserPoolOperation.PRE_SIGN_UP, checkEmailDomainFunction ); } const adminGroup = new CfnUserPoolGroup(this, "AdminGroup", { groupName: "Admin", userPoolId: userPool.userPoolId, }); const creatingBotAllowedGroup = new CfnUserPoolGroup( this, "CreatingBotAllowedGroup", { groupName: "CreatingBotAllowed", userPoolId: userPool.userPoolId, } ); const publishAllowedGroup = new CfnUserPoolGroup( this, "PublishAllowedGroup", { groupName: "PublishAllowed", userPoolId: userPool.userPoolId, } ); if (props.autoJoinUserGroups.length >= 1) { /** * Create a Cognito trigger to add a new user to the group specified with `autoJoinUserGroups`. * * Registering a Lambda function that uses a user pool as a trigger of the user pool itself * results circular reference, so CloudFormation cannot do this when creating a user pool. * Additionally, CloudFormation does not provide the functionality to add triggers to existing user pools. * Therefore, use a custom resource implementing that functionality. */ const addUserToGroupsFunction = new PythonFunction( this, "AddUserToGroups", { runtime: Runtime.PYTHON_3_12, index: "add_user_to_groups.py", entry: path.join( __dirname, "../../../backend/auth/add_user_to_groups" ), timeout: Duration.minutes(1), environment: { USER_POOL_ID: userPool.userPoolId, AUTO_JOIN_USER_GROUPS: JSON.stringify(props.autoJoinUserGroups), }, } ); addUserToGroupsFunction.addPermission("CognitoTrigger", { principal: new iam.ServicePrincipal("cognito-idp.amazonaws.com"), sourceArn: userPool.userPoolArn, scope: userPool, }); userPool.grant( addUserToGroupsFunction, "cognito-idp:AdminAddUserToGroup" ); const cognitoTriggerRegistrationFunction = new SingletonFunction( this, "CognitoTriggerRegistrationFunction", { uuid: "a84c6122-180e-48fc-afaf-f4d65da2b370", lambdaPurpose: "CognitoTriggerRegistrationFunction", code: Code.fromInline( fs.readFileSync( path.join( __dirname, "../../custom-resources/cognito-trigger/index.py" ), "utf8" ) ), handler: "index.handler", runtime: Runtime.PYTHON_3_12, environment: { USER_POOL_ID: userPool.userPoolId, }, timeout: Duration.minutes(1), } ); userPool.grant( cognitoTriggerRegistrationFunction, "cognito-idp:UpdateUserPool", "cognito-idp:DescribeUserPool" ); const cognitoTrigger = new CustomResource(this, "CognitoTrigger", { serviceToken: cognitoTriggerRegistrationFunction.functionArn, resourceType: "Custom::CognitoTrigger", properties: { Triggers: { PostConfirmation: addUserToGroupsFunction.functionArn, PostAuthentication: addUserToGroupsFunction.functionArn, }, }, }); cognitoTrigger.node.addDependency(addUserToGroupsFunction); } this.client = client; this.userPool = userPool; new CfnOutput(this, "UserPoolId", { value: userPool.userPoolId }); new CfnOutput(this, "UserPoolClientId", { value: client.userPoolClientId }); if (props.idp.isExist()) new CfnOutput(this, "ApprovedRedirectURI", { value: `https://${props.userPoolDomainPrefixKey}.auth.${ Stack.of(userPool).region }.amazoncognito.com/oauth2/idpresponse`, }); } }