AWS CDK - Cognito UserPool authorizer on API Gateway not working - amazon-web-services

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

Related

AWS API Gateway Export is exporting an older version

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?

How to Get AccessToken Dynamically from Cognito OAuth2.0 in Electron JS

Hello i use Electron JS for a desktop app which is related to a cloud plateform from which in needto get a list of Patients.
As far as now i can get it but with a static AccessToken. I really struggled to get it dynamic, please help.
Here is my code :
This is my configuration file where i specify Cognito Parameters :
export default {
s3: {
REGION: 'YOUR_S3_UPLOADS_BUCKET_REGION',
BUCKET: 'YOUR_S3_UPLOADS_BUCKET_NAME',
},
apiGateway: {
REGION: 'YOUR_API_GATEWAY_REGION',
URL: 'YOUR_API_GATEWAY_URL',
},
cognito: {
REGION: 'eu-west-1',
USER_POOL_ID: 'eu-west-1_P0Jcr7nig',
APP_CLIENT_ID: '4m1utu56hjm835dshts9jg63ou',
IDENTITY_POOL_ID: 'YOUR_IDENTITY_POOL_ID',
authenticationFlowType: 'USER_PASSWORD_AUTH',
AUTHENTICATION_FLOW_TYPE: 'USER_PASSWORD_AUTH',
},
API: {
endpoints: [
{
name: 'PatientsList',
endpoint: 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage/patients',
//endpoint: 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage',
},
],
},
};
Auth.signIn({
username: process.env.username,
password: process.env.password,
}).then().catch(err => {
console.log(err)});
In another file this is my getaccesstoken function which i export to the main
function getAccessToken() {
const poolData = {
UserPoolId : COGNITO_USER_POOL_ID,
ClientId : COGNITO_CLIENT_ID,
};
const userPool = new CognitoUserPool(poolData);
var authenticationData = {
Username : process.env.username, // your username here
Password : process.env.password, // your password here,
authenticationFlowType: process.env.AUTHENTICATION_FLOW_TYPE,
Pool : userPool
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
authenticationData);
var cognitoUser = new CognitoUser(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
console.log('access token + ' + result.getAccessToken().getJwtToken());
},
onFailure: function(err) {
console.log(err);
},
});
}
And finally here is how i get the data in main :
The declarations :
const { Auth } = require('./cognitoAuth');
const theAccessToken = require('./cognitoAuth');
The code :
//Get Data From Cloud ECS
const API_URL = 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage/patients';
const headers = {
"Content-Type": "application/json",
//Authorization: theAccessToken.getAccessToken()
Authorization: "eyJraWQiOiJBbE1DZnBCTHYyVUlNazhXSG4xaTk4RG1QNlFmcFpSSjFaSW1qcVVFZnVBPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI4OWYyZGMxZi1iMTI3LTQzM2QtODJhYS1iMjNkNWJhNzY5NGEiLCJjb2duaXRvOmdyb3VwcyI6WyJkb2N0b3IiXSwiZXZlbnRfaWQiOiI1OTM0ZmIwNC0yYTUzLTQ2NmQtYTU1Ni0zNTM3M2RhZmU1Y2UiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiYXV0aF90aW1lIjoxNTk1NDI2NjQ2LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtd2VzdC0xLmFtYXpvbmF3cy5jb21cL2V1LXdlc3QtMV9QMEpjcjduaWciLCJleHAiOjE1OTcxNTUxMDUsImlhdCI6MTU5NzE1MTUwNSwianRpIjoiNGRkN2U5ZGUtYmQ2YS00NTg4LWIzZDAtMTVjMWM1NWQxY2Y2IiwiY2xpZW50X2lkIjoiNG0xdXR1NTZoam04MzVkc2h0czlqZzYzb3UiLCJ1c2VybmFtZSI6Ijg5ZjJkYzFmLWIxMjctNDMzZC04MmFhLWIyM2Q1YmE3Njk0YSJ9.LYvzPRBxvKw2P3gHwV8NhYPg_EB3F7ZK2F5HpRWHtBHksr6D4N5Fw56ZVupkRCxVJSq0f93DdljI7BBcBnp9d_hpLzmJLTfBhA3t870omTxalTqpGXN_SvsZmuwcfCX-awn1Um6x_-fhq3zcfPkB9FBljbtwPN-kvCc-Iynei9anVxXI686nIbkfbYfnuRnHrbY0vg8FtyDIDBMv277FPoJ96NwPD4mJvNBxQHi_KfWxQ1DmLiAC6c_l2jP_wawAPBv788CjD_8OlKBbjAHinGEkaL1K9vjI5MhNPyTA5ym1IaWar7Jr8RkUDzQGvqEUPKoOUe9PswmOOxLBjehMgQ"
};
//console.log('Token Value:', theAccessToken.getAccessToken());
const getPatients = async(API_URL) => {
try {
//get data from cloud specifiying get method and headers which contain token
const response = await fetch(API_URL,{
method: 'GET', headers: headers}
);
var listPatients = await response.json();
listPatients.items.forEach(patient => {
//Checking what i got
console.log(patient);
});
} catch(err) {
console.log("Error" + err);
}
};
getPatients(API_URL);
Now when i make it dynamic by specifying theAccessToken.getAccessToken
I get this error, USER_SRP is not enabled even if specify it, when i asked team told me the cloud service doesn't want to enable it.
So how can i get this access token please?
For a desktop app it is recommended to do these 2 things, according to security guidance:
Use Authorization Code Flow (PKCE)
Login via the system browser, so that the app never sees the user's password
I have a couple of Electron code samples that use Cognito, which you can easily run - maybe start here:
First desktop app

Migrate from apigateway to apigatewayv2

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

Referencing the physical resource ID in a AwsCustomResource in onUpdate or onDelete

I am trying to create an AwsCustomResource to make single AWS API calls, specifically (create|update|delete)-organisational-unit. In the onCreate I can see that to return the OU's Id as the physical resource ID I can specify a physicalResourceIdPath, however for the onUpdate and onDelete I need to specify this in the API call.
I can not see how, other than to use regular custom resources with my own Lambda to gain access to the event.
Here is what I have so far, 'help?' is what I'm missing.
import * as cdk from '#aws-cdk/core';
import { AwsCustomResource } from '#aws-cdk/custom-resources'
export interface OrganisationalUnitProps {
readonly name: string;
readonly parentId: string;
}
export class OrgansationalUnit extends AwsCustomResource {
constructor(scope: cdk.Construct, id: string, props: OrganisationalUnitProps) {
super(scope, id, {
onCreate: {
service: 'Organizations',
action: 'createOrganizationalUnit',
parameters: {
Name: props.name,
ParentId: props.parentId
},
physicalResourceIdPath: 'OrganizationalUnit.Id'
},
onDelete: {
service: 'Organizations',
action: 'deleteOrganizationalUnit',
parameters: {
OrganizationalUnitId: 'help?'
}
}
});
}
}
Whilst there isn't a perfectly clean way to do it yet, here is an answer I was given in this issue that I raised on the CDK github page.
You create two custom resources. One is just a create, and one is just a delete.
const connectDirectory = new AwsCustomResource(this, 'ConnectDirectory', {
onCreate: {
service: 'DirectoryService',
action: 'connectDirectory',
parameters: { ... },
physicalResourceId: PhysicalResourceId.fromResponse('DirectoryId')
},
});
const deleteDirectory = new AwsCustomResource(this, 'DeleteDirectory', {
onDelete: {
service: 'DirectoryService',
action: 'deleteDirectory',
parameters: {
DirectoryId: connectDirectory.getResponseField('DirectoryId'),
},
},
});
That github issue has been accepted as a change request, so there may be a better answer in the future.
nowadays there is PhysicalResourceIdReference which you can use like this:
new AwsCustomResource(this, 'my-custom-resource', {
...
onCreate: {
...
physicalResourceId: PhysicalResourceId.fromResponse( ... )
},
onDelete: {
...
parameters: {
physicalResourceId: new PhysicalResourceIdReference(),
}
}
});

Manually sign AppSync URL to use in Lambda gives bad signature error

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.