AWSTemplateFormatVersion: 2010-09-09 Description: > This template updates a cognito user pool client with a domain and app configuration Parameters: CloudFrontUrl: Type: String Description: Url of the base CF distro web app used by callbacks within the user pool Default: '' WebAppUrl: Type: String Description: Url of the target web app (hosting page) )used by callbacks within the user pool Default: '' WebAppPath: Type: String Description: 'Path to reach top level page in within the WebAppUrl. ie: /index.html' Default: '/index.html' CodeBuildProjectName: Type: String Description: 'CodeBuildProjectName to update environment configuration' CognitoUserPool: Type: String Description: Cognito UserPool Id CognitoUserPoolClient: Type: String Description: Cognito UserPool Client Id Timestamp: Type: Number Description: > This is a required parameter. Resources: CognitoUserPoolDomain: Type: Custom::CognitouserPoolDomain Properties: ServiceToken: !GetAtt CognitoUserPoolDomainFunction.Arn CognitoUserPoolDomainFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt CognitoUserPoolDomainExecutionRole.Arn Runtime: python3.8 Timeout: 300 TracingConfig: Mode: Active Code: ZipFile: !Sub | from __future__ import print_function import json import boto3 import cfnresponse import time def handler(event, context): print(json.dumps(event)) stackname = '${CleanStackName.CleanStackNameValue}'[0:50] id = stackname + '${AWS::AccountId}' id = id.lower().replace("cognito","") print('final id: ' + id) if (event["RequestType"] == "Delete"): try: deleteDomain(id) except Exception as e: print("Exception thrown: %s" % str(e)) pass elif (event["RequestType"] == "Create"): try: name = createDomain(id) print('name: ' + name) fullname = name + '.auth.' + '${AWS::Region}' + '.amazoncognito.com' print('fullname: ' + fullname) updateCodeBuildEnvironment('${CodeBuildProjectName}', fullname) except Exception as e: print("Exception thrown: %s" % str(e)) pass else: print("RequestType %s, nothing to do" % event["RequestType"]) time.sleep(30) # pause for CloudWatch logs print('Done') responseData={"domainid":id} try: cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, id) except Exception as e: print("Exception thrown in cfnresponse: %s" % str(e)) pass def deleteDomain(stackName): normalized = stackName.lower() print("Deleting domain %s" % normalized) client = boto3.client('cognito-idp') response = client.delete_user_pool_domain( Domain=normalized, UserPoolId='${CognitoUserPool}' ) return response def createDomain(stackName): normalized = stackName.lower() print("Creating domain %s" % normalized) client = boto3.client('cognito-idp') response = client.create_user_pool_domain( Domain=normalized, UserPoolId='${CognitoUserPool}' ) return normalized def updateCodeBuildEnvironment(projectname, domainname): print("Updating codebuild project %s" % projectname) client = boto3.client('codebuild') data = client.batch_get_projects( names=[ projectname ] ) projects = data.get('projects') project = projects[0] environment = project.get('environment') variables = environment.get('environmentVariables') updated = False for element in variables : if element.get('name') == 'APP_DOMAIN_NAME': element.update({'value': domainname}) updated = True if not updated: item = { 'name': 'APP_DOMAIN_NAME', 'value': domainname, 'type': 'PLAINTEXT' } variables.append(item) response = client.update_project( name=projectname, environment=environment ) response = client.start_build( projectName=projectname ) return response CognitoUserPoolDomainExecutionRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Principal: Service: - lambda.amazonaws.com Effect: Allow Action: - sts:AssumeRole Policies: - PolicyName: LogsForLambda PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*:*" - PolicyName: CognitoAuth PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-sync:* - cognito-identity:* - cognito-idp:* Resource: - !Sub "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${CognitoUserPool}" - PolicyName: CodeBuildUpdate PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - codebuild:BatchGetProjects - codebuild:UpdateProject - codebuild:StartBuild Resource: - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProjectName}" - PolicyName: XRay PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: - "*" - PolicyName: AllowVPCSupport PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ec2:DescribeNetworkInterfaces - ec2:CreateNetworkInterface - ec2:DeleteNetworkInterface Resource: "*" CognitoUserPoolUpdates: Type: Custom::CognitoUserPoolUpdates Properties: ServiceToken: !GetAtt CognitoUserPoolUpdatesFunction.Arn CloudFrontUrl: !Ref CloudFrontUrl WebAppUrl: !Ref WebAppUrl WebAppPath: !Ref WebAppPath CodeBuildProjectName: !Ref CodeBuildProjectName CognitoUserPool: !Ref CognitoUserPool CognitoUserPoolClient: !Ref CognitoUserPoolClient Timestamp: !Ref Timestamp CognitoUserPoolUpdatesFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt CognitoUserPoolDomainExecutionRole.Arn Runtime: python3.8 Timeout: 300 Environment: Variables: TIMESTAMP: !Ref Timestamp TracingConfig: Mode: Active Code: ZipFile: !Sub | from __future__ import print_function import json import boto3 import cfnresponse import time def handler(event, context): print(json.dumps(event)) if (event["RequestType"] == "Create" or event["RequestType"] == "Update"): try: updatePool("${CleanStackName.CleanStackNameValue}") except Exception as e: print("Exception thrown: %s" % str(e)) pass else: print("RequestType %s, nothing to do" % event["RequestType"]) time.sleep(30) # pause for CloudWatch logs print('Done') responseData={"Data":"OK"} try: cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: print("Exception thrown in cfnresponse: %s" % str(e)) pass def updatePool(stackName): normalized = stackName.lower() print("Updating Pool domain %s" % normalized) callbackURLs=[ '${CloudFrontUrl}/index.html?loggedin=yes', '${CloudFrontUrl}/parent.html?loggedin=yes' ] logoutURLs=[ '${CloudFrontUrl}/index.html?loggedout=yes', '${CloudFrontUrl}/parent.html?loggedout=yes' ] # Add CallBack and Logout URLs for specified WebApp, if specified # Allow multiple paths under same webAppUrl - comma separated # If either WebAppUrl or WebAppPath are upper or mixed case, add a # lowercase variant to help avoid redirect failures due to case mismatch webAppUrl='${WebAppUrl}' webAppPaths = [x.strip() for x in '${WebAppPath}'.split(',')] if webAppUrl: for webAppPath in webAppPaths: fullUrl=f'{webAppUrl}{webAppPath}' callbackURLs.insert(0,'%s?loggedin=yes' % fullUrl) logoutURLs.insert(0,'%s?loggedout=yes' % fullUrl) if (fullUrl != fullUrl.lower()): callbackURLs.insert(0,'%s?loggedin=yes' % fullUrl.lower()) logoutURLs.insert(0,'%s?loggedout=yes' % fullUrl.lower()) client = boto3.client('cognito-idp') currentClientConfig = client.describe_user_pool_client( UserPoolId='${CognitoUserPool}', ClientId='${CognitoUserPoolClient}' ) supportedIDProviders = currentClientConfig.get('UserPoolClient').get('SupportedIdentityProviders') if not supportedIDProviders: supportedIDProviders = list() if len(supportedIDProviders) == 0: supportedIDProviders.append('COGNITO') print(supportedIDProviders) response = client.update_user_pool_client( UserPoolId='${CognitoUserPool}', ClientId='${CognitoUserPoolClient}', ClientName=normalized, RefreshTokenValidity=365, CallbackURLs=callbackURLs, LogoutURLs=logoutURLs, SupportedIdentityProviders=supportedIDProviders, AllowedOAuthFlows=[ 'code', ], AllowedOAuthScopes=[ 'phone', 'email', 'openid', 'profile' ], AllowedOAuthFlowsUserPoolClient=True ) CleanStackNameExecutionRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Principal: Service: - lambda.amazonaws.com Effect: Allow Action: - sts:AssumeRole Policies: - PolicyName: LogsForLambda PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*:*" - PolicyName: XRay PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" - PolicyName: AllowVPCSupport PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ec2:DescribeNetworkInterfaces - ec2:CreateNetworkInterface - ec2:DeleteNetworkInterface Resource: "*" CleanStackName: DependsOn: CleanStackNameExecutionRole Type: Custom::CleanStackName Properties: ServiceToken: !GetAtt CleanStackNameFunction.Arn CleanStackNameFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt CleanStackNameExecutionRole.Arn Runtime: python3.8 Timeout: 300 TracingConfig: Mode: Active Code: ZipFile: !Sub | from __future__ import print_function import json import boto3 import cfnresponse import time def handler(event, context): print(json.dumps(event)) if (event["RequestType"] == "Delete"): responseData={"Data":"OK"} try: cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: print("Exception thrown in cfnresponse: %s" % str(e)) pass else: val = enforceSyntax("${AWS::StackName}") time.sleep(10) # pause for CloudWatch logs responseData={"Data":"OK","CleanStackNameValue":val} try: cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: print("Exception thrown in cfnresponse: %s" % str(e)) pass def enforceSyntax(val): badChars=['0','1','2','3','4','5','6','7','8','9','-'] goodChars=['a','b','c','d','e','f','g','h','i','j','k'] i=0 res = val for b in badChars: res = res.replace(b,goodChars[i]) i +=1 return res