I created an API Gateway with AWS CDK with a resource and a request model:
const api = new apigateway.RestApi(this, 'MyApiGateway', {
deploy: true,
retainDeployments: false
});
const createDto = api.addModel('CreateUserDto', {
modelName: 'CreateUserDto',
schema: ...
});
const users = api.root.addResource('users');
users.addMethod(
'POST',
createUserLambdaIntegration, {
operationName: 'Create User',
requestModels: {
'application/json': createDto
},
methodResponses: [
{
statusCode: '201'
}
]
}
);
then I changed it by adding a response model and changing the name of the request model:
const api = new apigateway.RestApi(this, 'MyApiGateway', {
deploy: true,
retainDeployments: false
});
const createUserRequest = api.addModel('CreateUserRequest', {
modelName: 'CreateUserRequest',
schema: ...
});
const createUserResponse = api.addModel('CreateUserResponse', {
modelName: 'CreateUserResponse',
schema: ...
});
const users = api.root.addResource('users');
users.addMethod(
'POST',
createUserLambdaIntegration, {
operationName: 'Create User',
requestModels: {
'application/json': createUserRequest
},
methodResponses: [
{
statusCode: '201',
responseModels: {
'application/json': createUserResponse
}
}
]
}
);
this all works somewhat fine, as I am able to see the updated models on the API Gateway console, but when I try exporting the API as a Swagger JSON, it always exports the first version I deployed. I tried exporting it using both the API Gateway console and the AWS CLI with the exact same result.
What am I doing wrong?
Related
This has me stumped.
I have a cognito pool with a resource server, one custom scope and one app client (clientid and secret).
I have my (pre-request) script set up in Postman like this
const clientId = pm.environment.get("cognitoClientId");
const clientSecret = pm.environment.get("cognitoClientSecret");
const grantType = "client_credentials";
const tokenUrl = pm.environment.get("cognitoAuthTokenURL");
const postRequest = {
url: tokenUrl,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
mode: 'urlencoded',
urlencoded: [
{
key: "grant_type",
value: grantType
},
{
key: "client_id",
value: clientId
},
{
key: "client_secret",
value: clientSecret
},
{
key: "scope",
value: "myResourceServer/myResource.list"
}
]
}
};
pm.sendRequest(
postRequest
,(error, response) => {
console.log("Callback fired");
console.log(error);
console.log(response);
}
);
It fails on the response body with the following error:
{"error":"invalid_grant"}
From the AWS Documentation I get
Refresh token has been revoked
Which makes no sense as I am not requesting a refresh token
What am I doing wrong here, or where should I be looking?
I am following this guide for signing HTTP requests to an Amazon OpenSearch Service using Node.js (version 3 of the AWS SDK for JavaScript).
When I copy the exact sample code and export my AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY of my authorised user, the PUT index/type/id request to add an item is successful:
201 Created
Response body: {"_index":"products","_type":"_doc","_id":"2","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":0,"_primary_term":1}
However, when I change the request to instead GET /_search endpoint, I get:
403 Forbidden
Response body: {"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
The user is fully authorised against the index:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::**:user/aws-elasticbeanstalk-ec2-user"
},
"Action": "es:*",
"Resource": "arn:aws:es:ap-southeast-2:**:domain/mydomain/*"
},
How do I rectify my signature?
Here is my modified code from the above link:
const { HttpRequest } = require('#aws-sdk/protocol-http')
const { defaultProvider } = require('#aws-sdk/credential-provider-node')
const { SignatureV4 } = require('#aws-sdk/signature-v4')
const { NodeHttpHandler } = require('#aws-sdk/node-http-handler')
const { Sha256 } = require('#aws-crypto/sha256-browser')
const region = ''
const domain = ''
const index = 'products'
const type = '_search'
const createBody = (query) => ({
query: {
multi_match: {
query,
type: 'phrase',
fields: [
'tags',
'name',
'category',
'maker'
]
}
},
highlight: {
pre_tags: [''],
post_tags: [''],
fields: {
tags: {},
name: {},
category: {},
maker: {}
}
}
})
searchIndex('sh').then(() => process.exit())
async function searchIndex (query) {
const request = new HttpRequest({
body: JSON.stringify(createBody(query)),
headers: {
'Content-Type': 'application/json',
host: domain
},
hostname: domain,
method: 'GET',
path: index + '/' + type
})
const signer = new SignatureV4({
credentials: defaultProvider(),
region: region,
service: 'es',
sha256: Sha256
})
const signedRequest = await signer.sign(request)
const client = new NodeHttpHandler()
const { response } = await client.handle(signedRequest)
console.log(response.statusCode + ' ' + response.body.statusMessage)
let responseBody = ''
return new Promise((resolve) => {
response.body.on('data', (chunk) => {
responseBody += chunk
})
response.body.on('end', () => {
console.log('Response body: ' + responseBody)
resolve(responseBody)
})
}, (error) => {
console.log('Error: ' + error)
})
}
i also had this issue, using the same tutorial
reading the docs on request body searches, i found it states the following:
Note The _search API accepts HTTP GET and POST for request body
searches, but not all HTTP clients support adding a request body to a
GET request. POST is the more universal choice.
changing my method to POST solved the issue for me
My goal is to set up some lambda functions which are public (i.e. no authorization required to send requests) and other ones which require a User to be logged in within a Cognito UserPool.
In my CDK file below, I'm adding an Authorizer only on one of the two endpoints, but then when I launch a request both of them are unprotected, and in the function logs you can see there is no Cognito UserPool nor AuthenticationType.
Any ideas on what's missing?
Thanks!
{
"httpMethod":"GET",
"body":null,
"resource":"/private",
"requestContext":{
...,
"identity":{
"apiKey":null,
"userArn":null,
"cognitoAuthenticationType":null,
"caller":null,
"userAgent":"Custom User Agent String",
"user":null,
"cognitoIdentityPoolId":null,
"cognitoAuthenticationProvider":null,
"sourceIp":"127.0.0.1",
"accountId":null
},
...
},
...
}
CDK file:
import * as apigateway from '#aws-cdk/aws-apigateway';
import * as lambda from '#aws-cdk/aws-lambda';
import * as s3 from '#aws-cdk/aws-s3';
import { UserPool, VerificationEmailStyle, UserPoolClient } from '#aws-cdk/aws-cognito'
import { App, CfnParameter, Duration, Stack, StackProps } from '#aws-cdk/core';
export class CdkStack extends Stack {
constructor(scope: App, id: string, props: StackProps) {
super(scope, id, props);
new CfnParameter(this, 'AppId');
const userPool = new UserPool(this, 'dev-users', {
userPoolName: 'dev-users',
selfSignUpEnabled: true,
userVerification: {
emailSubject: 'Verify your email for our awesome app!',
emailBody: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
emailStyle: VerificationEmailStyle.CODE,
smsMessage: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
},
signInAliases: {
email: true
},
signInCaseSensitive: false,
standardAttributes: {
email: { required: true, mutable: false }
},
passwordPolicy: {
minLength: 6,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: false,
tempPasswordValidity: Duration.days(7),
}
})
const environment = { };
// The code will be uploaded to this location during the pipeline's build step
const artifactBucket = s3.Bucket.fromBucketName(this, 'ArtifactBucket', process.env.S3_BUCKET!);
const artifactKey = `${process.env.CODEBUILD_BUILD_ID}/function-code.zip`;
const code = lambda.Code.fromBucket(artifactBucket, artifactKey);
// This is a Lambda function config associated with the source code: get-all-items.js
const publicFunction = new lambda.Function(this, 'publicFunction', {
description: 'A simple example includes a HTTP get method accessible to everyone',
handler: 'src/handlers/public.publicHandler',
runtime: lambda.Runtime.NODEJS_10_X,
code,
environment,
timeout: Duration.seconds(60),
});
// Give Read permissions to the SampleTable
// This is a Lambda function config associated with the source code: put-item.js
const privateFunction = new lambda.Function(this, 'privateFunction', {
description: 'This functions should only be accessible to authorized users from a Cognito UserPool',
handler: 'src/handlers/private.privateHandler',
runtime: lambda.Runtime.NODEJS_10_X,
code,
timeout: Duration.seconds(60),
environment,
});
const api = new apigateway.RestApi(this, 'ServerlessRestApi', { cloudWatchRole: false });
const authorizer = new apigateway.CfnAuthorizer(this, 'cfnAuth', {
restApiId: api.restApiId,
name: 'HelloWorldAPIAuthorizer',
type: 'COGNITO_USER_POOLS',
identitySource: 'method.request.header.Authorization',
providerArns: [userPool.userPoolArn],
})
api.root.addResource('public').addMethod(
'GET',
new apigateway.LambdaIntegration(publicFunction)
);
api.root.addResource('private').addMethod(
'GET',
new apigateway.LambdaIntegration(privateFunction),
{
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer: {
authorizerId: authorizer.ref
}
}
);
}
}
const app = new App();
new CdkStack(app, 'CognitoProtectedApi', {});
app.synth();
Try doing the following in your addMethod.
{
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer // pass the authorizer object instead of authorizerId stuff.
}
Refer https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html for more details.
Following is for AWS CDK 2.20.0
You can create a CognitoUserPoolsAuthorizer and then either attach it as default authorizer for an API GW, or attach it specific route.
For adding to a specific method,
const userPool = new cognito.UserPool(this, 'UserPool');
const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', {
cognitoUserPools: [userPool]
});
declare const books: apigateway.Resource;
books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), {
authorizer: auth,
authorizationType: apigateway.AuthorizationType.COGNITO,
})
Refer https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html
How to migrate from apigateway to apigatewayv2 using AWS-CDK?
Specifically: I am using LambdaRestApi and restApiId and deploymentStage from that resource.
// old
const apiGw = new apigateway.LambdaRestApi(this, 'MyAPI', {
handler: lambdaFrontend,
proxy: true,
binaryMediaTypes: ['*/*'],
});
// new
const apiGw2 = new apigateway.CfnApi(this as any, 'MyAPIV2', {
protocolType: "http",
target: lambdaFrontend.functionArn,
})
I am trying to get the OriginSource for CF like so:
const domainName = ${apiGw.restApiId}.execute-api.${this.region}.${this.urlSuffix};
First question: How can I retrieve the domainName with ApiGW2?
I also need the stageName. Currently I am retrieving it like so:
const originPath = '/' + apiGw.deploymentStage.stageName;
Second question: How can I retrieve the origin Path with ApiGW2?
Alternatively: Is there a better way to connect my ApiGW2 with CF?
const fecf = new cf.CloudFrontWebDistribution(this, "MyCF", {
originConfigs: [{
customOriginSource: {
domainName: `${apiGw.restApiId}.execute-api.${this.region}.${this.urlSuffix}`,
},
originPath: '/' + apiGw.deploymentStage.stageName,
...
}
This can now be solved quite easily since we have official documentation for this now.
If anybody out there wants to migrate to V2 now, this is the way:
const httpApiIntegration = new apigatewayv2Integrations.LambdaProxyIntegration({
handler: fn,
});
const httpApi = new apigatewayv2.HttpApi(this, "MyApiV2");
httpApi.addRoutes({
path: "/",
methods: [HttpMethod.ANY],
integration: httpApiIntegration,
});
new cloudfront.CloudFrontWebDistribution(this, "MyCf", {
defaultRootObject: "/",
originConfigs: [
{
customOriginSource: {
domainName: `${httpApi.httpApiId}.execute-api.${this.region}.${this.urlSuffix}`,
},
behaviors: [
{
isDefaultBehavior: true,
},
],
},
],
enableIpV6: true,
});
https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-readme.html
Steps:
create an integration (e.g. a lambda function), this comes from a dedicated package (apigatewayv2-integrations)
create an HttpApi, no options needed
New: Opposed to APIGWv1 you will have to add route handlers for your paths (httpApi.addRoutes).
Cloudfront config is very similar
In a Lambda, I would like to sign my AppSync endpoint with aws-signature-v4 in order to use it for a mutation.
The URL generated seems to be ok but it gives me the following error when I try it:
{
"errors" : [ {
"errorType" : "InvalidSignatureException",
"message" : "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details. etc...
} ]
}
Here is my lambda function
import { Context, Callback } from 'aws-lambda';
import { GraphQLClient } from 'graphql-request';
const v4 = require('aws-signature-v4');
export async function handle(event: any, context: Context, callback: Callback) {
context.callbackWaitsForEmptyEventLoop = false;
const url = v4.createPresignedURL(
'POST',
'xxxxxxxxxxxxxxxxx.appsync-api.eu-west-1.amazonaws.com',
'/graphql',
'appsync',
'UNSIGNED-PAYLOAD',
{
key: 'yyyyyyyyyyyyyyyyyyyy',
secret: 'zzzzzzzzzzzzzzzzzzzzz',
region: 'eu-west-1'
}
);
const mutation = `{
FAKEviewProduct(title: "Inception") {
productId
}
}`;
const client = new GraphQLClient(url, {
headers: {
'Content-Type': 'application/graphql',
action: 'GetDataSource',
version: '2017-07-25'
}
});
try {
await client.request(mutation, { productId: 'jfsjfksldjfsdkjfsl' });
} catch (err) {
console.log(err);
callback(Error());
}
callback(null, {});
}
I got my key and secret by creating a new user and Allowing him appsync:GraphQL action.
What am I doing wrong?
This is how I trigger an AppSync mutation using by making a simple HTTP-request, using axios.
const AWS = require('aws-sdk');
const axios = require('axios');
exports.handler = async (event) => {
let result.data = await updateDb(event);
return result.data;
};
function updateDb({ owner, thingName, key }){
let req = new AWS.HttpRequest('https://xxxxxxxxxxx.appsync-api.eu-central-1.amazonaws.com/graphql', 'eu-central-1');
req.method = 'POST';
req.headers.host = 'xxxxxxxxxxx.appsync-api.eu-central-1.amazonaws.com';
req.headers['Content-Type'] = 'multipart/form-data';
req.body = JSON.stringify({
"query":"mutation ($input: UpdateUsersCamsInput!) { updateUsersCams(input: $input){ latestImage uid name } }",
"variables": {
"input": {
"uid": owner,
"name": thingName,
"latestImage": key
}
}
});
let signer = new AWS.Signers.V4(req, 'appsync', true);
signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());
return axios({
method: 'post',
url: 'https://xxxxxxxxxxx.appsync-api.eu-central-1.amazonaws.com/graphql',
data: req.body,
headers: req.headers
});
}
Make sure to give the IAM-role your Lambda function is running as, permissions for appsync:GraphQL.
Adding an answer here because I had difficulty getting the accepted answer to work and I found an issue on the AWS SDK GitHub issues that said it's not recommended to use the AWS.Signers.V4 object in production. This is how I got it to work using the popular aws4 npm module that is recommended later on in the issue linked above.
const axios = require('axios');
const aws4 = require('aws4');
const query = `
query Query {
todos {
id,
title
}
}`
const sigOptions = {
method: 'POST',
host: 'xxxxxxxxxx.appsync-api.eu-west.amazonaws.com',
region: 'eu-west-1',
path: 'graphql',
body: JSON.stringify({
query
}),
service: 'appsync'
};
const creds = {
// AWS access tokens
}
axios({
url: 'https://xxxxxxxxxx.appsync-api.eu-west/graphql',
method: 'post',
headers: aws4.sign(sigOptions, creds).headers,
data: {
query
}
}).then(res => res.data))
You don't need to construct a pre-signed URL to call an AWS AppSync endpoint. Set the authentication mode on the AppSync endpoint to AWS_IAM, grant permissions to your Lambda execution role, and then follow the steps in the "Building a JavaScript Client" tutorial to invoke a mutation or query.