As the second part of this picture-resizing system, we need to create an endpoint that will let our users upload their pictures to the S3 bucket. As we explained at the beginning, there is no need to develop any custom software for that because API Gateway, besides executing Lambda functions, also lets us expose some of AWS APIs to public the internet. It means, we can let API Gateway clients use the S3 upload API on our behalf.
How can we configure this? First, we have to create a new role that can be assumed by API Gateway and only grants s3:PutObject and s3:PutObjectAcl permissions to our profile pictures bucket. Let's add this permission to our Resources section of the CloudFormation template:
"ApiGatewayProxyRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "apigateway.amazonaws.com" ] }, "Action": "sts:AssumeRole" } ] }, "Path": "/", "Policies": [ { "PolicyName": "S3BucketPolicy", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:PutObjectAcl" ], "Resource": [ { "Fn::Sub": "arn:aws:s3:::${ProfilePicturesBucket}" }, { "Fn::Sub": "arn:aws:s3:::${ProfilePicturesBucket}/*" } ] } ] } } ] } }
Now, we can create REST resources for the endpoint. We will put our method to the /users/{userid}/picture path, and we will create a PUT method for updating the profile picture:
"UsersIdResource": { "Type": "AWS::ApiGateway::Resource", "Properties": { "PathPart": "{id}", "RestApiId": { "Ref": "RestApi" }, "ParentId": { "Ref": "UsersResource" } } }, "UsersIdPictureResource": { "Type": "AWS::ApiGateway::Resource", "Properties": { "PathPart": "picture", "RestApiId": { "Ref": "RestApi" }, "ParentId": { "Ref": "UsersIdResource" } } }
Now, we can add the PUT method to the proxy S3 call:
"UsersIdPicturePutMethod": { "Type": "AWS::ApiGateway::Method", "Properties": { "HttpMethod": "PUT", "RestApiId": { "Ref": "RestApi" }, "AuthorizationType": "CUSTOM", "AuthorizerId": { "Ref": "ApiGatewayAuthorizer" }, "ResourceId": { "Ref": "UsersIdPictureResource" }, "RequestParameters": { "method.request.path.id": "True", "method.request.header.Content-Type": "True", "method.request.header.Content-Length": "True" }, "Integration": { "Type": "AWS", "Uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:s3:path/
${ProfilePicturesBucket}/uploads/{filename}" }, "IntegrationHttpMethod": "PUT", "Credentials": { "Fn::GetAtt": [ "ApiGatewayProxyRole", "Arn" ] }, "RequestParameters": { "integration.request.path.filename": "context.requestId", "integration.request.header.Content-Type":
"method.request.header.Content-Type", "integration.request.header.Content-Length":
"method.request.header.Content-Length", "integration.request.header.Expect": "'100-continue'", "integration.request.header.x-amz-acl": "'public-read'", "integration.request.header.x-amz-meta-user-id": "method.request.path.id" }, "RequestTemplates": { }, "PassthroughBehavior": "WHEN_NO_TEMPLATES", "IntegrationResponses": [ { "SelectionPattern": "4\\d{2}", "StatusCode": "400" }, { "SelectionPattern": "5\\d{2}", "StatusCode": "500" }, { "SelectionPattern": ".*", "StatusCode": "202", "ResponseTemplates": { "application/json": { "Fn::Sub": "{\"status\": \"pending\"}" } } } ] }, "MethodResponses": [ { "StatusCode": "202" }, { "StatusCode": "400" }, { "StatusCode": "500" } ] } }
This method definition may seem a bit complicated. What we are doing here is mapping HTTP request properties coming from API Gateway to the S3 API calls. We are passing through the request body to S3 API, and in the case of a successful call, we are returning 202 Accepted response. The reason for using this status code is that our image processing is asynchronous; therefore, we have to signal the client that its request has been accepted and currently is being processed. Note that we are using the automatically generated API Gateway request ID to generate the uploaded file's name (integration.request.path.filename": "context.requestId"). The uploaded file will be saved in the upload/ folder and with this random name. But how will our Lambda function tell the user ID of the uploaded picture? We will use S3's file metadata feature for this (integration.request.header.x-amz-meta-user-id": "method.request.path.id"). We are reading the id value from the request path and passing it to the S3 API. S3 then saves the user-id metadata to the uploaded photo, which can be read by the Lambda function.
We enabled the Lambda authorizer for this method, which means this method can be only called with an authorization token. However, as you might have noted, in this case, every authenticated user can modify the profile picture of someone else. In the following sections, we will modify our authorizer to prevent this situation.
After uploading the stack, you can try to upload an image to S3 using the API Gateway endpoint. You can use the following command, adapting it to your domain and a local image:
$ curl --data-binary @${HOME} /test.jpg -X PUT -H
"Content-Type: image/jpeg" -H "Authorization: Bearer validtoken"
https://example.com/users/1234-1234-1234-1234/picture
You can see the uploaded image on the S3 bucket. Use this command to replace the bucket:
$ aws s3 ls YOUR_DOMAIN-profilepictures/uploads/
If you browse the S3 bucket and try to download the image, you can see that the image data is invalid. It is because the uploaded file has been treated as UTF-8 text. To tell API Gateway to treat specific Content-Type headers as a binary, we have to manually configure it. You can do it by navigating to API Gateway console, opening your API, clicking on Binary Support, and adding the desired content types. Here, we added image/png and image/jpeg as binary types, but you can add more according to your needs:
![](assets/0d14b05e-4e41-4ce7-a8b2-d4286afe3be3.png)
Unfortunately, this operation is not supported via CloudFormation; therefore, we have to do it manually.