REST API (Lambda integration) deployment using Terraform - POST is giving 200, but GET is giving 500 - api

I am trying to create a REST API with 2 methods GET and POST. Here is the work I have done:
Using Terraform to deploy AWS services.
Created 2 REST API methods - GET and POST.
Integrated API Gateway to AWS lambda. I created two lambdas, one for each method. The objective is to get "Hello World" as a response. The two lambdas (handlers) are identical in code.
The problem: After the deployment, it worked as expected (200) when I tried to hit POST. However, GET is giving a 500 error. This is what I see on the AWS console.
[When I am using type='MOCK' for GET]Execution failed due to configuration error: No match for output mapping and no default output mapping configured. Endpoint Response Status Code: 200
[When I am using type = 'AWS_PROXY' for GET] Lambda invocation failed with status: 403. Lambda request id: 5b23639d-f6fb-4130-acf0-15db9a2f76b0
Method completed with status: 500
Why is it that POST is working and GET is throwing an error? Any leads to rectifying this error or providing some explanation are appreciated.
Below is my Terraform configuration.
Note: I included only the "aws_api_gateway_*" resources. IAM roles, Lambdas, Lambda permissions are all good.
# Lambda handler - This is the same for both GET and POST. I used the same in both lambdas integrated to GET and POST.
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": json.dumps("Hello World")
}
################################################################################
# REST API and Resource
################################################################################
resource "aws_api_gateway_rest_api" "hello_world_v1" {
name = "hello_world_v1"
}
resource "aws_api_gateway_resource" "hello_world_v1" {
parent_id = aws_api_gateway_rest_api.hello_world_v1.root_resource_id
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
path_part = "test"
}
################################################################################
# GET - method and integration
################################################################################
resource "aws_api_gateway_method" "hello_world_v1_get" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
resource_id = aws_api_gateway_resource.hello_world_v1.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "hello_world_v1_get" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
resource_id = aws_api_gateway_method.hello_world_v1_get.resource_id
http_method = aws_api_gateway_method.hello_world_v1_get.http_method
integration_http_method = "GET"
type = "AWS_PROXY"
uri = aws_lambda_function.lambda_hello_world_v1_get.invoke_arn
# I initially didn't use this request template.
# I tried this after reading it somewhere while I was attempting to search for a solution.
# However,using or not using, didn't work out.
request_templates = {
"application/json" = jsonencode(
{
statusCode = 200
}
)
}
}
################################################################################
# POST - method and integration
################################################################################
resource "aws_api_gateway_method" "hello_world_v1_post" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
resource_id = aws_api_gateway_resource.hello_world_v1.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "hello_world_v1_post" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
resource_id = aws_api_gateway_method.hello_world_v1_post.resource_id
http_method = aws_api_gateway_method.hello_world_v1_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.lambda_hello_world_v1_post.invoke_arn
}
################################################################################
# Stage and Deployment
################################################################################
resource "aws_api_gateway_deployment" "hello_world_v1" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
depends_on = [
aws_api_gateway_method.hello_world_v1_get,
aws_api_gateway_method.hello_world_v1_post
]
}
resource "aws_api_gateway_stage" "hello_world_v1" {
deployment_id = aws_api_gateway_deployment.hello_world_v1.id
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
stage_name = "development"
}

According to the documentation:
integration_http_method - (Optional) The integration HTTP method (GET,
POST, PUT, DELETE, HEAD, OPTIONs, ANY, PATCH) specifying how API
Gateway will interact with the back end. Required if type is AWS,
AWS_PROXY, HTTP or HTTP_PROXY. Not all methods are compatible with all
AWS integrations. e.g., Lambda function can only be invoked via POST.
In a very simple way, the http_method attribute refers to the HTTP Method of your endpoint, the integration_http_method is the HTTP method of the call that API Gateway will do to invoke the Lambda function.
Just change it to POST:
resource "aws_api_gateway_integration" "hello_world_v1_get" {
rest_api_id = aws_api_gateway_rest_api.hello_world_v1.id
resource_id = aws_api_gateway_method.hello_world_v1_get.resource_id
http_method = aws_api_gateway_method.hello_world_v1_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.lambda_hello_world_v1_get.invoke_arn
# I initially didn't use this request template.
# I tried this after reading it somewhere while I was attempting to search for a solution.
# However,using or not using, didn't work out.
request_templates = {
"application/json" = jsonencode(
{
statusCode = 200
}
)
}
}

Related

Is it possible to convert HTTPBearer.credentials to Pydantic BaseModel in FastAPI?

I'm using FastAPI's HTTPBearer class to receive authorization tokens on request headers.
This is my dependencies file as per the FastAPI docs, which is used to retrieve the token on any request to the API.
classroom_auth_scheme = HTTPBearer(
scheme_name="Google Auth Credentials",
bearerFormat="Bearer",
description="O-Auth2 Credentials obtained on frontend, used to authenticate with Google services",
)
def get_classroom_token(
token: str = Depends(classroom_auth_scheme),
) -> requests.ClassroomAuthCredentials:
"""Converts a json string of Authorization Bearer token into ClassroomAuthCredentials class
Args:
token (str, optional): Autorization Header Bearer Token. Defaults to Depends(auth_scheme).
Raises:
HTTPException: 400 level response meaning the token was not in the correct format
Returns:
requests.ClassroomAuthCredentials
"""
try:
# token.credentials is a JSON String -> want: pydantic Basemodel
token_dict = json.loads(token.credentials)
token = requests.ClassroomAuthCredentials.parse_obj(token_dict)
return token
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"{e}",
)
And this is how I receive the token in my routes:
#router.post("/test-auth", summary="Validate authentication with Google Classroom API")
async def test_auth(token=Depends(get_classroom_token)):
try:
gc_service_test = get_service(token)
gc_api_test = ClassroomApi(service=gc_service_test)
user_profile = gc_api_test.get_user_profile("me")
response: responses.ListGoogleClassroomCourses = {
"message": f"Auth Credentials Are Valid",
"userProfile": user_profile,
}
return JSONResponse(response)
except errors.HttpError as error:
# handle exceptions...
Is there a way to specify the data structure of token.credentials like there is for request body?
This would make it easier to access properties as well as provide a format in the authorize modal on the swagger docs for other developers on the team, so they don't have to guess what the required properties of the Authorization token are.
This is my data model of the token.credentials as a BaseModel
class ClassroomAuthCredentials(BaseModel):
token: str = Field(..., example="MyJWT")
clientId: str = Field(..., example="myClientId")
clientSecret: str = Field(..., example="myClientSecret")
refreshToken: str = Field(..., example="myRefreshToken")
scopes: list[str] = Field(
...,
example=[
"https://www.googleapis.com/auth/classroom.courses.readonly",
"https://www.googleapis.com/auth/classroom.coursework.students",
],
)

Adding HTTP Security Headers Using Lambda#Edge and Amazon CloudFront for All routes (error routes)

I am using this doc https://aws.amazon.com/blogs/networking-and-content-delivery/adding-http-security-headers-using-lambdaedge-and-amazon-cloudfront/
I am using a react app in S3 bucket, with a cloudfront CDN. I have added a lambdaedge to add a security header
headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}];
headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}];
It is working fine for the homepage (mySite.com):
But it doesn't work for a different route, example mySite.com/login
When I check the error behavior in cloudFront, there are no options to add a header
Why this page /login is in error? Because of react router doesn't work in aws s3 bucket
The way that I solved this was to implement an origin request Lambda#Edge function that validates the event.Records[0].cf.request.uri value against a whitelist of known files and paths in the S3 origin. If the path doesn't match the whitelist, it updates the uri value to /index.html which allows the React app handle the request.
With this change, there will no longer be any 404 requests, which means the response will successfully pass through the origin response Lambda#Edge function which adds the response headers.
We solve this by using this feature called policy response headers in cloudfront. It add headers in all cases even in error response.
Doc: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html
We implemented it with terraform
resource "aws_cloudfront_response_headers_policy" "my_name" {
name = "policy-response-headers-my-name"
comment = "Add security headers to responses"
security_headers_config {
xss_protection {
mode_block = true
override = true
protection = true
}
frame_options {
frame_option = "DENY"
override = true
}
content_type_options {
override = true
}
content_security_policy {
content_security_policy = "frame-ancestors 'none'"
override = true
}
referrer_policy {
referrer_policy = "same-origin"
override = true
}
strict_transport_security {
access_control_max_age_sec = 63072000
override = true
}
origin_override = true
}
}

ses.sendmail() gives CORS error. 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'..credentials mode is 'include'

I have been stuck on this for 4 days now. I really need some insights.
I have a serverless express app deployed on AWS. I am serving my frontend from S3 and backend from lambda. API gateway has proxy as shown in the serverless.yml below.
I have also used cloudfront to map my domain(https://my.domain.com.au) with the S3 bucket origin URL.
The normal GET POST PUT DELETE requests are working fine. But when I try to access any of the other AWS service from Lambda I get following CORS error.
Access to XMLHttpRequest at 'https://0cn0ej4t5w.execute-api.ap-southeast-2.amazonaws.com/prod/api/auth/reset-password' from origin 'https://my.domain.com.au' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
My use case is to send a mail from my app for which I tried using.
ses.sendEmail(params).promise();
This gave me the same error. So i tried invoking it through lambda, same error. Now i am trying to push mail contents to S3 and send mail from lambda using trigger but this gave me the same error.
The issue doesn't seem to be on the code as its working perfect from local environment. However, i don't want to leave any stones unturned.
Since, my lambda is in a VPC i have used internet gateway and tried setting up the private link as well.
Serverless.yml
service: my-api
# plugins
plugins:
- serverless-webpack
- serverless-offline
- serverless-dotenv-plugin
# custom for secret inclusions
custom:
stage: ${opt:stage, self:provider.stage}
serverless-offline:
httpPort: 5000
webpack:
webpackConfig: ./webpack.config.js
includeModules: # enable auto-packing of external modules
forceInclude:
- mysql
- mysql2
- passport-jwt
- jsonwebtoken
- moment
- moment-timezone
- lodash
# provider
provider:
name: aws
runtime: nodejs12.x
# you can overwrite defaults here
stage: prod
region: ${env:AWS_REGION_APP}
timeout: 10
iamManagedPolicies:
- 'arn:aws:iam::777777777777777:policy/LambdaSESAccessPolicy'
vpc:
securityGroupIds:
- ${env:AWS_SUBNET_GROUP_ID}
subnetIds:
- ${env:AWS_SUBNET_ID1}
- ${env:AWS_SUBNET_ID2}
- ${env:AWS_SUBNET_ID3}
environment:
/// env variables (hidden)
iamRoleStatements:
- Effect: "Allow"
Action:
- s3:*
- ses:*
- lambda:*
Resource: '*'
# functions
functions:
app:
handler: server.handler
events:
- http:
path: /
method: ANY
- http:
path: /{proxy+}
method: ANY
cors:
origin: ${env:CORS_ORIGIN_URL}
allowCredentials: true
headers: 'Access-Control-Allow-Origin, Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization'
method: ANY
# you can add CloudFormation resource templates here
resources:
# API Gateway Errors
- ${file(resources/api-gateway-errors.yml)}
# VPC Access for RDS
- ${file(resources/lambda-vpc-access.yml)}
I have configured response headers as well:
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", process.env.CORS_ORIGIN_URL);
res.header("Access-Control-Allow-Headers", "Access-Control-Allow-Origin, Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT,DELETE");
next();
});
I actually have the same exact error as you but I've figured it out.
I'll just paste my code since you didn't show what your lambda function looks like.
I also know its been two weeks... so hopefully this helps someone in the future.
CORS errors are server side, and I'm sure you are aware. The problem with AWS SES is you have to handle the lambda correctly or it'll give you a cors error even though you have the right headers.
First things first... I don't think you have OPTIONS method in your api gateway...although I'm not sure if ANY can work as a replacement.
Second here is my code:
I check which http method I'm getting then I respond based on that. I am receiving a post event and some details come in the body. You might want to change the finally block to something else. The OPTIONS is important for the CORS, it lets the browser know that its okay to send the POST request (or at least that's how I see it)
var ses = new AWS.SES();
var RECEIVER = 'receiver#gmail.com';
var SENDER = 'sender#gmail.com';
exports.handler = async(event) => {
let body;
let statusCode = '200';
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,DELETE,POST,PATCH,OPTIONS',
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Headers': 'access-control-allow-credentials,access-control-allow-headers,access-control-allow-methods,Access-Control-Allow-Origin,authorization,content-type',
'Content-Type': 'application/json'
};
console.log(event);
try {
switch (event.httpMethod) {
case 'POST':
event = JSON.parse(event.body);
var params = {
Destination: {
ToAddresses: [
RECEIVER
]
},
Message: {
Body: {
Html: {
Data: html(event.name, event.phone, event.email, event.message), // 'Name: ' + event.name + '\nPhone: ' + event.phone + '\nEmail: ' + event.email + '\n\nMessage:\n' + event.message,
Charset: 'UTF-8'
}
},
Subject: {
Data: 'You Have a Message From ' + event.name,
Charset: 'UTF-8'
}
},
Source: SENDER
};
await ses.sendEmail(params).promise();
break;
case 'OPTIONS':
statusCode = '200';
body = "OK";
break;
default:
throw new Error(`Unsupported method "${event.httpMethod}"`);
}
}
catch (err) {
statusCode = '400';
body = err.message;
}
finally {
body = "{\"result\": \"Success\"}"
}
console.log({
statusCode,
body,
headers,
})
return {
statusCode,
body,
headers,
};
}

TRON not using request.header

I am using Xcode 8.3.3 (8E3004b)
I am using TRON (which includes Alamofire) to make HTTP Request to my REST API.
I have been successful getting a simple API working with this setup. I am trying to connect to a different API, which requires me to set the headers. It is this API that is throwing a Status 415 server error.
I have the following code to make the request via TRON. According to the TRON Github page, I should be ae to set the header like this:
request.headers = ["Content-Type":"application/json"]
I have also tried:
request.headerBuilder.headers(forAuthorizationRequirement: AuthorizationRequirement.allowed, including: ["Content-Type":"application/json"])
I tried adding a few different ways of writing that, but nothing seems to work.
Here's a bigger section of the code so you can see the context
let urlSubfix = "\(Constant.REST_MOBILE)\(Constant.REGISTER)"
let request: APIRequest<RegisterApiResult, JSONError> = tron.request(urlSubfix)
request.method = .put
// request.headers = ["Content-Type":"application/json"]
let header = request.headerBuilder.headers(forAuthorizationRequirement: AuthorizationRequirement.allowed, including: ["Content-Type":"application/json"])
request.headers = header
request.perform(withSuccess: { (registerApiResult) in
print("Successfully fetched our json object")
completion(registerApiResult)
}) { (err) in
print("Failed to fetch json...", err)
}
Here is the actual error from my log:
Failed to fetch json... APIError<JSONError>(request: Optional(http://www.slsdist.com/eslsd5/rest/mobileservice/register), response: Optional(<NSHTTPURLResponse: 0x618000028c20> { URL: http://www.slsdist.com/eslsd5/rest/mobileservice/register } { status code: 415, headers {
"Content-Length" = 0;
Date = "Sat, 22 Jul 2017 22:23:14 GMT";
Server = "Microsoft-IIS/7.5";
"X-Powered-By" = "Undertow/1, ASP.NET";
} }), data: Optional(0 bytes), error: Optional(Alamofire.AFError.responseValidationFailed(Alamofire.AFError.ResponseValidationFailureReason.unacceptableStatusCode(415))), errorModel: Optional(Go_Cart.Service.JSONError))
As you can see I have tried to set the headers a couple different ways, but neither of them seems to take affect. Any help or advice from anyone would be helpful.
Thanks in advance.

Akka authenticateOAuth2Async: credentials missing

I have the following:
def myUserPassAuthenticator(credentials: Credentials): Future[Option[String]] = {
log.info(credentials.toString)
credentials match {
case p#Credentials.Provided(id) if p.verify("a") =>
log.info("Login success!")
Future.successful(Some(id))
case _ =>
log.info("Login failure!")
Future.successful(None)
}
}
val authRoute = path("login") {
authenticateOAuth2Async(realm = "secure site", myUserPassAuthenticator) { userName =>
complete(s"The user is '$userName'")
}
}
when navigating to that endpoint and entering credentials, the log line
log.info(credentials.toString)
just becomes Missing. What is wrong here?
The content-type of the request is "application/x-www-form-urlencoded"
and the data is "grant_type=password&username=INSERT_USERNAME_HERE&password=INSERT_PWD_HERE"
You should wrap your route in Route.seal directive:
val authRoute =
Route.seal {
path("login") {
authenticateOAuth2Async(realm = "secure site", myUserPassAuthenticator) { userName =>
complete(s"The user is '$userName'")
}
}
}
from documentation it seems to rely on default rejection 401 if authentication fails
Given a function returning Some[T] upon successful authentication and
None otherwise, respectively applies the inner route or rejects the
request with a AuthenticationFailedRejection rejection, which by
default is mapped to an 401 Unauthorized response.
and the Route.seal provides exactly that:
A Route can be "sealed" using Route.seal, which relies on the in-scope
RejectionHandler and ExceptionHandler instances to convert rejections
and exceptions into appropriate HTTP responses for the client.