I'm build an API using AWS SAM (Lambda & API Gateway) whose contract is defined by a 3rd party.
The 3rd party calls my API with a GET request that contains JSON in the body. However, when a request is sent to the API with a body it gets rejected by CloudFront.
This is the request:
curl -X GET -H "Content-Type: application/json" --data '{"hello":"world"}' https://my-api.execute-api.us-east-2.amazonaws.com/Prod/my-api
This is the response:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD>
<BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Bad request.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: 1p0St_-e3noQL-2uMxeB_2I6lkMr1mg5afvxJRmVpCdnG67Vgnhj9w==
</PRE>
<ADDRESS></ADDRESS>
</BODY>
</HTML>
Checking the logs, the request never hits API Gateway or the Lambda function. However, if I remove the body from the request, then it hits the Lambda function and I get the appropriate error message from the API (telling the caller that the expected body is missing.)
curl -X GET -H "Content-Type: application/json" https://my-api.execute-api.us-east-2.amazonaws.com/Prod/my-api
I'm using basic configuration of API Gateway via a SAM template. This is the relevant section:
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: bin/main.zip
Handler: main
Runtime: go1.x
Tracing: Active
Role: !Sub ${MyApiLambdaExecutorRole.Arn}
Events:
CatchAll:
Type: Api
Properties:
Path: /my-api
Method: GET
GET requests cannot contain a request body on CloudFront. Try using POST instead.
If you want to send limited data in a GET request, you can use query parameters.
You can see in the AWS Docs that this isn't possible here:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#RequestCustom-get-body
If a viewer GET request includes a body, CloudFront returns an HTTP status code 403 (Forbidden) to the viewer.
You could perhaps use an EC2 instance or other service that doesn't use API Gateway to handle the request.
Related
I'm trying to use the token provided by AWS Cognito to access a URL via Postman or cURL, but I'm failing to.
I have used the CloudFormation template bellow to create an API with a JWT authentication.
https://github.com/awsdocs/amazon-api-gateway-developer-guide/blob/main/cloudformation-templates/HTTP/http-with-jwt-auth.yaml
After signing-in, I can access the lambda function using the returned URL and access_token. This works just as expected:
http://<api_url>/?access_token=<token>
But when I try to access it from Postman or cURL using the access_token in the header, it outputs a 401. I was expecting to have access granted.
$ curl -v -X GET <url> -H "Authorization: <token>"
{"message":"Unauthorized"}
What have I tried:
I have tried to add 'Content-Type: application/json', but still get 401.
I have tried to use Authorization: Bearer <token>, but still get 401.
This template only return the access_token, but another stack I have also returns the id_token, and a 401 is returned for both
The
complete returned header is:
HTTP/2 401
date: Thu, 03 Mar 2022 20:12:58 GMT
content-type: application/json
content-length: 26
www-authenticate: Bearer
apigw-requestid: ObIjqhmPIAMEJtA=
* Connection #0 to host <url> left intact
{"message":"Unauthorized"}
The JWT Authorizer is configured as:
JWTAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref MyAPI
AuthorizerType: JWT
IdentitySource:
- '$request.querystring.access_token'
JwtConfiguration:
Audience:
- !Ref AppClient
Issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}
Name: test-jwt-authorizer
The IdentitySource must be '$request.header.Authorization' in order for it to read from header.Authorization.
I have two services (APIs) deployed on GCP Cloud Run. Call them service-one.myDomain.com and service-two.myDomain.com. I would like service-one to be authenticated in calling service-two independently of what any user is doing.
I've read and implemented the instructions from GCP Cloud Run docs on Authenticating service-to-service (https://cloud.google.com/run/docs/authenticating/service-to-service) but service-one.myDomain.com is unsuccessful in calling service-two.myDomain.com receiving a 401:Unauthorized response.
Any thoughts on how to get service-one to successfully call service-two?
Here's my setup:
IAM and service accounts:
On google IAM, I created two service accounts and granted them both the "Cloud Run Invoker" (roles/run.invoker) role:
service-one#myproject.iam.gserviceaccount.com
service-two#myproject.iam.gserviceaccount.com
Inside Cloud Run I changed the service account from the "Default compute service account" to the service accounts I created. I assigned service-one#myproject.iam.gserviceaccount.com for service-one.myDomain.com and service-two#myproject.iam.gserviceaccount.com for service-two.myDomain.com
OIDC Auth token:
In service-one.myDomain.com I make a call to the metadata server to get a token (jwt) from the following url:
http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://service-two.myDomain.com with a request header set as {'Metadata-Flavor': 'Google'} The request is successful and the token I receive is decoded to have the following payload:
{
"alg": "RS256",
"kid": "9cef5340642b157fa8a4f0d874fe7543872d82db",
"typ": "JWT"
}
{
"aud": "https://service-two.mydomain.com",
"azp": "100959068407876085761",
"email": "service-one#myproject.iam.gserviceaccount.com",
"email_verified": true,
"exp": 1572806540,
"iat": 1572802940,
"iss": "https://accounts.google.com",
"sub": "100953168404568085761"
}
Http request:
Using the token I make a request from service-one.myDomain.com to an http endpoint on service-two.myDomain.com. I set the request header with {'Authorization': 'Bearer {token}'} ({token} is value of token).
Http Response:
The response is a 401 Unauthorized and my logs show the response headers to include:
{'WWW-Authenticate': 'Bearer error="invalid_token" error_description="The access token could not be verified"'}
With a content of:
"
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/health</code>.</h2>
<h2></h2>
</body></html>
"
I'm stumped.... any ideas on what I'm missing to get service-one to authenticate to service-two?
The answer was to use the gcp cloud run generated Url as the audience in the OIDC token request. And relatedly the "aud" field in the jwt.
My discovery was that Service-to-Service authentication in cloud run does not support custom domains (myDomain.com). I was using my custom domain.
(I feel like a bonehead) thanks #guillaumeblaquiere
I built a sample container(sample of Go, https://cloud.google.com/run/docs/quickstarts/build-and-deploy) and deployed to cloud run (I unchecked "allow unauthenticated invocations").
However, when I open the endpoint URL of my service, I get a 401 page,
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/</code>.</h2>
<h2></h2>
</body></html>
According to the official document (https://cloud.google.com/run/docs/securing/authenticating#developers), I used this command.
curl -H "Authorization: Bearer $(gcloud config config-helper --format 'value(credential.id_token)')" [SERVICE_URL]
My user account has "roles/run.invoker"
When I checked "allow unauthenticated invocations", I got an expected result.
Is there anything to open the endpoint?
Please make sure gcloud version is at least 243.0.0 by "gcloud --version"
Please use "gcloud components update" to update if gcloud is too old
[Updated] Using gcloud in Cloud Shell to call authentication-required Cloud Run service is also working now.
Thanks!
I managed to connect from a cloud function, finally, after 15-minute wrestling with the same 401 error when invoking a cloud run service that does not allow unauthenticated invocations:
import google.auth
auth_req = google.auth.transport.requests.Request()
id_token = google.oauth2.id_token.fetch_id_token(auth_req, URL)
headers = {'Authorization': 'Bearer ' + id_token,
"Content-Type": "application/json; charset=utf-8"}
response = requests.post(URL, json=request_json, headers=headers)
Below is the request the client makes. The GET request is a success, but the POST request gives an error.
fetch('api/public/libraries/sign-out-discourse', {
method: 'GET', // or 'POST'
headers: new Headers([
['Accept', 'application/json'],
['Content-Type', 'application/json'],
['Authorization', jwtToken],
]),
})
Here is the error from the POST request:
HTTP/1.1 403 Forbidden
Server: CloudFront
Date: Fri, 05 Oct 2018 08:50:14 GMT
Content-Type: text/html
Content-Length: 694
Connection: keep-alive
X-Cache: Error from cloudfront
Via: redacted
X-Amz-Cf-Id: redacted
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
This distribution is not configured to allow the HTTP request method that was used for this request. The distribution supports only cachable requests.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: redacted
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>
What I have tried
In CloudFront I have chosen behaviors -> edit -> Allowed HTTP Methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE. I have invalidated caches since making this chance in the unlikely case that AWS stores settings in the CloudFront cache somehow.
In API Gateway graphical, I have enabled CORS for the resource. I have redeployed the API since making this change.
Other debugging
curl -X POST API_ENDPOINT returns the same error as calling the endpoint with POST from my application.
If you are using Lambda Proxy integration, you need to provide the CORS header in the response, API Gateway will not do that for you for a POST request.
var response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin" : "*",
"Access-Control-Allow-Credentials" : true
},
body: JSON.stringify({"message":"Success"})
}
callback(null, response);
I am learning AWS API Gateway to manage the api in the cloud and not through express.js. I created the method DELETE under a resource with the following Integration Request.
Integration Request
Integration Type: AWS Service
AWS Region: us-east-1
AWS Service: DynamoDB
HTTP method: DELETE
Action: deleteItem
Execution role: XXXXXX
Content Handling: Passthrough
the execution role is working because i can read/write to DynamoDB. a Body Mapping Template that has a Content-Type: application/json as follows:
{
"TableName": "comments",
"Key": {
"commentId": {
"S": "$context.requestId"
},
"pageId": {
"S": "$input.path('$.pageId')"
},
"userName": {
"S": "$input.path('$.userName')"
}
}
}
when i try to run the test with a Request Body as follows:
{
"pageId": "breaking-news-story-01-18-2016",
"userName": "cacevedo"
}
I get the following information:
Request: /comments
Status: 200
Latency: 80 ms
Response Body
Response Body:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html
xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head>
<title>Page Not Found</title> </head> <body>Page Not Found</body>
</html>
Can someone help me out with this, because i am not able to delete the item. yet it seems that the integration request is working. Am i missing something or doing something wrong?
Just in case anyone runs into the same problem. the HTTP verb needs to be a post and not delete and also is case sensitive so instead of using deleteItem. use DeleteItem