Im using Serverless framework to deploy a set of API's running on API Gateway using cognito as authorizer. Everything seemed to work but i found an issue that when lambda crashes for some reason (maybe time out or some unhandled exception), API Gateway returns 401 Unauthorized.
I added some gateway responses and I can handle some of the errors but even if I get the initial error, I keep receiving the 401 Unauthorized in Frontend.
This is part of my serverless.yml file:
getSimulationStatus:
handler: getSimulationStatus.handler
events:
- http:
path: /simulation/status
method: post
cors: true
authorizer:
arn: arn:aws:cognito-idp:us-east-1:${self:custom.settings.COGNITO_ARN}
resources:
Resources:
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
gatewayresponse.header.Access-Control-Allow-Methods: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
gatewayresponse.header.Access-Control-Allow-Methods: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
For the frontend im using Angular and I have an error interceptor that captures all this events. Currently im forcing a 504 Request time out and Im able to see that, but also the 401 Unauthorized appears.
This is the code for the interceptor:
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
console.log("error captured", err);
return throwError(err.error.message);
}));
}
The idea is to sign out if I receive 401 from backend but currently if any of my lambdas fails, Im immediately logged out.
Any Idea on what could be the problem?
Edit:
This is a capture from web console:
I have a better solution.Why you don't verify if the token it's valid before send the request to api gateway?
you can install this:
npm i jwt-decode
In my case, I always have a fuction that return me the headers and that allows me to validate in that function the token on every call to the API.
Here a function that validate the token (you only need to pass the token of cognito if you are ussing amplify or something like that).
import jwt_decode from "jwt-decode";
....
some code
....
tokenValidator(){
if(localStorage.getItem('access_token') == null){
//call signout cognito and go to login
}
const decoded = jwt_decode(localStorage.getItem('access_token'));
if (decoded.exp === undefined) return null;
const date = new Date(0);
date.setUTCSeconds(decoded.exp);
if(new Date() > date){
//call signout cognito and go to login
}
}
This solution prevents your problem.
Related
I have an OpenAPI spec for an api which I am deploying via CDK. The spec looks like:
openapi: 3.0.1
info:
title: My API
description: My REST API with CORS enabled
version: 0.1.0
x-amazon-apigateway-cors:
allowOrigins:
- "*"
allowCredentials: true
exposeHeaders:
- "x-apigateway-header"
- "x-amz-date"
- "content-type"
maxAge: 3600
allowMethods:
- "*"
allowHeaders":
- "x-apigateway-header"
- "x-amz-date"
- "content-type"
- "Authorization"
components:
securitySchemes:
lambda:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "custom"
x-amazon-apigateway-authorizer:
authorizerUri: "{{my-lambda-authorizer}}"
authorizerResultTtlInSeconds: 300
type: "token"
paths:
/user/{id}:
get:
summary: Get info of specified user.
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
security:
- lambda: []
x-amazon-apigateway-integration:
uri: "{{my-lambda}}"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws_proxy"
When I try to access this via fetch(), I get an error Failed to load resource: Origin http://localhost:8000 is not allowed by Access-Control-Allow-Origin.
fetch('https://api.example.com/user/1')
.then(response => response.json())
.then((user: User) => {
// do something
})
.catch((err) => {
console.log("Error: " + err);
});
The API is accessible at api.example.com and I am running the website locally using Gatsby at localhost:8000.
The AWS docs seem to state that CORS is enabled when I put x-amazon-apigateway-cors at the root of the spec, but CORS doesn't seem to be enabled (or working) when I try to access the API. How do I enable CORS for my API without needing to configure it in the console?
Amazon API Gateway offers two types of APIs: REST APIs and HTTP APIs. REST APIs were the kind of APIs originally introduced with Amazon API Gateway, while HTTP APIs got announced at the end of 2019.
Based on the description of your OpenAPI specification I assume you're trying to deploy a REST API. The x-amazon-apigateway-cors OpenAPI extension however only works for HTTP APIs.
You have two choices now: You either switch to use a HTTP API or you configure CORS manually. As long as you don't need features only supported by REST APIs, I suggest you switch to use a HTTP API, as that's the more modern kind of API Amazon API Gateway offers.
If you want to use a REST API, enabling CORS requires more manual configuration. That's documented by AWS in Enabling CORS for a REST API resource. Essentially you have to ensure your integration returns proper CORS headers. For a NodeJS AWS Lambda function that could look like:
exports.handler = async (event) => {
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Origin": "https://www.example.com",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET"
},
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
For CORS pre-flight requests to work you'd also have to ensure that OPTIONS-requests also return the correct headers. The easiest way to achieve that is to add a mock-integration for the OPTIONS-request to your OpenAPI specification. That would look like:
options:
responses:
'200':
description: Default response
headers:
Access-Control-Allow-Headers:
schema:
type: string
Access-Control-Allow-Methods:
schema:
type: string
Access-Control-Allow-Origin:
schema:
type: string
x-amazon-apigateway-integration:
type: mock
requestTemplates:
application/json: |
{"statusCode" : 200}
responses:
default:
statusCode: 200
responseParameters:
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
method.response.header.Access-Control-Allow-Origin: "'https://example.com/'"
Please also mind that modern browsers don't support localhost as origin for CORS, so you might need to work around that as well.
I have an aws ApiGateway which verify my token and pass request to lambda.
When I have an error from lambda, APIGateway response is
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "..."
}
But if I don't pass my token, then APIGateway will return me
{
"message": "Unauthorized"
}
And in postman I have statusCode: 401.
How I want it to be:
{
"statusCode": 401,
"error": "Unauthorized"
}
I use serverless.yml to deploy:
functions:
index:
handler: dist/index.handler
events:
- http:
cors: true
path: '/'
method: any
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewayAuthorizer
Please, tell me how I have to change my serverless.yml to change the 'Unauthorized' error to be as at third code example.
try to implement this:
https://github.com/SeptiyanAndika/serverless-custom-authorizer:
Allows to get reponses like:
{
"success":false,
"message":"Custom Deny Message"
}
Adding to #svladimirrc, it will work even if you don't have a custom authorizer in place, just make sure you have the proper name of your API Gateway configured to link to:
resources:
Resources:
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ${self:provider.stage}-${self:service}
InvalidApiKeyGatewayResponse:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
RestApiId:
Ref: 'ApiGatewayRestApi'
ResponseType: INVALID_API_KEY
ResponseTemplates:
application/json: "{\"success\":false,\"message\":\"Invalid API key\"}"
StatusCode: '401'
You can achieve this by modifying Gateway Responses.
Go to API Gateway in AWS Management Console.
Select your API.
Click "Gateway Responses" which can be seen on the left side.
Select "Unauthorized" in the list of Gateway Responses.
Select "application/json" in Response Templates and click "Edit".
Update the response template body based on your requirements.
Click "Save".
Re-deploy your API.
I have a Lambda proxy integration with API Gateway that is working fine.
CORS is handled directly in the lambda code with checks against lists of authorized domains.
But the issue now is with unexpected errors during Lambda execution.
API Gateway returns the following message in such a case:
{
message: "Internal server error"
}
with a 502 HTTP status code. Unfortunately for me, the Access-Control-Allow-Origin header is missing in that response, which is causing errors on client side.
The same happens also with timeouts for example. The HTTP status code is then 504 but the response content and the lack of Access-Control-Allow-Origin is the same.
The same issue occurs also in case of permission issue: if the API Gateway does not have sufficient permissions to call the Lambda, then a 500 error is returned but, once again, without any header.
A fixed value of '*' would be OK in the case of Lambda errors but how and where can this be configured?
Late to the game, but you can add these to your SAM / CloudFormation template to fix this:
Resources:
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'
Taken from here: [https://serverless-stack.com/chapters/handle-api-gateway-cors-errors.html#create-a-resource][1]
Add in your handler function this callback with these values, you can add your proper response in the body
callback(null, {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({ "response": true })
});
I am trying to do an http request from an angularjs app to a lambda function that I had setup using serverless.
Here is my serverless.yaml function
functions:
createcustomer:
handler: handler.createcustomer
events:
- http: post /createcustomer
cors: true
Create Customer Function
module.exports.createcustomer = (event, context, callback) => {
let customer = JSON.parse(event.body).customer;
service.create(customer, function(result) {
let response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
body: JSON.stringify({
result: 'Created Customer Successfully',
message: 'The account has been created!',
type: 'success',
customer: result
})
};
callback(null, response);
});
};
From my AngularJS app I call it like this
app.factory('MyFactory', ['$http', function($http) {
return {
CreateCustomer: function(customer) {$http.post('<apipath>/createcustomer', {customer:customer})}
}
}]);
However, I keep getting this error:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:5000' is therefore not allowed access. The response had HTTP status code 403.
I have tried to enable CORS in the API Gateway on the POST method, but that did not change the outcome.
I've also tried setting CORS in the yaml file explicitly
functions:
createcustomer:
handler: handler.createcustomer
events:
- http: post /createcustomer
cors:
origin: '*'
Still no luck.
Does anyone know what I'm doing wrong here?
One weird thing is that I could get the post to work just fine through PostMan, but if I try it through my app it breaks.
Thanks
UPDATE
When I do serverless deploy it shows up in AWS like the picture above and the method looks like this
As I said before, I tried to enable CORS directly from the API Gateway console but there was no difference when I tried to call the method.
Your update with screenshots shows that the OPTIONS method is not set up for any of these resources. When you enable CORS for an API Gateway resource in the console, AWS sets this up automatically.
You can see this happen in the AWS console when you enable CORS for a resource, but, of course, your serverless deploy is overwriting this configuration.
To have the /createcustomer resource properly configured in AWS by the serverless deploy, you can rewrite this part of your serverless.yaml:
events:
- http: post /createcustomer
cors: true
To look like this:
events:
- http:
path: /createcustomer
method: post
cors: true
I'm not an expert in the framework's .yml syntax, so I can't explain exactly why this is.
Nonetheless, I've confirmed that a file like this:
functions:
deletecustomer:
handler: handler.deletecustomer
events:
- http:
path: /deletecustomer
method: post
cors: true
createcustomer:
handler: handler.createcustomer
events:
- http: post /createcustomer
cors: true
will create two resources in AWS API Gateway, one correctly configured for CORS, and one missing the OPTIONS method:
Here is the configuration that could help. Please note that it's always safe to be specific in allowing the CORS origins. So better to lock the origin down to localhost:{port-number}. In addition, you can also enable credentials on CORS settings. Please see the following serverless config as an example:
cors:
origin: 'http://localhost:4200'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
allowCredentials: true
The cors configuration got moved up into the provider config:
provider:
httpApi:
cors: true
In order to prevent users who have not logged in to call my lambda function through the AWS API Gateway, I'm using the Custom Authorizer lambda solution.
If the request is authorized (200) and I get a response from the called lambda everything works fine and I get the Access-Control-Allow-Origin header.
But if the request is not authorized, I get a 401 that has no Access-Control-Allow-Origin header, therefore preventing me from reading the 401 status of the response and redirecting the user to the log-in page.
I believe this is because the Custom Autorization mechanism is unaware that the request needs to use CORS. Does anyone know that this is actually the issue? Are you aware of any possible solution?
I'm happy to announce the new Gateway Responses feature which allows you to customize the error responses for requests that don't call your integration. This allows you to ensure that CORS headers are included, even on failed auth requests.
Read more in our documentation, which includes a CORS example.
Yes, this is a known bug with API Gateway custom authorizers. Thanks for bringing this to our attention. The team will update this post when we've deployed a fix. Apologies for the inconvenience.
The easiest way to resolve this for all 4XX error (including 401 errors) is to go to "Gateway Responses" and then select "Default 4XX" and then add the the header "Access-Control-Allow-Origin" with the value '*'.
See screenshot:
This works for me (inline in AWS::APIGateway: definition)
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Dev
GatewayResponses:
UNAUTHORIZED:
StatusCode: 401
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
ACCESS_DENIED:
StatusCode: 403
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
DEFAULT_5XX:
StatusCode: 500
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
RESOURCE_NOT_FOUND:
StatusCode: 404
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"
The available GatewayResponses Naming are:
DEFAULT_INTERNAL
DEFAULT_4XX
DEFAULT_5XX
RESOURCE_NOT_FOUND
UNAUTHORIZED
ACCESS_DENIED
AUTHORIZER_FAILURE
AUTHORIZER_CONFIGURATION_ERROR
MISSING_AUTHENTICATION_TOKEN
INVALID_SIGNATURE
EXPIRED_TOKEN
INTEGRATION_FAILURE
INTEGRATION_TIMEOUT
API_CONFIGURATION_ERROR
UNSUPPORTED_MEDIA_TYPE
REQUEST_TOO_LARGE
BAD_REQUEST_PARAMETERS
BAD_REQUEST_BODY
THROTTLED
QUOTA_EXCEEDED
INVALID_API_KEY
WAF_FILTERED
So you could specify the Response customization for these Controlled AWS responses.
Because it took me a while to figure out how to put it all together in Cloud Formation, here is a snippet showing how to set it up.
...
MyApi:
Type: "AWS::ApiGateway::MyApi"
Properties:
Description: My API
Name: "my-api"
MyApiAuthorizer:
Type: "AWS::ApiGateway::Authorizer"
Properties:
Name: "my-api-authorizer"
IdentitySource: "method.request.header.Authorization"
ProviderARNs:
- !GetAtt MyUserPool.Arn
RestApiId: !Ref MyAApi
Type: COGNITO_USER_POOLS
MyApiGatewayResponse:
Type: "AWS::ApiGateway::GatewayResponse"
Properties:
ResponseParameters:
"gatewayresponse.header.Access-Control-Allow-Origin": "'*'"
"gatewayresponse.header.Access-Control-Allow-Headers": "'*'"
ResponseType: UNAUTHORIZED
RestApiId: !Ref MyApi
StatusCode: "401"
If, like me, you are running into problems with API Gateway V2, specifically with an HTTP API - the ANY method doesn't seem to work with the plug and play CORS offering. I had to individually create a route for each method (annoying because they all call the same lambda function, but oh well).
Adding to the answers above, if you're not using Cloudformation/SAM template, you can save yourself some manual steps using this python script:
import boto3
import sys
if len(sys.argv) != 3:
print("usage: python script.py <API_ID> <STAGE>")
exit()
client = boto3.client('apigateway')
response = client.put_gateway_response(
restApiId=sys.argv[1],
responseType='UNAUTHORIZED',
statusCode='401',
responseParameters={
"gatewayresponse.header.Access-Control-Allow-Origin": "'*'",
"gatewayresponse.header.Access-Control-Allow-Headers": "'*'"
}
)
response = client.create_deployment(
restApiId=sys.argv[1],
stageName=sys.argv[2])
To fix this for our SPA (we are using AWS Cognito authorizer) we added the next Response Headers in DEFAULT 4xxx and DEFAULT 5xxx Gateway responses:
Access-Control-Allow-Origin '{url_of_your_front-end}'
Access-Control-Allow-Headers '{url_of_your_front-end}'
we set {url_of_your_front-end} instead of '*', because the browser didn't like it :D
as an additional we set Access-Control-Allow-Credentials 'true' header to make a browser happy.
Picture with all headers
Adding the response header "Access-Control-Allow-Origin" for the Unauthorized response should fix this issue
Select the Api
Select the Gateway Responses
Select Unauthorized
Edit the response header and add Access-Control-Allow-Origin with value "*"