AWS SAM HttpApi - internal server error when adding lambda auth - amazon-web-services

I've declared an HttpApi and a simple hello world function using AWS SAM template and it's working, I'm trying to add a lambda request authorizer now but I'm getting internal server error when testing it using Postman.
If I remove the required header "tokenID" the response is
{
"message": "Unauthorized"
}
if I add it:
{
"message": "Internal Server Error"
}
also, the access logs headers are empty, for example: BVharit1liAEMrw=: (-) -
Template:
Parameters:
StageName:
Type: String
Default: Prod
Resources:
HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello-world
Handler: hello-world/app.lambdaHandler
Runtime: nodejs14.x
Events:
ExplicitApi:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Method: GET
Path: /
TimeoutInMillis: 15000
PayloadFormatVersion: "2.0"
AuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: Auth
Handler: Auth/auth.lambdaHandler
Runtime: nodejs14.x
AccessLogs:
Type: AWS::Logs::LogGroup
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowHeaders:
- tokenID
AllowMethods:
- GET
AllowOrigins:
- '*'
Auth:
DefaultAuthorizer: 'FirebaseAuth'
Authorizers:
FirebaseAuth:
FunctionInvokeRole: !GetAtt AuthFunctionRole.Arn
EnableSimpleResponses: true
AuthorizerPayloadFormatVersion: 2.0
FunctionArn: !GetAtt AuthFunction.Arn
Identity:
Headers:
- tokenID
StageName: !Ref StageName
AccessLogSettings:
DestinationArn: !GetAtt AccessLogs.Arn
Format: '$context.extendedRequestId: ($context.integrationErrorMessage) $event.headers'
Lambda request authorizer:
exports.lambdaHandler = async (event, context) => {
return {
"isAuthorized": true
}
}

Your lambda authorizer is not returning what is expected to be an actual lambda authorizer (an IAM policy). This is why you get that internal error when you turn it on.
To fix, replace is with something like this that returns an IAM policy (or rejects):
// A simple token-based authorizer example to demonstrate how to use an authorization token
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke
// the request if the token value is 'deny'. If the token value is 'unauthorized' or an empty
// string, the authorizer function returns an HTTP 401 status code. For any other token value,
// the authorizer returns an HTTP 500 status code.
// Note that token values are case-sensitive.
exports.handler = function(event, context, callback) {
var token = event.authorizationToken;
// modify switch statement here to your needs
switch (token) {
case 'allow':
callback(null, generatePolicy('user', 'Allow', event.methodArn));
break;
case 'deny':
callback(null, generatePolicy('user', 'Deny', event.methodArn));
break;
case 'unauthorized':
callback("Unauthorized"); // Return a 401 Unauthorized response
break;
default:
callback("Error: Invalid token"); // Return a 500 Invalid token response
}
};
// Help function to generate an IAM policy
var generatePolicy = function(principalId, effect, resource) {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
// Optional output with custom properties of the String, Number or Boolean type.
authResponse.context = {
"stringKey": "stringval",
"numberKey": 123,
"booleanKey": true
};
return authResponse;
}
Lots more information here : https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-lambda-function-create

Related

How to allow API Gateway to return a 405 response

Hello AWS Cloud Gurus,
I am trying to allow my REST API to return a 405 when an unsupported HTTP verb is used on any resource.
I see there are ways to define GatewayResponses.
However, I don't see any obvious approach to return a 405 (other than to define it as the DEFAULT_4XX which seems incorrect)
ExampleApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
OpenApiVersion: '3.0.1'
GatewayResponses:
DEFAULT_4XX:
StatusCode: 405
ResponseTemplates:
"application/*": '{ "message": "Method Not Allowed" }'
Does anyone know how to do this?
One solution is to create a lambda function, attached to the API, to handle a specific endpoint which needs to indicate 405
ExampleApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
OpenApiVersion: '3.0.1'
MethodNotAllowedResponse:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs14.x
Handler: index.handler
InlineCode: |
let response;
exports.handler = (event, context, callback) => {
response = {
"statusCode": 405,
"headers": {
"Content-Type": "application/problem+json"
},
"body": JSON.stringify({
"type": "https://tools.ietf.org/html/rfc7231#section-6",
"status": 405,
"title": "Method Not Allowed",
"detail": `Method ${event.httpMethod} is not allowed on ${event.path}`
})
}
callback(null, response);
}
Events:
Televisions:
Type: Api
Properties:
Auth:
Authorizer: NONE
RestApiId: !Ref ExampleApi
Path: '/not/allowed/path'
Method: patch
This can be implemented as a mock integration that you then use for any methods that are not implemented/supported.
Integration request mapping
{
"statusCode": 405,
"message": "The invoked method is not supported on the API resource."
}

How to use AWS SAM CLI Local HttpAPI with JWT Bearer token Auth offline?

I would like to use AWS SAM JWT HttpApi Auth offline
Based on this AWS example, I decided to create the following YAML file.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs10.x
Events:
ExplicitApi: # warning: creates a public endpoint
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Method: GET
Path: /path
TimeoutInMillis: 15000
PayloadFormatVersion: "2.0"
RouteSettings:
ThrottlingBurstLimit: 600
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
FailOnWarnings: True
Auth:
Authorizers:
MyOauthAuthorizer:
IdentitySource: $request.header.Authorization
JwtConfiguration:
audience:
- audience
issuer: issuer-url
DefaultAuthorizer: MyOauthAuthorizer
Using AWS::Serverless:HttpApi based on docs creates an Amazon API Gateway HTTP API which supports JWT based auth.
I start it with
sam local start-api
However, when I query it with Postman, with or without JWT Bearer token, the request succeeds.
And the AWS query does not contain a single authenticated user object.
Running it with Debug mode does not provide any useful additional information either.
let response;
exports.lambdaHandler = async (event, context) => {
try {
// const ret = await axios(url);
response = {
statusCode: 200,
body: JSON.stringify({
message: "hello world",
event,
context,
// location: ret.data.trim()
}),
};
} catch (err) {
console.log(err);
return err;
}
return response;
};
My expectation would be that AWS SAM CLI would convert the Bearer token based on the correctly provided Issuer URL into an identity value which I can use in later operations.
Does AWS SAM Local not support this while running locally?
SAM Local unfortunately doesn't support Authorizers. There is a feature request on AWS SAM's GitHub repository to add this feature, see https://github.com/aws/aws-sam-cli/issues/137

AWS SAM Deployment Configuration Issue

I have set up a simple serverless rest api using Node JS and AWS Lambda.
The deployment is done using AWS SAM
Below is the SAM Template :
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Serverless API With SAM
Resources:
createUser:
Type: AWS::Serverless::Function
Properties:
Handler: handler.create
MemorySize: 128
Runtime: nodejs12.x
Timeout: 3
Events:
createUserApi:
Type: Api
Properties :
Path : /users
Method : post
listUsers:
Type: AWS::Serverless::Function
Properties:
Handler: handler.list
MemorySize: 128
Runtime: nodejs12.x
Timeout: 3
Events:
listUserApi:
Type: Api
Properties :
Path : /users
Method : get
This works fine but below is my observation regarding stacks created.
It creates two AWS Lambda functions instead of one.
Both contain two APIs listed as -
createUser
listUsers
Can it not contain only on Lambda function with these two handlers inside ?
handler.js file :
const connectedToDb = require('./src/db/db.js');
const User = require('./src/model/user.js');
module.exports.create = async (event,context) =>{
console.log('create function called !!');
try{
console.log('before connecting to DB ');
await connectedToDb();
console.log('after connecting to DB ');
const usr = new User(JSON.parse(event.body));
console.log('saving a user now with ',event.body);
await usr.save();
return{
statusCode : 200,
body:JSON.stringify(usr)
}
}catch(err){
return{
statusCode : err.statusCode || 500,
body : 'Cannot Create Order'
}
}
}
module.exports.list = async (event,context) => {
console.log('listing all users');
try{
await connectedToDb();
const users = await User.find({});
console.log('users are == ',users);
return {
statusCode:200,
body:JSON.stringify(users)
}
}catch(err){
return {
statusCode:err || 500,
body:JSON.stringify(err)
}
}
}

AWS Serverless custom jwt authorizer lambda set cors response

I have a rest api being deployed on aws with serverless framework.
Now, I have created a simple jwt token custom token authorizer in it to authorize my endpoints.
This is my routes definition in the serverless.yml -
loginUser:
handler: src/controllers/auth.loginUser
events:
- http:
path: auth/local/login
method: POST
cors: true
tokenVerifier:
handler: ./src/helpers/tokenVerifier.auth
userProfile:
handler: src/controllers/users.me
events:
- http:
path: user/me
method: GET
authorizer: tokenVerifier
cors: true
And this is my custom authorizer tokenVerifier function definition -
const jwt = require('jsonwebtoken');
// Policy helper function
const generatePolicy = (principalId, effect, resource) => {
const authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
const policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
const statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
};
const auth = (event, context, callback) => {
// check header or url parameters or post parameters for token
const token = event.authorizationToken;
if (!token) return callback(null, 'Unauthorized');
// verifies secret and checks exp
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return callback(null, 'Unauthorized');
// if everything is good, save to request for use in other routes
return callback(null, generatePolicy(decoded._id, 'Allow', event.methodArn));
});
};
export {
auth
};
Also I have created a response helper utility which I use everywhere to return response to users -
const responseHelper = (response, context, callback, status = 404) => {
eventLogger(["----RESPONSE DATA----", response], context.functionName);
callback(null, {
statusCode: status,
body: JSON.stringify(response),
headers: {
"Access-Control-Allow-Origin" : "*", // Required for CORS support to work
"Access-Control-Allow-Credentials" : true, // Required for cookies, authorization headers with HTTPS
"Content-Type": "application/json"
}
});
};
So, as you can see I have added cors: true to the routes yml, and also added the cors headers to all my lambda responses.
On google I came across this answer and I tried to add these to the default_4xx, default_5xx and expired token responses too
But, still I am getting the same error -
Access to XMLHttpRequest at '<url>/dev/user/me' from origin '<site_url>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
So, am I missing something here ? or can anyone help find and fix this issue here ?
Assuming you're following this example, you should add the resources part to your serverless.yml:
resources:
Resources:
# This response is needed for custom authorizer failures cors support ¯\_(ツ)_/¯
GatewayResponse:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: EXPIRED_TOKEN
RestApiId:
Ref: 'ApiGatewayRestApi'
StatusCode: '401'
AuthFailureGatewayResponse:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: UNAUTHORIZED
RestApiId:
Ref: 'ApiGatewayRestApi'
StatusCode: '401'
https://github.com/serverless/examples/blob/acfa06e6a93c1cb6e5d9306e65675d1acdec5eb3/aws-node-auth0-custom-authorizers-api/serverless.yml#L36
Also, I would first test the API in POSTMAN (or any other similar tool), to make sure everything works as expected before handling CORS.

How to obtain AWS IOT endpoint URL from within a CloudFormation template?

I want some of my Lambda resources to push to an AWS IOT endpoint using aws-sdk's AWS.IotData({ endpoint: url }) function - where endpoint is a required parameter.
Right now, I am passing the endpoint URL via an environment variable to my Lambda. However, when put into a SAM/CF template, I can't find a way to retrieve my IOT endpoint URL, so that I could simply !Ref it.
Browsing through the AWS resource type reference I did not find any resource that corresponds to an IOT endpoint.
It seems like IOT endpoint can only be provisioned manually, via AWS Console (enabled / disabled), as on the screenshot below:
Any advice on how to have control over provisioning an IOT endpoint or at least reading the IOT URL from within a SAM/CF template, without scripting this with aws-cli?
For anyone interested in the solution with CloudFormation Custom Resource, I wrote a simple Lambda and a CF template that provides an IOT endpoint address to other CF stacks.
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
IotEndpointProvider:
Type: 'AWS::Serverless::Function'
Properties:
FunctionName: IotEndpointProvider
Handler: iotEndpointProvider.handler
Runtime: nodejs6.10
CodeUri: .
MemorySize: 128
Timeout: 3
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iot:DescribeEndpoint
Resource:
- '*'
IotEndpoint:
Type: 'Custom::IotEndpoint'
Properties:
ServiceToken: !GetAtt IotEndpointProvider.Arn
Outputs:
IotEndpointAddress:
Value: !GetAtt IotEndpoint.IotEndpointAddress
Export:
Name: IotEndpointAddress
iotEndpointProvider.js
var aws = require("aws-sdk");
exports.handler = function(event, context) {
console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));
// For Delete requests, immediately send a SUCCESS response.
if (event.RequestType == "Delete") {
sendResponse(event, context, "SUCCESS");
return;
}
const iot = new aws.Iot();
iot.describeEndpoint({}, (err, data) => {
let responseData, responseStatus;
if (err) {
responseStatus = "FAILED";
responseData = { Error: "describeEndpoint call failed" };
console.log(responseData.Error + ":\n", err);
} else {
responseStatus = "SUCCESS";
responseData = { IotEndpointAddress: data.endpointAddress };
console.log('response data: ' + JSON.stringify(responseData));
}
sendResponse(event, context, responseStatus, responseData);
});
};
// Send response to the pre-signed S3 URL
function sendResponse(event, context, responseStatus, responseData) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
console.log("RESPONSE BODY:\n", responseBody);
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length
}
};
console.log("SENDING RESPONSE...\n");
var request = https.request(options, function(response) {
console.log("STATUS: " + response.statusCode);
console.log("HEADERS: " + JSON.stringify(response.headers));
// Tell AWS Lambda that the function execution is done
context.done();
});
request.on("error", function(error) {
console.log("sendResponse Error:" + error);
// Tell AWS Lambda that the function execution is done
context.done();
});
// write data to request body
request.write(responseBody);
request.end();
}
I'm afraid you cannot provision an IoT endpoint, as the only API call that is related to an IoT endpoint is DescribeEndpoint.
What you can do is create a Lambda-backed CloudFormation Custom Resource. The Lambda function will execute the DescribeEndpoint call (using the AWS SDK of your choice depending on the Lambda's runtime) and return the endpoint's URL so your other CloudFormation resources can consume it.
Here's a good example on Lambda-backed Custom Resources: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html.