diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 48bf5364..2051c091 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,7 +36,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGateway', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" archive_plugin_enabled: true diff --git a/Examples/APIGateway+LambdaAuthorizer/.gitignore b/Examples/APIGateway+LambdaAuthorizer/.gitignore new file mode 100644 index 00000000..e4044f6f --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/.gitignore @@ -0,0 +1,2 @@ +samconfig.toml +Makefile diff --git a/Examples/APIGateway+LambdaAuthorizer/Package.swift b/Examples/APIGateway+LambdaAuthorizer/Package.swift new file mode 100644 index 00000000..574bdbbe --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Package.swift @@ -0,0 +1,69 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +#if os(macOS) +let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] +#else +let platforms: [PackageDescription.SupportedPlatform]? = nil +#endif + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]), + .executable(name: "AuthorizerLambda", targets: ["AuthorizerLambda"]), + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + .executableTarget( + name: "AuthorizerLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/APIGateway+LambdaAuthorizer/README.md b/Examples/APIGateway+LambdaAuthorizer/README.md new file mode 100644 index 00000000..a7a55812 --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/README.md @@ -0,0 +1,112 @@ +# Lambda Authorizer with API Gateway + +This is an example of a Lambda Authorizer function. There are two Lambda functions in this example. The first one is the authorizer function. The second one is the business function. The business function is exposed through a REST API using the API Gateway. The API Gateway is configured to use the authorizer function to implement a custom logic to authorize the requests. + +>[!NOTE] +> If your application is protected by JWT tokens, it's recommended to use [the native JWT authorizer provided by the API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). The Lambda authorizer is useful when you need to implement a custom authorization logic. See the [OAuth 2.0/JWT authorizer example for AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-controlling-access-to-apis-oauth2-authorizer.html) to learn how to use the native JWT authorizer with SAM. + +## Code + +The authorizer function is a simple function that checks data received from the API Gateway. In this example, the API Gateway is configured to pass the content of the `Authorization` header to the authorizer Lambda function. + +There are two possible responses from a Lambda Authorizer function: policy and simple. The policy response returns an IAM policy document that describes the permissions of the caller. The simple response returns a boolean value that indicates if the caller is authorized or not. You can read more about the two types of responses in the [Lambda authorizer response format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html) section of the API Gateway documentation. + +This example uses an authorizer that returns the simple response. The authorizer function is defined in the `Sources/AuthorizerLambda` directory. The business function is defined in the `Sources/APIGatewayLambda` directory. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function. +The ZIP file are located under `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager` + +## Deploy + +The deployment must include the Lambda functions and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayWithLambdaAuthorizer \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URI +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +When invoking the Lambda function without `Authorization` header, the response is a `401 Unauthorized` error. + +```bash +curl -v https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> +* Request completely sent off +< HTTP/2 401 +< date: Sat, 04 Jan 2025 14:03:02 GMT +< content-type: application/json +< content-length: 26 +< apigw-requestid: D3bfpidOoAMESiQ= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"message":"Unauthorized"} +``` + +When invoking the Lambda function with the `Authorization` header, the response is a `200 OK` status code. Note that the Lambda Authorizer function is configured to accept any value in the `Authorization` header. + +```bash +curl -v -H 'Authorization: 123' https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> Authorization: 123 +> +* Request completely sent off +< HTTP/2 200 +< date: Sat, 04 Jan 2025 14:04:43 GMT +< content-type: application/json +< content-length: 911 +< apigw-requestid: D3bvRjJcoAMEaig= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"headers":{"x-forwarded-port":"443","x-forwarded-proto":"https","host":"6sm6270j21.execute-api.us-east-1.amazonaws.com","user-agent":"curl\/8.7.1","accept":"*\/*","content-length":"0","x-amzn-trace-id":"Root=1-67793ffa-05f1296f1a52f8a066180020","authorization":"123","x-forwarded-for":"81.49.207.77"},"routeKey":"ANY \/demo","version":"2.0","rawQueryString":"","isBase64Encoded":false,"queryStringParameters":{},"pathParameters":{},"rawPath":"\/demo","cookies":[],"requestContext":{"domainPrefix":"6sm6270j21","requestId":"D3bvRjJcoAMEaig=","domainName":"6sm6270j21.execute-api.us-east-1.amazonaws.com","stage":"$default","authorizer":{"lambda":{"abc1":"xyz1"}},"timeEpoch":1735999482988,"accountId":"401955065246","time":"04\/Jan\/2025:14:04:42 +0000","http":{"method":"GET","sourceIp":"81.49.207.77","path":"\/demo","userAgent":"curl\/8.7.1","protocol":"HTTP\/1.1"},"apiId":"6sm6270j21"},"stageVariables":{}} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete --stack-name APIGatewayWithLambdaAuthorizer +``` \ No newline at end of file diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift new file mode 100644 index 00000000..f7662d1c --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayV2Response(statusCode: .ok, headers: header, encodableBody: event) +} + +try await runtime.run() diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift new file mode 100644 index 00000000..60ea2b7b --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +// +// This is an example of a policy authorizer that always authorizes the request. +// The policy authorizer returns an IAM policy document that defines what the Lambda function caller can do and optional context key-value pairs +// +// This code is shown for the example only and is not used in this demo. +// This code doesn't perform any type of token validation. It should be used as a reference only. +let policyAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = { + (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Policy Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + // then it creates and returns a response + return APIGatewayLambdaAuthorizerPolicyResponse( + principalId: "John Appleseed", + + // this policy allows the caller to invoke any API Gateway endpoint + policyDocument: .init(statement: [ + .init( + action: "execute-api:Invoke", + effect: .allow, + resource: "*" + ) + + ]), + + // this is additional context we want to return to the caller + context: [ + "abc1": "xyz1", + "abc2": "xyz2", + ] + ) + } + +// +// This is an example of a simple authorizer that always authorizes the request. +// A simple authorizer returns a yes/no decision and optional context key-value pairs +// +// This code doesn't perform any type of token validation. It should be used as a reference only. +let simpleAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerSimpleResponse = { + (_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Simple Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + return APIGatewayLambdaAuthorizerSimpleResponse( + // this is the authorization decision: yes or no + isAuthorized: true, + + // this is additional context we want to return to the caller + context: ["abc1": "xyz1"] + ) + } + +// create the runtime and start polling for new events. +// in this demo we use the simple authorizer handler +let runtime = LambdaRuntime(body: simpleAuthorizerHandler) +try await runtime.run() diff --git a/Examples/APIGateway+LambdaAuthorizer/template.yaml b/Examples/APIGateway+LambdaAuthorizer/template.yaml new file mode 100644 index 00000000..ae9a026f --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/template.yaml @@ -0,0 +1,77 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # The API Gateway + MyProtectedApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + DefaultAuthorizer: MyLambdaRequestAuthorizer + Authorizers: + MyLambdaRequestAuthorizer: + FunctionArn: !GetAtt AuthorizerLambda.Arn + Identity: + Headers: + - Authorization + AuthorizerPayloadFormatVersion: "2.0" + EnableSimpleResponses: true + + # Give the API Gateway permissions to invoke the Lambda authorizer + AuthorizerPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref AuthorizerLambda + Principal: apigateway.amazonaws.com + SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyProtectedApi}/* + + # Lambda business function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + Properties: + ApiId: !Ref MyProtectedApi + Path: /demo + Method: ANY + + # Lambda authorizer function + AuthorizerLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AuthorizerLambda/AuthorizerLambda.zip + Timeout: 29 # max 29 seconds for Lambda authorizers + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URI + Value: !Sub "https://${MyProtectedApi}.execute-api.${AWS::Region}.amazonaws.com/demo" diff --git a/Examples/README.md b/Examples/README.md index 0057e380..53ccc8bd 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -18,6 +18,8 @@ This directory contains example code for Lambda functions. - **[API Gateway](APIGateway/README.md)**: an HTTPS REST API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) and a Lambda function as backend (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). +- **[API Gateway with Lambda Authorizer](APIGateway+LambdaAuthorizer/README.md)**: an HTTPS REST API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) protected by a Lambda authorizer (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + - **[BackgroundTasks](BackgroundTasks/README.md)**: a Lambda function that continues to run background tasks after having sent the response (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). - **[CDK](CDK/README.md)**: a simple example of an AWS Lambda function invoked through an Amazon API Gateway and deployed with the Cloud Development Kit (CDK).