Skip to content

Instantly share code, notes, and snippets.

@grosscol
Last active October 5, 2022 06:47
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grosscol/3623d2c2affdd3b88ed4538537bb0850 to your computer and use it in GitHub Desktop.
Save grosscol/3623d2c2affdd3b88ed4538537bb0850 to your computer and use it in GitHub Desktop.
Custom Cloudformation Resource to get CloudFront Distribution of Cognito User Pool
---
#
# This template example assumes a UserPool and UserPoolDomain exist.
# The function of this is to produce a custom resource with an attribute
# that can be referenced for DNSName of an Route53::RecordSet AliasTarget.
#
# AliasTarget:
# HostedZone: Z2FDTNDATAQYW2
# DNSNAME: !GetAtt UPDomain.CloudFrontDistribution
# Note: swap out AuthDomain parameter and use however you're determining your User Pool Domain in your stack
#
# Run from CLI:
# aws cloudformation create-stack --template-body file://get-cognito-cfd-target.yaml \
# --stack-name LambdaGetterDemo --capabilities CAPABILITY_IAM \
# --parameters ParameterKey=AuthDomain,ParameterValue=auth.example.com
#
# Clean up from CLI:
# aws cloudformation delete-stack --stack-name LambdaGetterDemo
Parameters:
AuthDomain:
Type: String
Default: auth.example.com
Description: UserPool custom domain.
Resources:
#
# Lambda to get access to resource attributes cloudformation doesn't expose yet.
#
# Policy to allow access to logs and cognito-identity
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
- Effect: Allow
Action:
- cognito-idp:DescribeUserPoolDomain
Resource: '*'
GetUserPoolClientCFDistribution:
Type: AWS::Lambda::Function
Properties:
Description: Look up CloudFrontDistribution of UserPoolDomain
Handler: index.handler
MemorySize: 128
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: "python3.7"
Timeout: 30
Code:
ZipFile: |
import json
import boto3
import cfnresponse
import logging
def handler(event, context):
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# initialize our responses, assume failure by default
response_data = {}
response_status = cfnresponse.FAILED
logger.info('Received event: {}'.format(json.dumps(event))) #'
# When you get deleted, congratulate the deleter.
if event['RequestType'] == 'Delete':
response_status = cfnresponse.SUCCESS
cfnresponse.send(event, context, response_status, response_data)
return None
# Make ourselves a cognito api client
try:
cognito=boto3.client('cognito-idp')
except Exception as e:
logger.info('boto3.client failure: {}'.format(e)) #'
cfnresponse.send(event, context, response_status, response_data)
return None
# Look up the properties of the user pool domain
# UserPoolDomain is passed in via the event
user_pool_domain = event['ResourceProperties']['UserPoolDomain']
try:
user_pool_domain_info = cognito.describe_user_pool_domain(Domain=user_pool_domain)
except Exception as e:
logger.info('cognito.describe_user_pool_client failure: {}'.format(e)) # appease yaml highlighting'
cfnresponse.send(event, context, response_status, response_data)
return None
# Extract the pertient information
cloudfront_distribution = user_pool_domain_info['DomainDescription']['CloudFrontDistribution']
# Stuff the information into the response
response_data['CloudFrontDistribution'] = cloudfront_distribution
response_data['Foo'] = 'Bar'
# Ship off the reponse
response_status = cfnresponse.SUCCESS
cfnresponse.send(event, context, response_status, response_data, noEcho=True)
#
# Custom Resource to hold user pool DNS alias target for custom domain
#
# UserPoolDomain is passed in via the event
UPDomain:
Type: Custom::UserPoolCloudFrontDistribution
Properties:
ServiceToken: !GetAtt GetUserPoolClientCFDistribution.Arn
UserPoolDomain: !Ref AuthDomain
Outputs:
UserPoolDomainTarget:
Description: "The CloudFront distribution target for A and AAAA aliases."
Value: !GetAtt UPDomain.CloudFrontDistribution
@grosscol
Copy link
Author

You could use Ouput Exports and [Fn::Import](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) to use the value in other stacks.

I use the gist'd approach it in the same stack because the custom resource, UPDomain, holds the value. The custom resource request object gets it's properties defined by lambda function pointed to by ServiceToken.

I'm using it to get the distribution for a cognito hosted auth page.... which is a bit of a toy example. It does require creating a Route53 record and pointing it to a cloudfront distribution. So in that sense, I expect this to be generally useful. At least it solved an issue I had that required some ugly external scripting or manual intervention.

Below is roughly how I'm using it in a small stack.

  # See gist for definition of lambda that does cloudformation lookup
  #  GetUserPoolClientCFDistribution:
  #    Type: AWS::Lambda::Function
                                                                                                       
  # Create UserPoolDomain that will need a record pointing at a cloudfront distribution.
  AuthSubDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      CustomDomainConfig:
        # Cert arn is supplied as parameter to stack
        CertificateArn: !Ref DNSCertArn
      Domain: auth.example.com
      # User pool id is supplied as parameter to stack.
      UserPoolId: !If [ UseInternalPool, !Ref InternalUserPool, !Ref ExtUserPoolId ]

  # Record set that will have an alias target pointing to the cloudfront distribution.
  AuthDomainRecordSet:
    Type: AWS::Route53::RecordSet
    DependsOn: AuthSubDomain
    Properties:
      Name: auth.example.com
      HostedZoneId: !Ref ZoneId
      Comment: Custom auth domain for MICCA
      Type: A
      AliasTarget:
        # Hard coded zone id for cloudformation
        HostedZoneId: Z2FDTNDATAQYW2
        # The UPDomain custom resource holds the cloudfront dist domain target,
        #   which was a property defined by the lambda response 
        DNSName: !GetAtt UPDomain.CloudFrontDistribution
        EvaluateTargetHealth: false

  UPDomain:
    Type: Custom::UserPoolCloudFrontDistribution
    Properties:
      # This is the labmda that will define the properties for custom object
      ServiceToken: !GetAtt GetUserPoolClientCFDistribution.Arn
      # This becomes a ResourceProperty of the event passed to lambda
      UserPoolDomain: auth.example.com

I use !Sub auth.${MainDomainName} instead of auth.example.com. Substitute your own domain variables scheme as necessary to make it generally useful to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment