I have a SAM template that was working fine until I added a trigger to my cognito user pool.
I searched about the error that is throwing me: Circular dependency between resources I can understand that the trigger is creating a reference to the user pool and then the circular dependency arises, but I can not find how to solve the problem. I only need to set the trigger of my cognito user pool to get custom messages/emails when a user is created.
This is my SAM code:
AdminCognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
AutoVerifiedAttributes:
- email
VerificationMessageTemplate:
DefaultEmailOption: CONFIRM_WITH_LINK
Policies:
PasswordPolicy:
MinimumLength: 8
UsernameAttributes:
- email
Schema:
- AttributeDataType: String
Name: email
Required: true
Mutable: true
- AttributeDataType: String
Name: id
# Required: false
Mutable: true
AdminCognitoChangePassword:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/config.customCognitoEvents
Role: !GetAtt lambdaRole.Arn
Events:
CognitoEvent:
Type: Cognito
Properties:
UserPool: !Ref AdminCognitoUserPool
Trigger: CustomMessage
The problem was in the globals function environment variables. I was calling AdminCognitoUserPool and thats why the circular dependency was rising.
I have a API Gateway Rest Api resource defined with this template:
AWSTemplateFormatVersion: '2010-09-09'
Description: "Api gateway"
Resources:
ApiGateway:
Type: "AWS::ApiGateway::RestApi"
Properties:
BodyS3Location: "./openapi-spec.yaml"
And the contents of openapi-spec.yaml (based on this example) being:
openapi: "3.0.2"
info:
title: SampleApi
paths:
/test:
get:
summary: Test
responses:
"200":
description: Ok
security:
- UserPool: [ ]
x-amazon-apigateway-integration:
# ....
components:
securitySchemes:
UserPool:
type: apiKey
name: Authorization
in: header
x-amazon-apigateway-authtype: cognito_user_pools
x-amazon-apigateway-authorizer:
type: cognito_user_pools
providerARNs:
### THIS VALUE ###
- "arn:aws:cognito-idp:eu-west-1:123456789012:userpool/eu-west-1_abcd12345"
I'd like to be able to deploy this template in multiple environments/account and having this hardcoded providerARN is limiting that. So my questions are:
How can values for the providerARNs field be passed in dynamically?
If that can't be done, then are there any workarounds to this so that I don't have to hardcode the providerArns here?
Note: Already tried to use stage variables and they don't seem to work here.
If you don't have an existing Cognito user pool then you would have to define one using AWS::Cognito::UserPool in CloudFormation, then you can simply reference the arn of this user pool using !GetAtt.
But if you have an existing Cognito user pool then you can also import it to a stack using CloudFormation following these steps.
Here's an example:
template.yaml
Resources:
ApiGateway:
Type: "AWS::ApiGateway::RestApi"
Properties:
BodyS3Location: "./openapi-spec.yaml"
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
# ....
openapi-spec.yaml
openapi: "3.0.2"
# ....
components:
securitySchemes:
UserPool:
type: apiKey
name: Authorization
in: header
x-amazon-apigateway-authtype: cognito_user_pools
x-amazon-apigateway-authorizer:
type: cognito_user_pools
providerARNs:
- !GetAtt CognitoUserPool.Arn
When testing my TOKEN endpoint in PostMan, I'm getting the error HTTP 400 - "invalid_grant".
In PostMan, I've configured the Authorization header (w/Basic clientId:secret) and header Content_Type. In the url encoded form, I've set the grant_type = client_credentials. All of these settings are confirmed in the instructions here:
https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
After manual inspection, my CloudFormation template deploys all the settings correctly.
If I go into Cognito settings, select App Clients from the navigation and then “Save app client changes” without making any changes, I no longer get the same error in PostMan and I can retrieve a valid access code from there on. It’s almost as is the changes aren’t ‘active’ in AWS unless I re-save in the AWS Console for whatever reason.
Is something not fully committed on the AWS backend side unless I manually hit save in the console?
**Again, this template, settings and PostMan test do work BUT only after I go into Cognito and make an edit, save, undo my edit and save again.
Here's my CF template
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Integration for webSvc1 and webSvc2
Parameters:
StageName:
...
Globals:
Function:
Timeout: 20
Api:
OpenApiVersion: 3.0.1
Resources:
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
UserPoolName: !Sub ${CognitoUserPoolName}-${EnvironmentName}
UserPoolResourceServer:
Type: 'AWS::Cognito::UserPoolResourceServer'
DependsOn:
- UserPool
Properties:
Identifier: !Sub ${CognitoUserPoolName}-${EnvironmentName}
Name: api-resource-server
Scopes:
- ScopeName: "api.read"
ScopeDescription: "Read access"
UserPoolId: !Ref UserPool
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
DependsOn:
- UserPool
- UserPoolResourceServer
Properties:
UserPoolId: !Ref UserPool
Domain: !Sub id-${EnvironmentName}
# Creates a User Pool Client to be used by the identity pool
UserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
DependsOn:
- UserPool
- UserPoolResourceServer
Properties:
ClientName: !Sub ${CognitoUserPoolName}-client-${EnvironmentName}
GenerateSecret: true
UserPoolId: !Ref UserPool
SupportedIdentityProviders:
- COGNITO
AllowedOAuthFlows:
- client_credentials
AllowedOAuthScopes:
- !Sub ${CognitoUserPoolName}-${EnvironmentName}/api.read
In my AWS project, I use the serverless framework to deploy lambda functions and a Cognito user pool.
I want 2 of my lambda functions to be triggered by Cognito user pool events, so here is what I did:
functions:
autoMoveToUserGroup:
handler: src/auto-move-to-user-group.handler
name: auto-move-to-user-group
events:
- cognitoUserPool:
pool: test-user-pool
trigger: PostAuthentication
existing: True
autoValidationUserEmailModification:
handler: src/auto-validation-user-email-modification.handler
name: auto-validation-user-email-modification
events:
- cognitoUserPool:
pool: test-user-pool
trigger: CustomMessage
existing: True
resources:
Resources:
MyCognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: test-user-pool
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
Schema:
- Name: email
Required: True
When I deploy using the serverless deploy command, I sometimes got the following error:
An error occurred: AutoDashvalidationDashuserDashemailDashmodificationCustomCognitoUserPool1 - Failed to create resource.
Only one request to update this UserPool can be processed at a time.
It looks like a random bug, since it doesn’t occurs everytime (happens very often, though). Also, when it occurs, it doesn’t always occurs on the same function.
Did I do something wrong? How can I fix that?
Thanks for your help.
OK so here is a workaround: without using the events part in the serverless syntax, we can do this only using raw CloudFormation syntax:
resources:
Resources:
MyCognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
...
LambdaConfig:
CustomMessage: arn:aws:lambda:eu-central-1:123456789012:function:auto-validation-user-email-modification
lambdaCognitoPermissionCustomMessage:
Type: AWS::Lambda::Permission
Properties:
Action: 'lambda:InvokeFunction'
FunctionName: !Ref 'AutoValidationUserEmailModificationLambdaFunction'
Principal: cognito-idp.amazonaws.com
SourceArn: !GetAtt
- MyCognitoUserPool
- Arn
Also, according to the serverless team, it's actually a bug that should be fixed and deployed on August, 14th 2019.
Hope it helps.
I am trying to create a custom Lambda authorizer that will be shared between a few different services/serverless stacks. If I understand the documentation here https://serverless.com/framework/docs/providers/aws/events/apigateway/#note-while-using-authorizers-with-shared-api-gateway, that means that I need to create a shared authorizer resource in a “common resources” service/serverless stack, and then refer to that shared authorizer from my other services. First of all: Is my understanding correct?
If my understanding is correct, my next question becomes: How do I do this? The documentation doesn’t provide a clear example for lambda authorizers, so here’s how I tried to customize it:
functions:
authorizerFunc:
handler: authorizer/authorizer.handler
runtime: nodejs8.10
resources:
Resources:
authorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
AuthorizerResultTtlInSeconds: 0
Name: Authorizer
Type: REQUEST
AuthorizerUri: ???
RestApiId:
Fn::ImportValue: myRestApiId
I don’t understand what the syntax for AuthorizerUri is supposed to be. I’ve tried “Ref: authorizerFunc”, “Fn::GetAtt: [authorizerFunc, Arn]” etc. to no avail.
When I get the authorizerUri working, do I just add an Output for my authorizer resource, then Fn::ImportValue it from the services containing my API Lambdas?
Link to my question on the Serverless forum for posterity: https://forum.serverless.com/t/shared-lambda-authorizer/6447
EDIT: Apparently my answer is now outdated. For newer versions of serverless, see the other answers. I don't know which answer is best/most up-to-date, but if someone lets me know I'll change which answer is accepted to that one.
I eventually got it to work, so here's how I set up my autherizer's serverless.yml:
service: user-admin-authorizer
custom:
region: ${file(serverless.env.yml):${opt:stage}.REGION}
provider:
name: aws
region: ${self:custom.region}
functions:
authorizer:
handler: src/authorizer.handler
runtime: nodejs8.10
resources:
Resources:
Authorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: Authorizer
Type: REQUEST
AuthorizerUri:
Fn::Join: [ "",
[
"arn:aws:apigateway:",
"${self:custom.region}",
":lambda:path/",
"2015-03-31/functions/",
Fn::GetAtt: ["AuthorizerLambdaFunction", "Arn" ],
"/invocations"
]]
RestApiId:
Fn::ImportValue: api-gateway:${opt:stage}:rest-api-id
apiGatewayLambdaPermissions:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: [ AuthorizerLambdaFunction, Arn]
Action: lambda:InvokeFunction
Principal:
Fn::Join: [ "",
[
"apigateway.",
Ref: AWS::URLSuffix
]]
Outputs:
AuthorizerRef:
Value:
Ref: Authorizer
Export:
Name: authorizer-ref:${opt:stage}
Things to note: Even though the authorizer function is called "authorizer", you need to capitalize the first letter and append "LambdaFunction" to its name when using it with GetAtt, so "authorizer" becomes "AuthorizerLambdaFunction" for some reason. I also had to add the lambda permission resource.
The API gateway resource also needs two outputs, its API ID and its API root resource ID. Here's how my API gateway's serverless.yml is set up:
resources:
Resources:
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ApiGateway
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGateway
Export:
Name: api-gateway:${opt:stage}:rest-api-id
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGateway
- RootResourceId
Export:
Name: api-gateway:${opt:stage}:root-resource-id
Now you just need to specify to your other services that they should use this API gateway (the imported values are the outputs of the API gateway):
provider:
name: aws
apiGateway:
restApiId:
Fn::ImportValue: api-gateway:${opt:stage}:rest-api-id
restApiRootResourceId:
Fn::ImportValue: api-gateway:${opt:stage}:root-resource-id
After that, the authorizer can be added to individual functions in this service like so:
authorizer:
type: CUSTOM
authorizerId:
Fn::ImportValue: authorizer-ref:${opt:stage}
I had the same issue that you describe. Or at least I think so. And I managed to get it solved by following the documentation on links you provided.
The serverless documentation states for the authorizer format to be
authorizer:
# Provide both type and authorizerId
type: COGNITO_USER_POOLS # TOKEN or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
authorizerId:
Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID
Per my understanding, my solution (provide below) follows the hard-coded authorizer ID approach.
In the service that has the shared authorizer, it is declared in the serverless.yml in normal fashion, i.e.
functions:
myCustomAuthorizer:
handler: path/to/authorizer.handler
name: my-shared-custom-authorizer
Then in the service that wishes to use this shared authorizer, the function in servlerless.yml is declared as
functions:
foo:
# some properties ...
events:
- http:
# ... other properties ...
authorizer:
name: authorize
arn:
Fn::Join:
- ""
- - "arn:aws:lambda"
# References to values such as region, account id, stage, etc
# Can be done with Pseudo Parameter Reference
- ":"
- "function:myCustomAuthorizer"
It was crucial to add the name property. It would not work without it, at least at the moment.
For details see
ARN naming conventions
Pseudo Parameter Reference
Fn::Join
Unfortunately I cannot say whether this approach has some limitations compared to your suggestion of defining authorizer as a resource. In fact, that might make it easier to re-use the same authorizer in multiple functions within same service.
Serverless 1.35.1
For people stumbling across this thread, here is the new way
Wherever you create the user pool, you can go ahead and add ApiGatewayAuthorizer
# create a user pool as normal
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
# Generate an app client name based on the stage
ClientName: ${self:custom.stage}-user-pool-client
UserPoolId:
Ref: CognitoUserPool
ExplicitAuthFlows:
- ADMIN_NO_SRP_AUTH
GenerateSecret: true
# then add an authorizer you can reference later
ApiGatewayAuthorizer:
DependsOn:
# this is pre-defined by serverless
- ApiGatewayRestApi
Type: AWS::ApiGateway::Authorizer
Properties:
Name: cognito_auth
# apparently ApiGatewayRestApi is a global string
RestApiId: { "Ref" : "ApiGatewayRestApi" }
IdentitySource: method.request.header.Authorization
Type: COGNITO_USER_POOLS
ProviderARNs:
- Fn::GetAtt: [CognitoUserPool, Arn]
Then when you define your functions
graphql:
handler: src/app.graphqlHandler
events:
- http:
path: /
method: post
cors: true
integration: lambda
# add this and just reference the authorizer
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewayAuthorizer
This is how I did my set up since the answer posted above didn't worked for me. May be it could be helpful for someone.
resources:
Resources:
ApiGatewayAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
AuthorizerResultTtlInSeconds: 0
IdentitySource: method.request.header.Authorization
AuthorizerUri:
Fn::Join: ["",
[
"arn:aws:apigateway:",
"${self:custom.region}",
":lambda:path/",
"2015-03-31/functions/",
Fn::GetAtt: ["YourFunctionNameLambdaFunction", "Arn" ],
"/invocations"
]]
RestApiId:
Fn::ImportValue: ${self:custom.stage}-ApiGatewayRestApiId
Name: api-${self:custom.stage}-authorizer
Type: REQUEST
ApiGatewayAuthorizerPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: ["YourFunctionNameLambdaFunction", "Arn"]
Action: lambda:InvokeFunction
Principal:
Fn::Join: ["",["apigateway.", { Ref: "AWS::URLSuffix"}]]
Outputs:
AuthorizerRef:
Value:
Ref: ApiGatewayAuthorizer
Export:
Name: authorizer-ref:${self:custom.stage}
I hope you know how to add an API gateway and import it here like
RestApiId:
Fn::ImportValue: ${self:custom.stage}-ApiGatewayRestApiId
, since it's already specified in the accepted answer
And In my case I passed value in the event header as Authorization
type to get it in the authorizer lambda function and my type is
REQUEST
Changing to a shared custom API Gateway Lambda Authorizer was straightforward once it was working as part of the service. At that point it was just add an arn: to a deployed lambda (authorizer) and remove the "authorizer" definition from the service to a separate deployable service.
myLambdaName:
handler: handler.someNodeFunction
name: something-someNodeFunction
events:
- http:
path: /path/to/resource
method: get
cors: true
authorizer:
name: myCustomAuthorizer
# forwarding lambda proxy event stuff to the custom authorizer
IdentitySource: method.request.header.Authorization, context.path
type: request
arn: 'arn:aws:lambda:region:##:function:something-else-myCustomAuthorizer'
Then the other "service" just has some custom authorizers shared by multiple deployed microservices.
functions:
myCustomAuthorizer:
name: something-else-myCustomAuthorizer
handler: handler.myCustomAuthorizer
Side note:
If you'd want a token type authorizer, which does forward the "authorization: bearer xyzsddfsf" as a simple event:
{
"type": "TOKEN",
"methodArn": "arn:aws:execute-api:region:####:apigwIdHere/dev/GET/path/to/resource",
"authorizationToken": "Bearer ...."
}
authorizer:
arn: 'arn:aws:lambda:region:##:function:something-else-myCustomAuthorizer'