Request mapping and wildcards in #aws-cdk/aws-apigatewayv2 - amazon-web-services

I'm trying to add a HttpRoute in a HttpApi with the following features:
a wildcard after the initial path: i'd like to proxy everything after "/foo" (such as "/foo", "/foo/bar" etc) to an ecs service (via HttpServiceDiscoveryIntegration, already set)
a request mapping so that the path is correctly handled (I'm using stages for the Api)
Right now I've the following code:
new HttpRoute(scope, 'Route', {
httpApi,
routeKey: HttpRouteKey.with('/foo'),
integration: new HttpServiceDiscoveryIntegration({
service: service.cloudMapService!,
vpcLink: VpcLink.fromVpcLinkAttributes(scope, 'VpcLink', {
vpc,
vpcLinkId: 'aaa',
}),
}),
});
I can't find the right place to put the wildcard (if I put the wildcard inside the HttpRouteKey.with('/foo*') it rises an error)
For what concerns the request mapping, I'd like to obtain what follows:
Thanks!!!

Found out CDK doesn't support what I wanted, so I used the Cfn classes:
const integration = new CfnIntegration(scope, 'Integration', {
apiId: httpApi.apiId,
integrationType: HttpIntegrationType.HTTP_PROXY,
integrationUri: service.cloudMapService!.serviceArn,
integrationMethod: HttpMethod.ANY,
connectionId: vpcLink.vpcLinkId,
connectionType: HttpConnectionType.VPC_LINK,
payloadFormatVersion: PayloadFormatVersion.VERSION_1_0.version,
requestParameters: {
'overwrite:path': '$request.path',
},
});
new CfnRoute(scope, 'BeckyRoute', {
apiId: httpApi.apiId,
routeKey: `ANY /foo`,
target: `integrations/${integration.ref}`,
});
new CfnRoute(scope, 'BeckyProxyRoute', {
apiId: httpApi.apiId,
routeKey: `ANY /foo/{proxy+}`,
target: `integrations/${integration.ref}`,
});

Related

How to add multiple base path mappings from different projects into the same AWS API Gateway?

We have separate AWS CDK projects for different APIs. We want to use same subdomain with different base path mappings for the same API Gateway resource. For example; lets say we have two APIs which are tenantApi and invoiceApi mapping to test.example.com/tenant and test.example.com/invoice. This is doable from one repository with creating one RestApi and defining multiple base path mappings to it. However, I couldn't find a way to achieve this doing it from different repositories since I need to create only one ARecord for the subdomain. My thought was creating ARecord inside a repository where we manage shared resources and importing that record from the repositories we will use the same Api Gateway.
Here is the simple aws cdk code about how I am creating an Api Gateway. As you can see, we have to pass RestApi instance into route53.RecordTarget.fromAlias so I am not really sure if we can create a ARecord before creating an Api Gateway.
export class ApiGatewayStack extends Stack {
constructor(scope: Construct, id: string, props: StackEnvProps) {
super(scope, id, props);
const tenantApi = new apigateway.RestApi(this, 'tenantApi', {
domainName: {
domainName: props.context['domainName'],
certificate: acm.Certificate.fromCertificateArn(this, 'certificateArn', props.context['certificateArn']),
basePath: 'tenant'
},
deploy: true,
deployOptions: {
stageName: 'prod',
},
defaultCorsPreflightOptions: {
allowMethods: apigateway.Cors.ALL_METHODS,
allowOrigins: apigateway.Cors.ALL_ORIGINS,
}
});
const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: 'example.com' });
// create an alias for mapping
new route53.ARecord(this, 'domainAliasRecord', {
zone: zone,
recordName: "test",
target: route53.RecordTarget.fromAlias(new ApiGateway(tenantApi)),
});
const methodOptions: apigateway.MethodOptions = {
methodResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Content-Type': true,
},
},
{
statusCode: '400',
responseParameters: {
'method.response.header.Content-Type': true,
},
},
],
};
const postPaymentsLambda = new NodejsFunction(this, 'postTenantLambda', {
entry: './lambda/rest/tenant-api/post-tenant-api.ts',
handler: 'handler',
memorySize: 512,
runtime: lambda.Runtime.NODEJS_14_X,
});
// tenant/v1
const tenantV1 = tenantApi.root.addResource('v1');
tenantV1.addMethod('POST', new apigateway.LambdaIntegration(postPaymentsLambda), methodOptions);
}
}
I appreciate for any help. Thanks!
I had to first create a domainName then create a ARecord with targeting that domainName which can be imported from different APIs I want to attach.
// create domain name for api gateway
const domainName = new apigateway.DomainName(this, 'domainName', {
domainName: `test.${props.context['domainName']}`, // replace with props.type later
certificate: acm.Certificate.fromCertificateArn(this, 'certificateArn', props.context['certificateArn']),
endpointType: apigateway.EndpointType.REGIONAL,
securityPolicy: apigateway.SecurityPolicy.TLS_1_2,
});
const zone = route53.HostedZone.fromLookup(this, 'hostedZone', {
domainName: props.context['domainName']
});
// create an alias for mapping
new route53.ARecord(this, 'apiGatewayDomainAliasRecord', {
zone: zone,
recordName: 'test',
target: route53.RecordTarget.fromAlias(new r53target.ApiGatewayDomain(domainName)),
});
new CfnOutput(this, 'apiGatewayDomainNameAliasTarget', {
value: domainName.domainNameAliasDomainName,
description: 'domainNameAliasTarget attribute used when importing domain name',
exportName: 'apiGatewayDomainNameAliasTarget'
});
Later on, I will import this domainName to create a BasePathMapping. There are three attributes used when importing a domainName;
domainName: the domainName we created before.
domainNameAliasHostedZoneId: Hosted ZoneId where the domain is defined.
domainNameAliasTarget: AWS documentation doesn't clearly state out about what it is. Basically, it is the domainNameAliasDomainName value of the domainName we created at the very first place.
const tenantApi = new apigateway.RestApi(this, 'tenantApi', {
deployOptions: {
stageName: 'dev',
},
deploy: true,
defaultCorsPreflightOptions: {
allowMethods: apigateway.Cors.ALL_METHODS,
allowOrigins: apigateway.Cors.ALL_ORIGINS,
}
});
const domainName = apigateway.DomainName.fromDomainNameAttributes(this, 'domainName', {
domainName: `test.${props.context['domainName']}`,
domainNameAliasHostedZoneId: props.context['hostedZoneId'],
domainNameAliasTarget: Fn.importValue(props.context['domainNameAliasTarget']),
});
const nodeBasePathMapping = new apigateway.BasePathMapping(this, 'nodeBasePathMapping', {
basePath: 'node',
domainName,
restApi: tenantApi,
});

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 API Gateway, default base mappings with CDK

I'm setting up an environment with AWS CDK but I'm having trouble with API Gateway and base mappings for custom domains.
I got an API that should have two stages: "internal" and "external".
Whenever I create a new RestApi and either specify domainName as a prop to the construcor, or use the addDomainName method afterwards. This will always create a default Base Mapping, which I do not want. I want to add my own mappings like this:
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'internal', stage: internalStage });
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'external', stage: externalStage });
The problem if I do the above is that the default mapping have already been created and since it will be created with an empty basePath, I can't add any other mappings to the same API.
I've checked the source code and there does not seem to be a way to pass mappings when you add a domain, they are always created automatically.
Is there a way to change the default mappings, or to get pass this problem in another way?
Example that would be nice:
apiGateway.domainName.basePathMappings[0] = { ... }
My code right now:
const apiGateway = new apigw.RestApi(this, 'RestApi', {
deploy: false,
domainName: {
domainName: 'sub.example.com',
certificate,
endpointType: apigw.EndpointType.REGIONAL,
securityPolicy: apigw.SecurityPolicy.TLS_1_2,
},
});
const deployment = new apigw.Deployment(this, 'Deployment', { api: apiGateway });
const internalStage = new apigw.Stage(this, 'InternalStage', {
stageName: 'internal',
deployment,
});
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'internal', stage: internalStage });
const externalStage = new apigw.Stage(this, 'ExternalStage', {
stageName: 'external',
deployment,
});
apiGateway.domainName.addBasePathMapping(apiGateway, { basePath: 'external', stage: externalStage });
The generated syntax when i run Synth, will show 3 different AWS::ApiGateway::BasePathMapping.
One for internal, one for external (with basePath set correctly) and one is the default created one with no basePath (which I want gone).
The moment we add a domainName either by passing to RestApi or by calling .addDomainName , cdk is adding a base path mapping /.
I was able to work around by using cfn resources for DomainName and Base path mapping.
const cfnInternalDomain = new apigw.CfnDomainName(this, "internal-domain", {
domainName: internalDomainName,
regionalCertificateArn: myCert.certificateArn,
endpointConfiguration: { types: [apigw.EndpointType.REGIONAL] },
});
const intBasePath = new apigw.CfnBasePathMapping(
this,
"internal-base-path",
{
basePath: "intPath",
domainName: cfnInternalDomain.ref,
restApiId: myRestApi.restApiId,
stage: internalStage.stageName,
}
);
This is full code.
const myRestApi = new apigw.RestApi(this, "rest-api", {
deploy: false,
});
myRestApi.root.addMethod("ANY", new apigw.MockIntegration());
const deployment = new apigw.Deployment(this, "api-deployment", {
api: myRestApi,
retainDeployments: false,
});
const internalStage = new apigw.Stage(this, "internal-stage", {
stageName: "internal",
deployment,
});
const internalDomainName = "internal.mytest.domain.com";
const cfnInternalDomain = new apigw.CfnDomainName(this, "internal-domain", {
domainName: internalDomainName,
regionalCertificateArn: myCert.certificateArn,
endpointConfiguration: { types: [apigw.EndpointType.REGIONAL] },
});
const intBasePath = new apigw.CfnBasePathMapping(
this,
"internal-base-path",
{
basePath: "intPath",
domainName: cfnInternalDomain.ref,
restApiId: myRestApi.restApiId,
stage: internalStage.stageName,
}
);

How to configure CORS for AWS API Gateway using OpenAPI?

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.

Swagger definition for an AWS Api-Gateway Lambda Proxy endpoint

FYI - I've checked similar issues related to this, but none solves my problem.
I'm trying to create the Swagger definition for a number of APIs under AWS Api-Gateway. I'm able to successfully do this for other(POST, GET) endpoints from an auto-generated YAML configuration I downloaded from the API Stage.
But I encountered issues when I tried to do same for an Api-Gateway endpoint with Lambda Proxy Integration: Error from Swagger editor.swagger.io
Below is my YAML definition for the failing endpoint:
swagger: "2.0"
info:
version: "2018-04-18T17-09-07Z"
title: "XXX API"
host: "api.xxx.io"
schemes:
- "https"
parameters:
stage:
name: stage
in: path
type: string
enum: [ staging, production]
required: true
paths:
/env/{stage}/{proxy+}:
x-amazon-apigateway-any-method:
produces:
- "application/json"
parameters:
- $ref: '#/parameters/stage'
- name: "proxy"
in: "path"
required: true
type: "string"
responses: {}
x-amazon-apigateway-integration:
uri: "arn:aws:apigateway:eu-central-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-central-1:xxxxxxxxx:function:environment/invocations"
responses:
default:
statusCode: "200"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
cacheNamespace: "4vbcjm"
cacheKeyParameters:
- "method.request.path.proxy"
contentHandling: "CONVERT_TO_TEXT"
type: "aws_proxy"
this is inline with AWS Documentation: enter link description here
Please, what am I missing?
At a glance I believe you have an error in your parameters block. If you include a $ref it discards anything in that block that follows it, so your proxy name is getting dropped. I have a similar setup with api-gateway proxying all calls to a lambda and this is my parameters block:
parameters:
- name: "proxy"
in: "path"
required: true
type: "string"
Additionally you may want an authorizer if you're at all worried about DDoS or serving up secure data. That's done by adding a security array as a sibling to parameters, and a securityDefinitions block as a sibling to paths
security:
- authorizer: []
securityDefinitions:
authorizer:
type : "apiKey"
name : "Authorization"
in : "header"
x-amazon-apigateway-authtype : "custom"
x-amazon-apigateway-authorizer : {
type : "request",
authorizerUri : "arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${region}:${account_id}:function:${authorizer_function_name}/invocations",
authorizerResultTtlInSeconds : 58,
identitySource: "method.request.header.authorization",
}
*note I'm publishing swagger as a terraform template, hence the ${} substitution.