-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Description
RFC Closure date: August 03 2018
API Gateway Authorizers come in three flavors - Cognito, Request Lambda, and Token Lambda. Both flavors of Lambda are very similar (TOKEN allows using a single header; REQUEST allows using multiple query strings, headers, stage variables, and "context"; these are sent as IdentitySource which also forms the cache key). We should be able to infer the Authorizer type based on the properties of the Authorizer, and allow the user to specify the Type
when necessary.
Authorizers are unique compared to other things we've supported so far in SAM since they can be applied to multiple paths/methods of an API. As modelled in API Gateway, Authorizers are a child resource of an API, however, we don't need to model it that way in SAM. That is, Authorizers can either be defined as a property of a Serverless::API or established as an entirely new SAM Resource which can be used across multiple APIs.
The main benefit of defining Authorizers directly on the Serverless::API Resource is it is simpler/more concise. It also maps cleanly to API Gateway modeling which helps ensure future-proofing.
Globals:
Api:
Auth: # We will also define other auth options here such as Usage Plans/Api Keys, AWS_IAM, and Resource Policies
Authorizers: # APIs can define multiple Authorizers
MyAuth: # Authorizer properties will be discuused separately
... # Will go into detail on properties in a separate section
DefaultAuthorizer: MyAuth # Optional; This will be used as the default Authorizer for all Resources/Methods; Alternate name: MainAuthorizer?
Resources:
ServerlessRestApi: # This is the implicit API resource created when you don't specify an `ApiId` in an Api Event.
Type: "AWS::Serverless::API"
Properties:
Auth:
Authorizers:
MyLambdaAuth:
...
MyCognitoAuth:
...
DefaultAuthorizer: MyLambdaAuth # NOTE: We are using the Authorizer name directly here (instead of !Ref/!GetAtt) since these are created in Swagger, not as CloudFormation resources.
MyFn:
Type: AWS::Serverless::Function
Properties:
...
Events:
GetRoot:
Type: Api
Properties:
Method: get
Path: /
Auth:
Authorizer: MyCognitoAuth # Overrides the API's DefaultAuthorizer; can also specify null to make it open (null is default when no DefaultAuthorizer is specified)
The benefit of creating a new Serverless::Authorizer resource is it allows sharing Authorizers across multiple APIs*, and allows defining Authorizers for the Implicit API without needing to define the implicit ServerlessRestApi
resource (which isn't obvious to users). The disadantages are it's less concise, and would increase the confusion around not referencing with !Ref/!GetAtt (the standar way you reference all other Resources).
- You can achieve sharing of Authorizers with the other approach by defining them in Globals. However, this only works when the Authorizers are the same across the APIs which you want to share Authorizers; i.e. if you need to define an additional Authorizer, it will override all Authorizers defined in Globals.
Globals:
Api:
DefaultAuthorizer: MyCognitoAuth
ApiAuthorizer:
IdentityValidationExpression: abc_.*
Resources:
MyCognitoAuth:
Type: "AWS::Serverless::ApiAuthorizer" # NOTE: Authorizers will be generated as part of the Swagger document, not an AWS::ApiGateway::Authorizer CFN resource which you might expect when seeing this defined as a Resource like this.
Properties:
...
MyApi:
Type: "AWS::Serverless::API"
Properties:
Auth:
DefaultAuthorizer: MyCognitoAuth # Again, we are referencing direclty by name without using !Ref, which is what you would expect when referencing a Resource.
I am inclined for the former syntax defining Authorizers directly on an Api. The syntax is cleaner; maps to API Gateway modeling; and the lack of !Ref/!GetAtt will be less confusing. Exposing the Implicit API Resource is not desirable, but for most advanced use-cases users will define their own Api, making this a non-issue. I don't have any data on how useful "Shareable API Authorizers" is, but hopefully using Globals will be acceptable for most.
I'll use my preferred syntax for describing an Authorizers properties, but they should remain the same for whichever syntax we agree on.
Resources:
MyApi:
Type: AWS::Serverless::API
Properties:
Auth:
Authorizers:
MyCognitoAuth:
Properties:
Type: COGNITO_USER_POOLS # Optional; inferred due to existence of `UserPools`
Name: ... # Optional; default to MyCognitoAuth (since Authorizer names don't need to be unique across APIs) or MyApiMyCognitoAuth (prefix with API name for consistency; likely be an issue when specifying Global Authorizers)
UserPool: !GetAtt MyUserPool.Arn # Must specify either UserPool or UserPools
UserPools: # Most of the time users will want to integrate with just a single UserPool; should we have 2 separate properties? Or a single `UserPools` which accepts both string and Array<string>?
- !GetAtt MyUserPool.Arn
RequestHeader: Authorization # is "Authorization" a sane default header? Sets IdentitySource to method.request.header.Authorization
RequestHeaderValidationExpression: ... # Optional; ensures the request header matches a pattern before checking in with the Authorizer endpoint; is there a default we can set for Cognito User Pools Auth?
MyLambdaTokenAuth:
Properties:
Type: LAMBDA_TOKEN # Optional; inferred due to existence of `RequestHeader`
FunctionArn: !GetAtt MyAuthFn.Arn # Potential future enhancement might be to specify `Function` directly on the Authorizer
RequestHeader: Authorization
InvokeWithCredentials: ... # Optional; null (default) | arn:aws:iam::123456789012:role/S3Access | S3Access; NOTE: CALLER_CREDENTIALS is not a valid value here unlike the proposed InvokeWithCredentials on an Integration
ReauthorizeEverySeconds: ... # Optional; Default: 300 (5 minutes); 0 means the authorizer will be called on every request
RequestHeaderValidationExpression: ... # Optional
MyLambdaRequesAuth:
Properties:
Type: LAMBDA_REQUEST # Optional; inferred due to existence of `RequestHeaders`, `RequestQueryStrings`, `RequestStageVariables`, or `RequestContext`
FunctionArn: !GetAtt MyAuthFn.Arn
RequestHeaders:
- Authorization1 # method.request.header.Authorization1
RequestQueryStrings:
- Authorization2 # method.request.querystring.Authorization2
RequestStageVariables: # "Request" doesn't make much sense for StageVariables or Context; I use it for consistency's sake
- Authorization3 # stageVariables.Authorization3
RequestContext:
- Authorization4 #context.Authorization4
# end result would be `IdentitySource: method.request.header.Authorization1,method.request.querystring.Authorization2,stageVariables.Authorization3,stageVariables.Authorization3`
InvokeWithCredentials: ... # Optional
ReauthorizeEverySeconds: ... # Optional
RequestHeaderValidationExpression: ... # Optional
Should we stick with IdentitySource*
as it exists in CloudFormation instead of renaming it to Request*
? I think the latter is easier to understand in most cases, e.g. IdentitySourceHeader vs RequestHeader. Happy to hear alternatives.