Closed rpattcorner closed 4 years ago
Looking closely at the auth@edge 2.0 code, I think it can go wrong if you:
CreateCloudFrontDistribution
to false
AlternateDomainNames
Which is indeed the scenario you're in, looking at that piece of CFN you pasted.
You can unblock yourself by providing AlternateDomainNames or by setting CreateCloudFrontDistribution to true.
Meanwhile, I will fix the code, to make this scenario work again too.
Question: is this just a test or do you actually want to deploy like this? Because in this scenario you always have to manually update the redirect URI's in the User Pool Domain, after doing the deployment.
This isn't our most common deploy scenario -- the common usage is to use an AlternateDomainName
-- but it's a useful testing scenario for a quick setup that doesn't require a certificate and domain, and for us a required scenario for an agency or company that does not have control over their domains or certificates..
So we would like to be able to both create the cloudfront in the top level stack, setting CreateCloudFrontDistribution
to false and providing no value for AlternateDomainName
. One original hope in testing a@e 2.0 was that in bringing both the UserPool and the Cloudfront inside the top level CFN stack we could avoid a circular dependency by creating the CloudFront (thus making its entry point available to CFN) then using the Cloudfront entry point as the redirect URI in the User Pool.
Unfortunately it looks like a@e needs the Cloudfront default URL, and CloudFront needs to know the URLs of the a@e lambdas so there is still a circularity.
Failing a true solution it would be very welcome to restore the 1.2 capability to allow no AlternateDomainName and CreateCloudFrontDistribution to False, then manually update the user pool.
Eventually we may rewrite to avoid the top level CloudFormation stack altogether in favor of our internal cloud API tool called mu -- but that's a long term prospect.
Unfortunately it looks like a@e needs the Cloudfront default URL, and CloudFront needs to know the URLs of the a@e lambdas so there is still a circularity.
Yes that can only be solved by a custom resource in the top level stack. But the custom resource implementation can be borrowed from a@e, so it will be simple actually. But the a@e custom resource handler arn needs to be made a stack output, so you can refer to it (a 1 min change to a@e)
Funny, I was out walking the dog and the same idea occurred to me. There's already a LambdaCodeUpdateHandler
lambda in a@e that fills the architectural function that would be required in a custom resource, since the LambdaCodeUpdateHandler
adjusts the JSON in the other lambdas for changes in headers and the like!
That would be a great solution ... and I hope it's something you might do. I'm seeing it like this -- just to get it on paper:
AlternateDomainName
and a false CreateCloudFrontDistribution
, and if nothing more is done, manual intervention on the user pool will be required, similar to 1.2. That's the part of the discussion that restores the scenario present in 1.2 but not in 2.0In addition, we can add functionality to significantly make the stacks more independent as you suggest:
LambdaCodeUpdateHandler
is enhanced to allow modifying the CloudFront URL wherever it appearsLambdaCodeUpdateHandler
's Arn is made available as an a@e stack output so it can be invoked from the top level stackLambdaCodeUpdateHandler
with the CloudFront URL Is that what you had in mind? Is that possible? If so I'd be happy to test it out.
R.
Funny, I was out walking the dog and the same idea occurred to me.
LOL!
Yeah that's it in principle––but I mean the UserPoolClientUpdate custom resource to be exact. I need to have a step back to see how this resource is used now and if the plan really makes sense, but I think it does. I'll keep you posted.
If you wanna do it by the way, that's fine too. I can provide guidance while you code the PR (if you need it)
I'd rather you did if you're willing ... I'm pretty snowed on the app itself, and don't have any experience compiling a SAM module.
This should be it: #83
OK you should now be able to add a resource like this to the top-level stack:
UserPoolClientUpdate:
Type: Custom::UserPoolClientUpdate
Condition: UpdateUserPoolClient
Properties:
ServiceToken: !GetAtt LambdaEdgeProtection.Outputs.UserPoolClientUpdateHandler
UserPoolArn: <your arn, or grab from Auth@Edge stack>
UserPoolClientId: <your client id, or grab from Auth@Edge stack>
CloudFrontDistributionDomainName: <your CloudFront domain>
RedirectPathSignIn: "/parseauth"
RedirectPathSignOut: "/"
AlternateDomainNames: []
OAuthScopes: <your scopes>
v2.0.1 in the SAR
It must be about a million o'clock in NE -- thanks! I'll test it right away
Brilliant ... as we're in different time zones (I think), I'll give you preliminary results which are:
Error: [Cognito] invalid_request: invalid_scope [log region: us-east-1]
The only things in cloudtrail I immediately see seem to relate to parseAuth creating logs ...
"eventTime": "2020-09-07T21:52:50Z",
"eventSource": "logs.amazonaws.com",
"eventName": "CreateLogStream",
"awsRegion": "us-east-1",
"sourceIPAddress": "3.235.155.143",
"userAgent": "awslambda-worker/1.0 rusoto/0.42.0 rust/1.45.2 linux",
"errorCode": "ResourceNotFoundException",
"errorMessage": "The specified log group does not exist.",
"requestParameters": {
"logGroupName": "/aws/lambda/us-east-1.ae201nocustom-LambdaEdgeProtectio-ParseAuthHandler-S6CI0U5A5ZVZ",
"logStreamName": "2020/09/07/[1]35bbd71f99a34f9a8f1c4b2537020c06"
},
for scenario 1 (no custom) and
"eventTime": "2020-09-07T21:55:19Z",
"eventSource": "logs.amazonaws.com",
"eventName": "CreateLogStream",
"awsRegion": "us-east-1",
"sourceIPAddress": "3.237.173.225",
"userAgent": "awslambda-worker/1.0 rusoto/0.42.0 rust/1.45.2 linux",
"errorCode": "ResourceNotFoundException",
"errorMessage": "The specified log group does not exist.",
"requestParameters": {
"logStreamName": "2020/09/07/[1]1b33630fb1d14708be678dc689047831",
"logGroupName": "/aws/lambda/us-east-1.ae201customb-LambdaEdgeProtection-ParseAuthHandler-OT6ZOK55QK6B"
}
for scenario 2 (custom resource)
These may be unrelated. I'll dig in more in the AM, but wanted to give you early feedback as this looks very promising
Oh yeah, I think it's something about the log group creation. the URL of the fail says clearly the problematic lambda is parseAuth: https://d8whov6mdjadw.cloudfront.net/parseauth?error_description=invalid_scope&state=eyJub25jZSI6IjE1OTk1MTU1NjRUaHAuQlM5Nm5wRUtuT0RYeiIsInJlcXVlc3RlZFVyaSI6Ii8ifQ&error=invalid_request
. When I navigate to CLoudwatch Events I see:
So maybe something funky with the group creation ... is there somewhere I should have specified a region now that we are no longer tied to us-east-1?
The log group error might be a red herring––the real error looks to be "invalid_scope"
How are you passing the scopes to the custom resource? It expects them here as a list of strings (not as a CommaDelimitedList):
UserPoolClientUpdate:
Type: Custom::UserPoolClientUpdate
Condition: UpdateUserPoolClient
Properties:
ServiceToken: !GetAtt LambdaEdgeProtection.Outputs.UserPoolClientUpdateHandler
UserPoolArn: <your arn, or grab from Auth@Edge stack>
UserPoolClientId: <your client id, or grab from Auth@Edge stack>
CloudFrontDistributionDomainName: <your CloudFront domain>
RedirectPathSignIn: "/parseauth"
RedirectPathSignOut: "/"
AlternateDomainNames: []
OAuthScopes: ["profile", "openid", "email", "phone", "aws.cognito.signin.user.admin"]
That list needs to be the same as what you passed to Auth@Edge (if you did not pass anything it needs to be the same as the Auth@Edge default). But then it is probably easier to not provide the scopes when invoking the custom resource––and keep the ones from auth@edge. So, this should also work:
UserPoolClientUpdate:
Type: Custom::UserPoolClientUpdate
Condition: UpdateUserPoolClient
Properties:
ServiceToken: !GetAtt LambdaEdgeProtection.Outputs.UserPoolClientUpdateHandler
UserPoolArn: <your arn, or grab from Auth@Edge stack>
UserPoolClientId: <your client id, or grab from Auth@Edge stack>
CloudFrontDistributionDomainName: <your CloudFront domain>
RedirectPathSignIn: "/parseauth"
RedirectPathSignOut: "/"
AlternateDomainNames: []
Well, not having a lot of luck here.
Originally, I was passing scopes like this to both the UserPoolClient and the UserPoolClientUpdate:
AllowedOAuthScopes:
- phone
- email
- openid
- profile
and the stack deployed, but the run failed, as noted. I believe the two formats should be the same ... but apparently not.
Substituting the corrected string format in both the UserPoolClient and the UserPoolClientUpdate allowed for a deploy, but I get the identical error on scope.
Adding the additional scope parameter aws.cognito.signin.user.admin
fixed that problem. Seems to allow a user to edit their own profile from this note. Was never needed before..
So, once I got past the scope error I was able to log into Cognito and change my password, but then encountered a new error in parseAuth:
Sign-in issue
We can't sign you in because of a technical problem
Error: Failed to exchange authorization code for tokens: Error: Request failed with status code 400 [log region: us-east-1]
Here are the two stanzas as they currently are ... note that the parameter names are different which seems to be required ... AllowedOAuthScopes
for the UserPoolClient and OAuthScopes
for UserPoolClientUpdate. Making them the same fails at deploy.
UserPoolClient:
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
PreventUserExistenceErrors: ENABLED
GenerateSecret: true
AllowedOAuthScopes: ["profile", "openid", "email", "phone", "aws.cognito.signin.user.admin"]
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthFlows:
- code
SupportedIdentityProviders:
- COGNITO
CallbackURLs:
- https://example.com/will-be-replaced
LogoutURLs:
- https://example.com/will-be-replaced
UserPoolClientUpdate:
UserPoolClientUpdate:
Type: Custom::UserPoolClientUpdate
Condition: UpdateUserPoolClient
Properties:
ServiceToken: !GetAtt LambdaEdgeProtection.Outputs.UserPoolClientUpdateHandler
UserPoolArn: !GetAtt UserPool.Arn
UserPoolClientId: !Ref UserPoolClient
CloudFrontDistributionDomainName: !GetAtt CloudFrontDistribution.DomainName
RedirectPathSignIn: "/parseauth"
RedirectPathSignOut: "/"
AlternateDomainNames: []
OAuthScopes: ["profile", "openid", "email", "phone", "aws.cognito.signin.user.admin"]
By the way, the OAuthScopes
param seems to be required in the UserPoolClientUpdate ... commenting it out leads to a deploy error on the custom resource:
Failed to update resource. InvalidParameterException: AllowedOAuthFlows and AllowedOAuthScopes are required if user pool client is allowed to use OAuth flows. at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:51:27) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:688:14) at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10) at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12) at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10 at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9) at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:690:12) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:1
By the way2 , the scenario 1 (no custom resource, just cloudfront=false and no alternate domain) also fails, this time with an unspecified error after login password entered to cognito:
https://auth-3f673eb0-f150-11ea-a7c2-0a3fc8cbcb75.auth.us-east-1.amazoncognito.com/error
Mmm :|
Can you paste your entire CFN template here, I'll try to reproduce
Preparing a stripped version as the original accesses buckets, etc.
Edited, simplified. Gives the error Failed to exchange authorization code for tokens: Error: Request failed with status code 400 [log region: us-east-1]
that we're working on.
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
Sample stack for custom user pool
Parameters:
AlternateDomainNames:
Type: CommaDelimitedList
Description: "Optional custom domain name for the CloudFront distribution. Must manually point the custom name's A record to CloudFront URL as an alias after deploy."
ACMCertificate:
Type: String
Description: "Only if using an alternate domain name. ARN for a us-east-1 ACM certificate in this account. Leave blank if no alternate domain name"
Installer:
Type: String
Description: Who is installing this application, used for tags
PriceClass:
Type: String
Description: CloudFront price class, e.g. PriceClass_200 for most regions (default), PriceClass_All for all regions (the default), PriceClass_100 least expensive (US, Canada, Europe), or PriceClass_All
Default: PriceClass_100
SemanticVersion:
Type: String
Description: Semantic version of the back end
Default: 2.0.1
HttpHeaders:
Type: String
Description: The HTTP headers to set on all responses from CloudFront. Defaults are illustrations only and contain a report-only Cloud Security Policy -- adjust for your application
Default: >-
{
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
"Referrer-Policy": "same-origin",
"X-XSS-Protection": "1; mode=block",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff"
}
BucketNameParameter:
Type: String
Description: A legal bucket name. Must not exist.
Conditions:
IsCommercial: !Equals [ 'aws', !Ref "AWS::Partition" ]
HasAlternateDomainName: !Not [!Equals [ '', !Join [ "", !Ref AlternateDomainNames ] ] ]
UpdateUserPoolClient: !Equals [ "true", "true" ]
Resources:
ApplicationBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName:
Ref: BucketNameParameter
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
CorsConfiguration:
CorsRules:
-
AllowedOrigins:
- !Sub
- "https://${DomainName}"
- {DomainName: !Select [0, !Ref AlternateDomainNames ] }
AllowedMethods:
- POST
- GET
- PUT
- DELETE
- HEAD
AllowedHeaders:
- "*"
Tags:
-
Key: "OWNER"
Value: "egt-labs"
-
Key: "APPLICATION"
Value: "jmpr"
-
Key: "PATH"
Value: {"Fn::Join": ["", ["/", {"Ref": "AWS::StackName"}, "-installer/"]]}
-
Key: "INSTALLER"
Value: !Ref Installer
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub '${BucketNameParameter}-OAI'
S3AccessPolicyForOAI:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: ApplicationBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
CanonicalUser:
Fn::GetAtt: [ CloudFrontOriginAccessIdentity , S3CanonicalUserId ]
Action: "s3:GetObject"
Resource: !Sub "${ApplicationBucket.Arn}/*"
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
!If
- HasAlternateDomainName
- !Ref AlternateDomainNames
- !Ref AWS::NoValue
ViewerCertificate:
!If
- HasAlternateDomainName
- AcmCertificateArn: !Ref ACMCertificate
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2018
- !Ref AWS::NoValue
CacheBehaviors:
- PathPattern: /parseauth
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt LambdaEdgeProtection.Outputs.ParseAuthHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
- PathPattern: /refreshauth
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt LambdaEdgeProtection.Outputs.RefreshAuthHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
- PathPattern: /signout
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt LambdaEdgeProtection.Outputs.SignOutHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
DefaultCacheBehavior:
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt LambdaEdgeProtection.Outputs.CheckAuthHandler
- EventType: origin-response
LambdaFunctionARN: !GetAtt LambdaEdgeProtection.Outputs.HttpHeadersHandler
TargetOriginId: protected-origin
ViewerProtocolPolicy: redirect-to-https
Enabled: true
Origins:
- DomainName: example.org # Dummy origin is used for Lambda@Edge functions, keep this as-is
Id: dummy-origin
CustomOriginConfig:
OriginProtocolPolicy: match-viewer
- DomainName: !Sub "${ApplicationBucket}.s3.amazonaws.com"
Id: protected-origin
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
PriceClass: !Ref PriceClass
DefaultRootObject: index.html
Tags:
-
Key: "OWNER"
Value: "egt-labs"
-
Key: "APPLICATION"
Value: "jmpr"
-
Key: "PATH"
Value: {"Fn::Join": ["", ["/", {"Ref": "AWS::StackName"}, "-installer/"]]}
-
Key: "INSTALLER"
Value: !Ref Installer
CognitoIdentityPool:
Type: "AWS::Cognito::IdentityPool"
Properties:
IdentityPoolName: !Sub ${BucketNameParameter}-Identity
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId: !Ref UserPoolClient
# ProviderName: !Sub 'cognito-idp.${AWS::Region}.amazonaws.com/${LambdaEdgeProtection.Outputs.UserPoolId}'
ProviderName: !Sub
- cognito-idp.${AWS::Region}.amazonaws.com/${userpool}
- { userpool: !Ref UserPool }
# ProviderName: !Sub
# - 'cognito-idp.${AWS::Region}.amazonaws.com/${userpool}'
# - { userpool: !Ref UserPoolClient }
# ProviderName: !Sub 'cognito-idp.${AWS::Region}.amazonaws.com/${!Ref UserPool}'
# Create a role for unauthorized acces to AWS resources.
# Present only for illustration or possible future use. Not assigned to pool
CognitoUnAuthorizedRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud": !Ref CognitoIdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": unauthenticated
Policies:
- PolicyName: "CognitoUnauthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
Resource: "*"
Tags:
-
Key: "OWNER"
Value: "egt-labs"
-
Key: "APPLICATION"
Value: "jmpr"
-
Key: "PATH"
Value: {"Fn::Join": ["", ["/", {"Ref": "AWS::StackName"}, "-installer/"]]}
-
Key: "INSTALLER"
Value: !Ref Installer
# Create a role for authorized acces to AWS resources. Control what your user can access. This example only allows Lambda invokation
# Only allows users in the previously created Identity Pool
CognitoAuthorizedRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
"cognito-identity.amazonaws.com:aud": !Ref CognitoIdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
Policies:
- PolicyName: "BasicCognito"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "mobileanalytics:PutEvents"
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"
- PolicyName: "jmprExecution"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "lambda:InvokeFunction"
Resource: "*"
- PolicyName: "jmprS3"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "s3:*"
Resource: "*"
Tags:
-
Key: "OWNER"
Value: "egt-labs"
-
Key: "APPLICATION"
Value: "jmpr"
-
Key: "PATH"
Value: {"Fn::Join": ["", ["/", {"Ref": "AWS::StackName"}, "-installer/"]]}
-
Key: "INSTALLER"
Value: !Ref Installer
# Assigns the roles to the Identity Pool
IdentityPoolRoleMapping:
Type: "AWS::Cognito::IdentityPoolRoleAttachment"
Properties:
IdentityPoolId: !Ref CognitoIdentityPool
Roles:
authenticated: !GetAtt CognitoAuthorizedRole.Arn
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Ref AWS::StackName
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
UsernameAttributes:
- email
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
PreventUserExistenceErrors: ENABLED
GenerateSecret: true
AllowedOAuthScopes: ["profile", "openid", "email", "phone", "aws.cognito.signin.user.admin"]
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthFlows:
- code
SupportedIdentityProviders:
- COGNITO
CallbackURLs:
# Ideally you would put your real callback URL's here, pointing to your custom domain name––the custom
# domain name that you would also supply as AlternateDomainNames to the cloudfront-authorization-at-edge stack below.
# However, if you just want to use the CloudFront domain name, use the sentinel value as below, to avoid a circular dependency.
# This sentinal value will be replaced automatically by the cloudfront-authorization-at-edge stack, with the CloudFront domain name
- https://example.com/will-be-replaced
LogoutURLs:
# Ideally you would put your real logout URL's here, pointing to your custom domain name––the custom
# domain name that you would also supply as AlternateDomainNames to the cloudfront-authorization-at-edge stack below.
# However, if you just want to use the CloudFront domain name, use the sentinel value as below, to avoid a circular dependency.
# This sentinal value will be replaced automatically by the cloudfront-authorization-at-edge stack, with the CloudFront domain name
- https://example.com/will-be-replaced
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub
- "auth-${StackIdSuffix}"
- StackIdSuffix: !Select
- 2
- !Split
- "/"
- !Ref AWS::StackId
UserPoolId: !Ref UserPool
LambdaEdgeProtection:
Type: AWS::Serverless::Application
DependsOn:
- UserPoolDomain
Properties:
Location:
ApplicationId: arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge
SemanticVersion: !Ref SemanticVersion
Parameters:
CreateCloudFrontDistribution: "false"
HttpHeaders: !Ref HttpHeaders
UserPoolArn: !GetAtt UserPool.Arn
UserPoolClientId: !Ref UserPoolClient
UserPoolClientUpdate:
Type: Custom::UserPoolClientUpdate
Condition: UpdateUserPoolClient
Properties:
ServiceToken: !GetAtt LambdaEdgeProtection.Outputs.UserPoolClientUpdateHandler
UserPoolArn: !GetAtt UserPool.Arn
UserPoolClientId: !Ref UserPoolClient
CloudFrontDistributionDomainName: !GetAtt CloudFrontDistribution.DomainName
RedirectPathSignIn: "/parseauth"
RedirectPathSignOut: "/"
AlternateDomainNames: []
OAuthScopes: ["profile", "openid", "email", "phone", "aws.cognito.signin.user.admin"]
Only stack parms needed are the bucket name and stack name
I've found the culprit: in your template change GenerateSecret to false and redeploy and 🎉 it works.
Or, leave it to true, but disable SPA mode then for Auth@Edge: EnableSPAMode = false
I'll update the docs to point this out clearly (and fix the reuse example which was wrong).
Note: I found the cause by setting LogLevel to "debug" for Auth@Edge and checking what happened in parseAuth logs.
Thanks! That's great news! Starting testing now. Some interesting questions:
Testing away, exciting!
Updated this example: example-serverless-app-reuse/reuse-with-existing-user-pool.yaml
About your Q's:
It looks like generateSecret is optional and allows the deployer to generate a secret on the client that the SPA can check to avoid impersonators. Am I right in guessing that the a@e doesn't use that feature yet, which is why it fails? It sounds useful at some point, but it's unclear how the SPA would know what to check the generated secret against, unless perhaps the UserPoolClient returned it on deploy as a configuration for the SPA. Is that right? What does it mean to run as EnableSPAMode as false when in fact you're a SPA. I worry about that one
Have a read of this: SPA mode or Static Site mode?
For SPAs it's not useful to have a Client Secret, as anyone can do "view source" and see it.
Where do you set the LogLevel on Auth@Edge? How come you get parseAuth logs and I just get the complaint about no log group?
It's a param to the app, just as CreateCloudFrontDistribution and EnableSPAMode: https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/78867bbe30ddc83fae4fc98f7766e2089f3c95d7/template.yaml#L122-L131
To find the logs you have to go through hoops a bit, as Lambda@Edge logs get their own special log groups, in the region where they end up running (the "normal" log group of the Lambda would not show anything). Easiest way to find them is to go to the CloudFront monitoring dashboard, find the function invocations and jump to the log in the right region there.
Thanks, makes sense. I took SPA mode too literally I think. For whatever reason I do not see the updates to example-serverless-app-reuse/reuse-with-existing-user-pool.yaml in the master branch.
Continuing testing
There may be an issue on the 3rd test, which is the app without custom resources and a manual update to the Application Client callback URLs. Will pursue and get back
Still an issue on the final test. Works like this:
This doesn't look like it's coming from the lambdas, rather from Cognito This scenario works in the 1.2 version Not critical for us now that we have the custom resource but might be worth a look
I can't reproduce this error. For me deploying the below template, and after deployment changing the redirect URL's, works fine.
So what is different in your case?
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
TODO
Parameters:
EnableSPAMode:
Type: String
Description: Set to 'false' to disable SPA-specific features (i.e. when deploying a static site that won't interact with logout/refresh)
Default: "true"
AllowedValues:
- "true"
- "false"
OAuthScopes:
Type: CommaDelimitedList
Description: The OAuth scopes to request the User Pool to add to the access token JWT
Default: "phone, email, profile, openid, aws.cognito.signin.user.admin"
PriceClass:
Type: String
Description: The price class of the CloudFront distribution
Default: PriceClass_100
Conditions:
GenerateClientSecret: !Equals
- EnableSPAMode
- "false"
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Ref AWS::StackName
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
UsernameAttributes:
- email
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
PreventUserExistenceErrors: ENABLED
GenerateSecret: !If
- GenerateClientSecret
- true
- false
AllowedOAuthScopes: !Ref OAuthScopes
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthFlows:
- code
SupportedIdentityProviders:
- COGNITO
CallbackURLs:
# Replace
- https://example.com/will-be-replaced
LogoutURLs:
# Replace
- https://example.com/will-be-replaced
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub
- "auth-${StackIdSuffix}"
- StackIdSuffix: !Select
- 2
- !Split
- "/"
- !Ref AWS::StackId
UserPoolId: !Ref UserPool
ApplicationBucket:
Type: AWS::S3::Bucket
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub "${ApplicationBucket}-OAI"
S3AccessPolicyForOAI:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: ApplicationBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
CanonicalUser:
Fn::GetAtt: [CloudFrontOriginAccessIdentity, S3CanonicalUserId]
Action: "s3:GetObject"
Resource: !Sub "${ApplicationBucket.Arn}/*"
- Effect: "Allow"
Principal:
CanonicalUser:
Fn::GetAtt: [CloudFrontOriginAccessIdentity, S3CanonicalUserId]
Action: "s3:ListBucket"
Resource: !GetAtt ApplicationBucket.Arn
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
# Aliases: <your alternate domain names, also pass these to the serverless stack below>
# ViewerCertificate: <the config for your HTTPS certificate>
CacheBehaviors:
- PathPattern: /parseauth
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt MyLambdaEdgeProtectedSpaSetup.Outputs.ParseAuthHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
- PathPattern: /refreshauth
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt MyLambdaEdgeProtectedSpaSetup.Outputs.RefreshAuthHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
- PathPattern: /signout
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt MyLambdaEdgeProtectedSpaSetup.Outputs.SignOutHandler
TargetOriginId: dummy-origin
ViewerProtocolPolicy: redirect-to-https
DefaultCacheBehavior:
Compress: true
ForwardedValues:
QueryString: true
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt MyLambdaEdgeProtectedSpaSetup.Outputs.CheckAuthHandler
- EventType: origin-response
LambdaFunctionARN: !GetAtt MyLambdaEdgeProtectedSpaSetup.Outputs.HttpHeadersHandler
TargetOriginId: protected-origin
ViewerProtocolPolicy: redirect-to-https
Enabled: true
Origins:
- DomainName: example.org # Dummy origin is used for Lambda@Edge functions, keep this as-is
Id: dummy-origin
CustomOriginConfig:
OriginProtocolPolicy: match-viewer
- DomainName: !Sub "${ApplicationBucket}.s3.amazonaws.com"
Id: protected-origin
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
PriceClass: !Ref PriceClass
DefaultRootObject: index.html
MyLambdaEdgeProtectedSpaSetup:
Type: AWS::Serverless::Application
DependsOn: UserPoolDomain
Properties:
Location:
ApplicationId: arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge
SemanticVersion: 2.0.1
Parameters:
UserPoolArn: !GetAtt UserPool.Arn
UserPoolClientId: !Ref UserPoolClient
EnableSPAMode: !Ref EnableSPAMode
CreateCloudFrontDistribution: false
OAuthScopes: !Join
- ","
- !Ref OAuthScopes
Outputs:
WebsiteUrl:
Description: URL of the CloudFront distribution that serves your SPA from S3
Value: !Sub "https://${CloudFrontDistribution.DomainName}"
Thanks, Otto. I've gone back to that install after doing nothing to it over the weekend, and it is now working! Very peculiar that it stabilized without action. Let's pass over this particular rabbit-hole and I'l raise it again if it becomes an issue. Looks like 2.01 is now a going concern ... will work on some of the cookie injection magic it brings.
Working on migrating our application to a@e 2.0 to look further at #81 and other features, and run into a roadblock. The demonstration stack for 2.0 that illustrates how to define Cognito UserPool etc. in the parent stack works fine but ...
I've pulled the Cognito User Pool and other related artifacts into the main stack, and successfully created all artifacts until we get to the calls to the nested a@e stack. In creating the framework I see:
Embedded stack arn:aws:cloudformation:us-east-1:REDACTED:stack/ae200d-LambdaEdgeProtection-L97QEYQVDZLR/4cb7b300-ef9e-11ea-ace7-126c97cb5bc1 was not successfully created: Cannot export output RedirectUrisSignOut. Exported values must not be empty or whitespace-only.
My call to the nested stack which worked fine in 1.2 has changed only in minor ways ... I've added parameters for the UserPoolArn and UserPoolClientId I've created in the parent stack, just as the a@e example for 2.0 does. The failing call looks like this:
A visit through the code shows that RedirectUrisSignOut is mainly referenced in
src/cfn-custom-resources/user-pool-client/index.ts
and in the main template, suggesting that maybe I've got the dependencies wrong, but they seem obvious ... an explicit depends on the UserPoolDomain, and implicit ones for the UserPoolClient and UserPool.Arn. All we're really doing here is adding the HttpHeaders parameter from the main template.Another possibility -- because this example does not use custom domains, but just relies on the CloudFront URL, as does the example, is some problem with the sentinel values -- but I left them alone, as they are in the example.
Any idea what might be going on?