aws-cloudformation / cloudformation-coverage-roadmap

The AWS CloudFormation Public Coverage Roadmap
https://aws.amazon.com/cloudformation/
Creative Commons Attribution Share Alike 4.0 International
1.1k stars 54 forks source link

AWS::Cognito::UserPoolDomain missing return value for the CloudFront target #356

Open mvanleest opened 4 years ago

mvanleest commented 4 years ago

I am missing the return value for the CloudFront target so that you could create a DNS record in the same template so that you could do this:

  DNS:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: mydomain.com.
      Name: myuserpool.mydomain.com.
      Type: A
      AliasTarget:
        HostedZoneId: Z2FDTNDATAQYW2
        DNSName: !GetAtt UserPoolDomain.DomainName

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      UserPoolId: !Ref UserPool
      Domain: myuserpool.mydomain.com
      CustomDomainConfig:
        CertificateArn: !Ref CertificateArn

Originally posted by @Nr18 in https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/58#issuecomment-539652016

lkolchin commented 4 years ago

Yeah need this one too.

jk2l commented 4 years ago

i am glad i am not the only need this feature... back to write my Cfn custom resources for now

lkolchin commented 4 years ago

i am glad i am not the only need this feature... back to write my Cfn custom resources for now

Please share it here....until AWS provides proper resolution :)

jk2l commented 4 years ago

sorry, as contractor my works are IP of my client. can't share it out

0xdevalias commented 4 years ago

Edit 2: See this comment for the modern AWS CDK version of this.


For those that end up here like I did, as a workaround it's possible to get the CloudFrontDistribution using the AWS SDK/CLI:


Edit: My personal workaround using https://github.com/aws/aws-cdk and @aws-cdk/custom-resources.AwsSdkCall:

const cdk = require('@aws-cdk/core')
const cognito = require('@aws-cdk/aws-cognito')
const cr = require('@aws-cdk/custom-resources')
const route53 = require('@aws-cdk/aws-route53')

// The userPool was defined earlier in the code (not shown here)

// This creates the user pool domain resource
const userPoolDomain = new cognito.CfnUserPoolDomain(
  this,
  'UserPoolDomain',
  {
    userPoolId: userPool.userPoolId,
    domain: authDomain,
    customDomainConfig: {
      certificateArn,
    },
  }
)
userPoolDomain.node.addDependency(userPool)

// This allows us to get the cloudfront distribution using a custom resource that calls the AWS SDK
const describeCognitoUserPoolDomain = new cr.AwsCustomResource(
  this,
  'DescribeCognitoUserPoolDomain',
  {
    resourceType: 'Custom::DescribeCognitoUserPoolDomain',
    onCreate: {
      region: 'us-east-1',
      service: 'CognitoIdentityServiceProvider',
      action: 'describeUserPoolDomain',
      parameters: {
        Domain: userPoolDomain.domain,
      },
      physicalResourceId: cr.PhysicalResourceId.of(userPoolDomain.domain),
    },
    // TODO: can we restrict this policy more? Get the ARN for the user pool domain? Or the user pool maybe?
    policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
      resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
    }),
  }
)
describeCognitoUserPoolDomain.node.addDependency(userPoolDomain)

const userPoolDomainDistribution = describeCognitoUserPoolDomain.getResponseField(
  'DomainDescription.CloudFrontDistribution'
)
swoldemi commented 4 years ago

I recently threw together a Custom Resource to do this and released it in AWS SAR: https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:273450712882:applications~amazon-cognito-domain-distribution

Happy to iterate on any improvement ideas

clorichel commented 4 years ago

Here's how to get around it. Assuming you have a stack.yaml that you deploy with a CI tool, say through bash:

THE_STACK_NAME="my-cognito-stack"
THE_DOMAIN_NAME="auth.yourveryowndomain.org"

# get the alias target
# notice that it will be empty upon first launch (chicken and the egg problem)
ALIAS_TARGET=$(aws cognito-idp describe-user-pool-domain --domain ${THE_DOMAIN_NAME} | grep CloudFrontDistribution | cut -d \" -f4)

# create/update the deployment CloudFormation stack
# notice the AliasTarget parameter (which can be empty, it's okay!)
aws cloudformation deploy --stack-name ${THE_STACK_NAME} --template-file stack.yaml --parameter-overrides AliasTarget=${ALIAS_TARGET} DomainName=${THE_DOMAIN_NAME}

The stack.yaml minimal version (remember to fill the UserPool config):

---
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  DomainName:
    Type: String
    Default: auth.yourveryowndomain.org
    Description: The domain name to use to serve this project.

  ZoneName:
    Type: String
    Default: yourveryowndomain.org
    Description: The hosted zone name coming along with the DomainName used.

  AliasTarget: # no default value, can be empty
    Type: String
    Description: The UserPoolDomain alias target.

Conditions: # here's "the trick"
  HasAliasTarget: !Not [!Equals ['', !Ref AliasTarget]]

Resources:
  Certificate:
    Type: "AWS::CertificateManager::Certificate"
    Properties: 
      DomainName: !Ref ZoneName
      DomainValidationOptions:
        - DomainName: !Ref ZoneName
          ValidationDomain: !Ref ZoneName
      SubjectAlternativeNames:
        - !Ref DomainName

  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      [... fill that with your configuration! ...]

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      UserPoolId: !Ref UserPool
      Domain: !Ref DomainName
      CustomDomainConfig:
        CertificateArn: !Ref Certificate

  DnsRecord: # if AliasTarget parameter is empty, well we just can't do that one!
    Condition: HasAliasTarget # and here's how we don't do it when we can't
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub "${ZoneName}."
      AliasTarget:
        DNSName: !Ref AliasTarget
        EvaluateTargetHealth: false
        # HostedZoneId value for CloudFront is always this one
        # see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html
        HostedZoneId: Z2FDTNDATAQYW2
      Name: !Ref DomainName
      Type: A

Be aware CloudFormation conditions are not "a trick" at all: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html. We simply use it as a trick along with the "first launch won't do it all" to get around our scenario.

Kinda weird, but only for the first run! Launch it again: everything is fine 👌


PS: can't wait to avoid all that by simply having the CloudFrontDistribution alias target directly in the AWS::Cognito::UserPoolDomain return values 🤗

asterikx commented 4 years ago

CDK recently added constructs for this. In CDK, all you need to do is:

const userPoolDomain = new cognito.UserPoolDomain(this, 'UserPoolDomain', {
  userPool,
  customDomain: {
    domainName: `auth.${domainName}`,
    certificate,
  },
});

new route53.ARecord(this, 'UserPoolDomainAliasRecord', {
  zone: hostedZone,
  recordName: `auth.${domainName}`,
  target: route53.RecordTarget.fromAlias(new route53_targets.UserPoolDomainTarget(userPoolDomain)),
});

CDK API reference for UserPoolDomainTarget. In fact, it was @0xdevalias's proposal that was adopted by the CDK team (see here).

henning-krause commented 3 years ago

Nice to see it solved in the CDK. But we need that for simple Cloudformation templates as well.

michaelwittig commented 2 years ago

duplicate of #241 ?

jeff1evesque commented 2 years ago

I will probably utilize this custom resource, and forget (next year or after) when the attribute is built in.

sigpwned commented 2 years ago

@swoldemi Just tried your custom resource SAR, and it worked a treat. Thanks for sharing!

mattyboy commented 1 year ago

This may have been fixed recently? I initially hit this and came across this article, then I noticed CloudFrontDistribution is listed as an attribute in the aws-resource-cognito-userpooldomain documentation

I managed to successfully deploy a AWS::Route53::RecordSet along with AWS::Cognito::UserPoolDomain with no workaround. Hopefully this helps someone else who ends up on this path.

Using the original example posted the CloudFormation Template looks like this:

  AuthDomain:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Domain: !Ref AuthDomainName
      Type: 'A'
      AliasTarget:
        DNSName: !GetAtt UserPoolDomain.CloudFrontDistribution        
        HostedZoneId: Z2FDTNDATAQYW2 # For CloudFront, HostedZoneId is always Z2FDTNDATAQYW2
        EvaluateTargetHealth: false

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      UserPoolId: !Ref UserPool
      Domain: !Ref AuthDomainName
      CustomDomainConfig:
        CertificateArn: !Ref DomainCertificateArn

I needed to give my user cognito-idp:DescribeUserPoolDomain permission for it to read this value, just in case you have tight IAM policies.

hauntingEcho commented 1 year ago

Yup, looks like it was added here in February