I am trying to create and deploy a CustomMessageTriggerHandler lambda for customizing the verification messages sent out by Cognito using cdk, and I would like to include an image asset to be included in the email. This will need to be a public url, but I'm struggling to update its permissions so that the it does not return 403 access denied.
Here is the code I have tried:
export class MyServiceStack extends Stack {
constructor(app: Construct, id: string, props: MyServiceStackProps) {
super(app, id, props)
const imageAsset = new Asset(this, 'logo', {
path: join(__dirname, './assets/logo.png')
})
imageAsset.bucket.grantPublicAccess() // this was my attempt to allow public reads
const customizeVerificationMessage = new NodejsFunction(
this,
'customizeVerificationMessage',
{
//...other config
environment: {
LOGO_URL: imageAsset.httpUrl
}
}
)
// ...other code
const userPool = new UserPool(this, 'userpool', {
//...other config
lambdaTriggers: {
//...other triggers
customMessage: customizeVerificationMessage
},
})
}
}
I expected that this code would create a publicly accessible asset, but
imageAsset.httpUrl
included in the email returns 403.
Yes that will not work as Assets will end up in a private S3 Bucket. You should not use the Assets. Instead create an S3 bucket and upload the picture there with the S3Deployment construct.
Related
Basically I'm trying to get a pre-signed URL for the putObject method in S3. I send this link back to my frontend, which then uses it to upload the file directly from the client.
Here's my code :
const AWS = require("aws-sdk");
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_IAM_ACCESS,
secretAccessKey: process.env.AWS_IAM_SECRET,
region: 'ap-southeast-1',
});
const getPreSignedUrlS3 = async (data) => {
try {
//DO SOMETHING HERE TO GENERATE KEY
const options = {
Bucket: process.env.AWS_USER_CDN,
ContentType: data.type,
Key: key,
Expires: 5 * 60
};
return new Promise((resolve, reject) => {
s3.getSignedUrl(
"putObject", options,
(err, url) => {
if (err) {
reject(err);
}
else resolve({ url, key });
}
);
});
} catch (err) {
console.log(err)
return {
status: 500,
msg: 'Failed to sync with CDN, Please try again later!',
}
}
}
I'm getting the following error from the aws sdk : The security token included in the request is invalid.
Things I have tried :
Double check the permissions from my IAM user. Even made bucket access public for testing. My IAM user is given full s3 access policy.
Tried using my root user security key and access details. Still got the same error.
Regenerated new security credentials for my IAM user. I don't have any MFA turned on.
I'm following this documentation.
SDK Version : 2.756.0
I've been stuck on this for a while now. Any help is appreciated. Thank you.
Pre-signed URLs are created locally in the SDK so there's no need to use the asynchronous calls.
Instead, use a synchronous call to simplify your code, something like this:
const getPreSignedUrlS3 = (Bucket, Key, ContentType, Expires = 5 * 60) => {
const params = {
Bucket,
ContentType,
Key,
Expires
};
return s3.getSignedUrl("putObject", params);
}
What I want to do?
I want to create REST API that returns data from my DynamoDB table which is being created by GraphQL model.
What I've done
Create GraphQL model
type Public #model {
id: ID!
name: String!
}
Create REST API with Lambda Function with access to my PublicTable
$ amplify add api
? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this category in the project: rest
? Provide a path (e.g., /book/{isbn}): /items
? Choose a Lambda source Create a new Lambda function
? Provide an AWS Lambda function name: listPublic
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the category storage
? Storage has 8 resources in this project. Select the one you would like your Lambda to access Public:#model(appsync)
? Select the operations you want to permit for Public:#model(appsync) create, read, update, delete
You can access the following resource attributes as environment variables from your Lambda function
API_MYPROJECT_GRAPHQLAPIIDOUTPUT
API_MYPROJECT_PUBLICTABLE_ARN
API_MYPROJECT_PUBLICTABLE_NAME
ENV
REGION
? Do you want to invoke this function on a recurring schedule? No
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? No
Successfully added resource listPublic locally.
Next steps:
Check out sample function code generated in <project-dir>/amplify/backend/function/listPublic/src
"amplify function build" builds all of your functions currently in the project
"amplify mock function <functionName>" runs your function locally
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud
Succesfully added the Lambda function locally
? Restrict API access No
? Do you want to add another path? No
Successfully added resource rest locally
Edit my Lambda function
/* Amplify Params - DO NOT EDIT
API_MYPROJECT_GRAPHQLAPIIDOUTPUT
API_MYPROJECT_PUBLICTABLE_ARN
API_MYPROJECT_PUBLICTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
const AWS = require("aws-sdk");
const region = process.env.REGION
AWS.config.update({ region });
const docClient = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: "PublicTable"
}
async function listItems(){
try {
const data = await docClient.scan(params).promise()
return data
} catch (err) {
return err
}
}
exports.handler = async (event) => {
try {
const data = await listItems()
return { body: JSON.stringify(data) }
} catch (err) {
return { error: err }
}
};
Push my updates
$ amplify push
Open my REST API endpoint /items
{
"message": "User: arn:aws:sts::829736458236:assumed-role/myprojectLambdaRolef4f571b-dev/listPublic-dev is not authorized to perform: dynamodb:Scan on resource: arn:aws:dynamodb:us-east-1:8297345848236:table/Public-ssrh52tnjvcdrp5h7evy3zdldsd-dev",
"code": "AccessDeniedException",
"time": "2021-04-21T21:21:32.778Z",
"requestId": "JOA5KO3GVS3QG7RQ2V824NGFVV4KQNSO5AEMVJF66Q9ASUAAJG",
"statusCode": 400,
"retryable": false,
"retryDelay": 28.689093010346657
}
Problems
What I did wrong?
How do I access my table and why I didn't get it when I created it?
Why API_MYPROJECT_PUBLICTABLE_NAME and other constants are needed?
Decision
The problem turned out to be either the NodeJS version or the amplify-cli version. After updating amplify-cli and installing the node on the 14.16.0 version, everything worked.
I also changed the name of the table to what Amplify creates for us, although this code did not work before. The code became like this:
/* Amplify Params - DO NOT EDIT
API_MYPROJECT_GRAPHQLAPIIDOUTPUT
API_MYPROJECT_PUBLICTABLE_ARN
API_MYPROJECT_PUBLICTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
const AWS = require("aws-sdk");
const region = process.env.REGION
const tableName = process.env.API_MYPROJECT_PUBLICTABLE_NAME
AWS.config.update({ region });
const docClient = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: tableName
}
async function listItems(){
try {
const data = await docClient.scan(params).promise()
return data
} catch (err) {
return err
}
}
exports.handler = async (event) => {
try {
const data = await listItems()
return { body: JSON.stringify(data) }
} catch (err) {
return { error: err }
}
};
I'm unable to find out how to get the api key out of an apigateway key. I can get its ID and its ARN but not the value. I know you can specify the value when creating the key, but not how to retrieve it once created--short of logging into the AWS GUI and finding it that way.
I've looked at the documentation for aws-apigateway.ApiKey and couldn't find any way to get the value. https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-apigateway.ApiKey.html I've also looked at kms keys since you can get their value, but I don't know if it's usable in the context of an API Gateway usage plan (not included in code below).
Failing the ability to get the value, is there a way to generate a value that won't change, or will persist? I'm using an ephemeral Jenkins node to run the CDK.
const apiGateway = require('#aws-cdk/aws-apigateway');
...
const apiKey = new apiGateway.ApiKey(this, 'api-key', {
apiKeyName: 'my-api-key',
});
...
new cdk.CfnOutput(this, 'x-api-key-apiKey_id', {
value: apiKey.keyId
});
new cdk.CfnOutput(this, 'x-api-key-apiKey_keyArn', {
value: apiKey.keyArn
});
We can't retrieve the auto generated key via cdk/cloudformation without a custom resource. But we can generate the key , store it in a secret manager or an ssm secret and use that to create api key.
const secret = new secretsmanager.Secret(this, 'Secret', {
generateSecretString: {
generateStringKey: 'api_key',
secretStringTemplate: JSON.stringify({ username: 'web_user' }),
excludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/#"\\',
},
});
this.restApi.addApiKey('ApiKey', {
apiKeyName: `web-app-key`,
value: secret.secretValueFromJson('api_key').toString(),
});
I'm going to use https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SecretsManager.html#getRandomPassword-property to generate the 20 characters and set the API key. Since nothing outside of my stack needs the key I'm ok with regenerating it and updating my resources every time I do a deploy. However if there are things outside of the stack that need the key then using Balu's answer is the best option.
The reason for this is keeping a secret has a cost associated with it.
The accepted answer is perhaps not the best way to go about this.
It can be solved without creating an extra secret using aws-cdk's custom resources.
Here is a snippet that will get you the value of an api key. The value of this key is generated randomly by the api gateway.
import * as iam from "#aws-cdk/aws-iam";
import { RetentionDays } from "#aws-cdk/aws-logs";
import * as cdk from "#aws-cdk/core";
import {
AwsCustomResource,
AwsCustomResourcePolicy,
AwsSdkCall,
PhysicalResourceId,
} from "#aws-cdk/custom-resources";
import { IApiKey } from "#aws-cdk/aws-apigateway";
export interface GetApiKeyCrProps {
apiKey: IApiKey;
}
export class GetApiKeyCr extends cdk.Construct {
apikeyValue: string;
constructor(scope: cdk.Construct, id: string, props: GetApiKeyCrProps) {
super(scope, id);
const apiKey: AwsSdkCall = {
service: "APIGateway",
action: "getApiKey",
parameters: {
apiKey: props.apiKey.keyId,
includeValue: true,
},
physicalResourceId: PhysicalResourceId.of(`APIKey:${props.apiKey.keyId}`),
};
const apiKeyCr = new AwsCustomResource(this, "api-key-cr", {
policy: AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [props.apiKey.keyArn],
actions: ["apigateway:GET"],
}),
]),
logRetention: RetentionDays.ONE_DAY,
onCreate: apiKey,
onUpdate: apiKey,
});
apiKeyCr.node.addDependency(props.apiKey);
this.apikeyValue = apiKeyCr.getResponseField("value");
}
}
I am trying to create projects programatically through the resource manager API from a google cloud function like so:
exports.createProjectAsync = async (projectId, projectName) => {
const scopes = "https://www.googleapis.com/auth/cloud-platform"
const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?scopes=${scopes}`
const tokenResult = await fetch(url, {
headers: {
"Metadata-Flavor": "Google"
},
});
const tokenStatus = tokenResult.status;
functions.logger.log(`Call to get token has status ${tokenStatus}`);
const tokenData = await tokenResult.json()
functions.logger.log(`Call to get token has data ${JSON.stringify(tokenData)}`);
const accessToken = tokenData.access_token;
if (accessToken === null) {
throw new Error(`Failed to retrieve access token`);
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
};
const payload = {
"projectId": projectId,
"name": projectName,
"parent": {
"type": "folder",
"id": FOLDER_NUMBER
}
};
const projectsCreateUrl = `https://cloudresourcemanager.googleapis.com/v1/projects/`
const result = await fetch(projectsCreateUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
const status = result.status;
functions.logger.log(`Call to create project returned status ${status}`);
const data = await result.json()
functions.logger.log(`data: ${JSON.stringify(data)}`);
return data;
}
For testing purposes I've added the Organization Administrator role to the default service account. I cannot see the projects creator role in IAM:
When calling the API I get the following error:
{"error":{"code":403,"message":"The caller does not have permission","status":"PERMISSION_DENIED"}}
How can I successfully access this resource?
Although of course, it gives you the ability to modify its own permissions, as you can verify in the GCP documentation, the Organization Admin role does not allow to create a new project.
As you indicated, for that purpose the service account should be granted the Project Creator (roles/resourcemanager.projectCreator) role.
According to your screenshot, you are trying to grant this permission at the project level, but please, be aware that this role can only be granted at the organization and folder levels. This is the reason why the dropdown menu in the Google Cloud Web console is not providing you the Project Creator option.
If you have the necessary permissions over the folder or organization, try to assign that role at the corresponding level.
I used Amplify for authentication and it seems to work fine. Now I want to setup an admin app for CRUD with the user pool. It seems that I have to leave Amplify and use the JavaScript SDK to use the appropriate api's.
How does this work? I've failed at figuring out how to get the tokens I receive in Amplify into AWS.config or wherever they are supposed to go.
What a struggle this had been. It seems that the code in the docs is dated and what little advice is online is worse. I suspect that is because an Amplify object contains the config options and I have to bring those to the AWS.config object. I've tried below and failed. Any idea what I need to do? I'm sure answers here will be useful for many AWS newbies.
I have this code in my Angular app but thinking about Lambda. I have an EC2 server with Node.js as another option.
This is for dev on my MBP but I'm integrating it with AWS.
With the code below I get an error message that contains in part:
Error in getCognitoUsers: Error: Missing credentials in config
at credError (config.js:345)
at getStaticCredentials (config.js:366)
at Config.getCredentials (config.js:375)
The JWT I inserted into the object below is the AccessKeyID that is in my browser storage and I used for authentication.
In console.log cognitoidentityserviceprovider I have this object, in part:
config: Config
apiVersion: "2016-04-18"
credentialProvider: null
credentials: "eyJraWQiOiJwaUdRSnc4TWtVSlR...
endpoint: "cognito-idp.us-west-2.amazonaws.com"
region: "us-west-2"
endpoint: Endpoint
host: "cognito-idp.us-west-2.amazonaws.com"
hostname: "cognito-idp.us-west-2.amazonaws.com"
href: "https://cognito-idp.us-west-2.amazonaws.com/"
These functions flow down as a sequence. I left some vars in the bodies in case someone wants to know how to get this data from the user object. I used them in various attempts build objects but most aren't needed here, maybe. The all produce the correct results from the Amplify user object.
import { AmplifyService } from 'aws-amplify-angular';
import Amplify, { Auth } from 'aws-amplify';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
import * as AWS from 'aws-sdk';
#Injectable()
export class CognitoApisService {
private cognitoConfig = Amplify.Auth.configure(); // Data from main.ts
private cognitoIdPoolID = this.cognitoConfig.identityPoolId;
private cognitoUserPoolClient = this.cognitoConfig.userPoolWebClientId;
private cognitoIdPoolRegion = this.cognitoConfig.region;
private cognitoUserPoolID = this.cognitoConfig.userPoolId;
...
constructor(
private amplifyService: AmplifyService,
) { }
public getAccessToken() {
return this.amplifyService
.auth() // Calls class that includes currentAuthenticaedUser.
.currentAuthenticatedUser() // Sets up a promise and gets user session info.
.then(user => {
console.log('user: ', user);
this.accessKeyId = user.signInUserSession.accessToken.jwtToken;
this.buildAWSConfig();
return true;
})
.catch(err => {
console.log('getAccessToken err: ', err);
});
}
public buildAWSConfig() {
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: this.accessKeyId,
region: this.cognitoIdPoolRegion
});
this.cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider(this.AWSconfig);
/* This doesn't get creds, probably because of Amplify.
this.cognitoidentityserviceprovider.config.getCredentials(function(err) {
if (err) console.log('No creds: ', err); // Error: Missing credentials
else console.log("Access Key:", AWS.config.credentials.accessKeyId);
});
*/
console.log('cognitoidentityserviceprovider: ', this.cognitoidentityserviceprovider);
this.getCognitoUsers();
}
public getCognitoUsers() {
// Used for listUsers() below.
const params = {
UserPoolId: this.cognitoUserPoolID,
AttributesToGet: [
'username',
'given_name',
'family_name',
],
Filter: '',
Limit: 10,
PaginationToken: '',
};
this.cognitoidentityserviceprovider.listUsers(params, function (err, data) {
if
(err) console.log('Error in getCognitoUsers: ', err); // an error occurred
else
console.log('all users in service: ', data);
});
}
The one problem was that the credentials needs the whole user object from Amplify, not just the access token as I show above. By the way, I have the Cognito settings in main.ts. They can also go in environment.ts. A better security option is to migrate this to the server side. Not sure how to do that yet.
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: this.accessKeyId, // Won't work.
region: this.cognitoIdPoolRegion
});
My complete code is simpler and now an observable. Notice another major issue I had to figure out. Import the AWS object from Amplify, not the SDK. See below.
Yeah, this goes against current docs and tutorials. If you want more background on how this has recently changed, even while I was working on it, see the bottom of this Github issue. Amplify is mostly for authentication and the JavaScript SDK is for the service APIs.
import { AmplifyService } from 'aws-amplify-angular';
// Import the config object from main.ts but must match Cognito config in AWS console.
import Amplify, { Auth } from 'aws-amplify';
import { AWS } from '#aws-amplify/core';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
// import * as AWS from 'aws-sdk'; // Don't do this.
#Injectable()
export class CognitoApisService {
private cognitoConfig = Amplify.Auth.configure(); // Data from main.ts
private cognitoIdPoolRegion = this.cognitoConfig.region;
private cognitoUserPoolID = this.cognitoConfig.userPoolId;
private cognitoGroup;
private AWSconfig;
// Used in listUsers() below.
private params = {
AttributesToGet: [
'given_name',
'family_name',
'locale',
'email',
'phone_number'
],
// Filter: '',
UserPoolId: this.cognitoUserPoolID
};
constructor(
private amplifyService: AmplifyService,
) { }
public getCognitoUsers() {
const getUsers$ = new Observable(observer => {
Auth
.currentCredentials()
.then(user => {
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: user, // The whole user object goes in the config.credentials field! Key issue.
region: this.cognitoIdPoolRegion
});
const cognitoidentityserviceprovider = new CognitoIdentityServiceProvider(this.AWSconfig);
cognitoidentityserviceprovider.listUsers(this.params, function (err, userData) {
if (err) {
console.log('Error in getCognitoUsers: ', err);
} else {
observer.next(userData);
}
});
});
});
return getUsers$;
}
Let's call this service from a component. I'm putting the JS object parsing in the component but for now, I left the console.log here for you to get started and see if the code works for your app.
// Called from button on html component.
public getAllCognitoUsers() {
this.cognitoApisService.getCognitoUsers()
.subscribe(userData => {
console.log('data in cognito component: ', userData);
})
}