diff --git a/README.md b/README.md index 49ecb2b79..ae97745b4 100644 --- a/README.md +++ b/README.md @@ -939,11 +939,18 @@ to change Zappa's behavior. Use these at your own risk! "iam_authorization": false, // optional, use IAM to require request signing. Default false. Note that enabling this will override the authorizer configuration. "include": ["your_special_library_to_load_at_handler_init"], // load special libraries into PYTHONPATH at handler init that certain modules cannot find on path "authorizer": { + "type": "REQUEST", // Authorizer type, REQUEST or TOKEN (Default 'TOKEN') "function": "your_module.your_auth_function", // Local function to run for token validation. For more information about the function see below. "arn": "arn:aws:lambda:::function:", // Existing Lambda function to run for token validation. "result_ttl": 300, // Optional. Default 300. The time-to-live (TTL) period, in seconds, that specifies how long API Gateway caches authorizer results. Currently, the maximum TTL value is 3600 seconds. "token_header": "Authorization", // Optional. Default 'Authorization'. The name of a custom authorization header containing the token that clients submit as part of their requests. "validation_expression": "^Bearer \\w+$", // Optional. A validation expression for the incoming token, specify a regular expression. + "identity_sources": { // Optional. The names of the custom request expressions destined for the authorizer. + "headers": ["Authorization", "Host"], + "query_strings": ["token"], + "stage_variables": ["test"], + "contexts": ["principalId"], + } }, "keep_warm": true, // Create CloudWatch events to keep the server warm. Default true. To remove, set to false and then `unschedule`. "keep_warm_expression": "rate(4 minutes)", // How often to execute the keep-warm, in cron and rate format. Default 4 minutes. diff --git a/tests/tests.py b/tests/tests.py index 563e158f5..3e0a51c7c 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -390,6 +390,7 @@ def test_create_api_gateway_routes_with_different_auth_methods(self): # Authorizer and IAM authorizer = { + "type": "TOKEN", "function": "runapi.authorization.gateway_authorizer.evaluate_token", "result_ttl": 300, "token_header": "Authorization", @@ -469,6 +470,60 @@ def test_create_api_gateway_routes_with_different_auth_methods(self): parsable_template["Resources"]["Authorizer"]["Properties"]["AuthorizerUri"], ) + # Authorizer of type request with identity sources + authorizer = { + "type": "REQUEST", + "function": "runapi.authorization.gateway_authorizer.evaluate_token", + "result_ttl": 300, + "identity_sources": { + "headers": ["Authorization"], + "query_strings": ["token"], + "stage_variables": ["test"], + "contexts": ["principalId"], + } + } + z.create_stack_template(lambda_arn, "helloworld", False, False, authorizer) + parsable_template = json.loads(z.cf_template.to_json()) + self.assertEqual( + "CUSTOM", + parsable_template["Resources"]["GET0"]["Properties"]["AuthorizationType"], + ) + self.assertEqual( + "CUSTOM", + parsable_template["Resources"]["GET1"]["Properties"]["AuthorizationType"], + ) + self.assertEqual( + "REQUEST", parsable_template["Resources"]["Authorizer"]["Properties"]["Type"] + ) + self.assertEqual( + "method.request.header.Authorization,method.request.querystring.token,method.stageVariables.test,method.context.principalId", + parsable_template["Resources"]["Authorizer"]["Properties"]["IdentitySource"] + ) + + # Authorizer of type request without identity sources + authorizer = { + "type": "REQUEST", + "function": "runapi.authorization.gateway_authorizer.evaluate_token", + "result_ttl": 300, + } + z.create_stack_template(lambda_arn, "helloworld", False, False, authorizer) + parsable_template = json.loads(z.cf_template.to_json()) + self.assertEqual( + "CUSTOM", + parsable_template["Resources"]["GET0"]["Properties"]["AuthorizationType"], + ) + self.assertEqual( + "CUSTOM", + parsable_template["Resources"]["GET1"]["Properties"]["AuthorizationType"], + ) + self.assertEqual( + "REQUEST", parsable_template["Resources"]["Authorizer"]["Properties"]["Type"] + ) + self.assertEqual( + "", + parsable_template["Resources"]["Authorizer"]["Properties"]["IdentitySource"] + ) + def test_policy_json(self): # ensure the policy docs are valid JSON json.loads(ASSUME_POLICY) diff --git a/tests/tests_placebo.py b/tests/tests_placebo.py index 1b6789c9a..8d3b40299 100644 --- a/tests/tests_placebo.py +++ b/tests/tests_placebo.py @@ -433,7 +433,7 @@ def test_handler(self, session): } self.assertEqual("AWS SQS EVENT", lh.handler(event, None)) - # Test Authorizer event + # Test Authorizer event of type TOKEN event = { "authorizationToken": "hubtoken1", "methodArn": "arn:aws:execute-api:us-west-2:1234:xxxxx/dev/GET/v1/endpoint/param", @@ -441,6 +441,64 @@ def test_handler(self, session): } self.assertEqual("AUTHORIZER_EVENT", lh.handler(event, None)) + # Test Authorizer event of type REQUEST + event = { + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-west-2:1234:xxxxx/dev/GET/v1/endpoint/param", + "resource": "/", + "path": "/", + "httpMethod": "GET", + "headers": { + "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + "Host": "example.com" + }, + "multiValueHeaders": { + "Authorization": [ + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ], + "Host": [ + "example.com" + ] + }, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "requestContext": { + "resourceId": "test-invoke-resource-id", + "resourcePath": "/", + "httpMethod": "GET", + "extendedRequestId": "ODtjMEaurPEFpbQ=", + "requestTime": "24/Feb/2022: 17: 39: 45 +0000", + "path": "/", + "accountId": "429480868624", + "protocol": "HTTP/1.1", + "stage": "test-invoke-stage", + "domainPrefix": "testPrefix", + "requestTimeEpoch": 1645724385013, + "requestId": "13e8a7e1-1b24-467a-afd1-854d7268db1b", + "identity": { + "cognitoIdentityPoolId": None, + "cognitoIdentityId": None, + "apiKey": "test-invoke-api-key", + "principalOrgId": None, + "cognitoAuthenticationType": None, + "userArn": "arn:aws:iam::fooo:user/my.username", + "apiKeyId": "test-invoke-api-key-id", + "userAgent": "aws-internal/3 aws-sdk-java/1.12.159 Linux/5.4.172-100.336.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.322-b06 java/1.8.0_322 vendor/Oracle_Corporation cfg/retry-mode/standard", + "accountId": "429480868624", + "caller": "AIDAWH7YN7MIM5EMI2SCJ", + "sourceIp": "test-invoke-source-ip", + "accessKey": "ASIAWH7YN7MIOMF3BO7W", + "cognitoAuthenticationProvider": None, + "user": None + }, + "domainName": "testPrefix.testDomainName", + "apiId": "nyfueqhql3" + } + } + self.assertEqual("AUTHORIZER_EVENT", lh.handler(event, None)) + # Ensure Zappa does return 401 if no function was defined. lh.settings.AUTHORIZER_FUNCTION = None with self.assertRaisesRegexp(Exception, "Unauthorized"): diff --git a/zappa/core.py b/zappa/core.py index 6de97f429..4e34b7174 100644 --- a/zappa/core.py +++ b/zappa/core.py @@ -1789,10 +1789,39 @@ def create_authorizer(self, restapi, uri, authorizer): if authorizer_type == "TOKEN": if not self.credentials_arn: self.get_credentials_arn() - authorizer_resource.AuthorizerResultTtlInSeconds = authorizer.get("result_ttl", 300) + authorizer_resource.AuthorizerResultTtlInSeconds = authorizer.get( + "result_ttl", 300 + ) + authorizer_resource.IdentitySource = ( + "method.request.header.%s" % authorizer.get("token_header", "Authorization") + ) authorizer_resource.AuthorizerCredentials = self.credentials_arn if authorizer_type == "COGNITO_USER_POOLS": authorizer_resource.ProviderARNs = authorizer.get("provider_arns") + if authorizer_type == "REQUEST": + if not self.credentials_arn: + self.get_credentials_arn() + authorizer_resource.AuthorizerResultTtlInSeconds = authorizer.get( + "result_ttl", 300 + ) + authorizer_resource.IdentitySource = "" + identity_sources = authorizer.get("identity_sources", {}) + for source_key in identity_sources: + if source_key == "headers": + for header in identity_sources[source_key]: + authorizer_resource.IdentitySource += "method.request.header.%s," % header + elif source_key == "query_strings": + for query_string in identity_sources[source_key]: + authorizer_resource.IdentitySource += "method.request.querystring.%s," % query_string + elif source_key == "stage_variables": + for stage_variable in identity_sources[source_key]: + authorizer_resource.IdentitySource += "method.stageVariables.%s," % stage_variable + elif source_key == "contexts": + for context in identity_sources[source_key]: + authorizer_resource.IdentitySource += "method.context.%s," % context + + if len(authorizer_resource.IdentitySource) > 1 and authorizer_resource.IdentitySource[-1] == ',': + authorizer_resource.IdentitySource = authorizer_resource.IdentitySource[:-1] self.cf_api_resources.append(authorizer_resource.title) self.cf_template.add_resource(authorizer_resource) @@ -2261,7 +2290,9 @@ def create_stack_template( elif iam_authorization: auth_type = "AWS_IAM" elif authorizer: - auth_type = authorizer.get("type", "CUSTOM") + auth_type = authorizer.get("type", "TOKEN").upper() + if auth_type in ["TOKEN", "REQUEST"]: + auth_type = "CUSTOM" # build a fresh template self.cf_template = troposphere.Template() diff --git a/zappa/handler.py b/zappa/handler.py index 1c6fdb0fd..33aa43e77 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -486,7 +486,7 @@ def handler(self, event, context): return result # This is an API Gateway authorizer event - elif event.get("type") == "TOKEN": + elif event.get("type") in ["TOKEN", "REQUEST"]: whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function(whole_function)