CloudFront with Lambda#Edge results in CORS problem - amazon-web-services

I've configured S3 with access only through CloudFront and protected with lambda executed on Viewer request. The problem is that I'm not able to access the files from SPA because of a failing preflight call.
When I removed the lambda function everything is starting to work. This is surprising to me because lambda is not modifying the request at all.
Here is my configuration:
S3:
CloudFront:
Lambda#Edge (executed at Viewer request)
exports.handler = async (event, context, callback) => {
let request;
let token;
try {
request = event.Records[0].cf.request;
const headers = request.headers;
const authorization = headers['authorization'][0];
const authorizationValue = authorization.value;
token = authorizationValue.substring(7);
} catch (error) {
console.error("Missing authorization header", error);
callback(null, missingAuthorizationHeaderResponse);
}
if (token) {
try {
if (!secret) {
secret = await getSecret();
}
jwt.verify(token, secret);
console.log("Token valid");
callback(null, request);
} catch (error) {
console.error("Token not valid", error);
callback(null, invalidTokenResponse);
}
} else {
console.error("Token not found");
callback(null, missingAuthorizationHeaderResponse);
}
};
I will be very grateful for help since I've spent a lot of time on this case, thanks!

The problem is that the preflight call are executed without any additional headers and in my case "authorization" header was missing and was generating 403. I found that by looking into logs of the lambda. I've added handling of options call to the lambda. Also I had to change s3 config to have the response with visible CORS headers.
Lambda Code:
const preflightCall = {
status: "204",
headers: {
'access-control-allow-origin': [{
key: 'Access-Control-Allow-Origin',
value: "*",
}],
'access-control-request-method': [{
key: 'Access-Control-Request-Method',
value: "PUT, GET, OPTIONS, DELETE",
}],
'access-control-allow-headers': [{
key: 'Access-Control-Allow-Headers',
value: "*",
}]
},
};
exports.handler = async (event, context, callback) => {
let request;
let token;
try {
request = event.Records[0].cf.request;
if(request.method === 'OPTIONS') {
console.log('preflight call');
callback(null, preflightCall);
return;
}
const headers = request.headers;
const authorization = headers['authorization'][0];
const authorizationValue = authorization.value;
token = authorizationValue.substring(7);
} catch (error) {
console.error("Missing authorization header", error);
callback(null, missingAuthorizationHeaderResponse);
}
if (token) {
try {
if (!secret) {
secret = await getSecret();
}
jwt.verify(token, secret);
console.log("Token valid");
callback(null, request);
} catch (error) {
console.error("Token not valid", error);
callback(null, invalidTokenResponse);
}
} else {
console.error("Token not found");
callback(null, missingAuthorizationHeaderResponse);
}
};
S3:
[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD",
"DELETE",
"POST",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"Access-Control-Allow-Origin"
],
"MaxAgeSeconds": 3000
}
]

Related

AWS Cognito revoke apple login

5.1.1 Legal: Privacy - Data Collection and Storage
Apps that offer Sign in with Apple should use the Sign in with Apple REST API to revoke user tokens.
REST API to revoke user tokens doc https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens/
I am getting "invalid client" or "invalid_grant" in the error response.
const client_secret = await getClientSecret();
const token = await authToken(authCode,client_secret);
await revokeToken(refreshToken, client_secret);
async function authToken(authCode, client_secret) {
const data = querystring.stringify({
code: authCode,
client_id: "****",
client_secret: client_secret,
grant_type: "authorization_code",
});
var config = {
method: "post",
url: "https://appleid.apple.com/auth/token",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
};
try {
const response = await axios.post(config.url, data, {
headers: config.headers,
});
return response.data.refresh_token
} catch (err) {
return err.response.data.error;
}
}
async function getClientSecret() {
const privateKey = fs.readFileSync("key/path");
return jwt.sign(
{
iss: "****",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 360000,
aud: "https://appleid.apple.com",
sub: "****",
},
privateKey,
{
algorithm: "ES256",
header: {
alg: "ES256",
kid: "***",
},
}
);
}

AWS post request works on postman but not react-native

This is my JS code for the API
export const getUser = async (user) => {
//Working
const json = await fetch( "*****/username/getUser", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user:user
}),
})
.then((res) => {
return res.json();
})
.catch((err) => {
console.log("Error in getUser: " + err);
});
return json;
};
Here is an attempt to make the request, which unfortunately return the authentication error.
fetchUser.request("kirolosM")
.then((result)=>{
console.log(result);
}).catch((err)=>{console.log("Error ",err);})
The error
{
"message": "Missing Authentication Token"
}
I have tested the API using postman and it is working as expexted.
Probably useful to compare the request sent from your json code to the one you're sending from postman. It looks like you need to include you auth token in your headers in your request.
Something like
export const getUser = async (user) => {
//Working
const json = await fetch( "*****/username/getUser", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authentication": `Bearer ${token}`
},
...

AWS S3 Bucket Policy not working for PUTting files from frontend Javascript

I have a bucket on AWS S3. If all is public i can easily upload files with:
startUpload = ev => {
const { file } = this.state
const { name: filename, type: filetype } = file
axios.put(`https://bucket-name.s3.amazonaws.com/${file.name}`, file, { headers: {
'Content-Type': filetype
}})
.then(res => console.log('success', res))
.catch(err => console.error(err))
}
I have made the bucket private and set up the bucket policy as such:
(generated from the policy generator)
{
"Version": "2012-10-17",
"Id": "Policy1564615030380",
"Statement": [
{
"Sid": "Stmt1564615027886",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::110286134735:user/andrepadez"
},
"Action": [
"s3:GetObject",
"s3:GetObjectAcl",
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::bucket-name/*"
}
]
}
now my frontend code looks like this:
startUpload = async ev => {
const { file } = this.state
const { name: filename, type: filetype } = file
this.setState({ uploading: true })
const response = await axios
.get(`http://localhost:8080/get-signed-url?filename=${filename}&filetype=${filetype}`)
const { signedUrl } = response.data
console.log('uploading')
axios.put(signedUrl, file, { headers: {
'Content-Type': filetype
}})
.then(res => console.log('success', res) || this.setState({ uploading: false }))
.catch(err => console.error(err) || this.setState({ uploading: false }))
}
and my backend code:
aws.config.update({
accessKeyId: AWS_ACCESS_KEY,
secretAccessKey: AWS_SECRET_ACCESS_KEY
})
const s3 = new aws.S3()
const app = express()
app.use(cors())
app.get('/get-signed-url', (req, res) => {
const { filename, filetype } = req.query
const params = {
Bucket: AWS_BUCKET,
Key: filename,
// Expires: 60,
ContentType: filetype
}
s3.getSignedUrl('putObject', params, function(err, signedUrl) {
if (err) {
console.log(err)
res.send(err)
} else {
// const signature = signedUrl.match(/Signature=(\S+)/)[1]
res.send({ signedUrl })
}
})
})
I get the following error message when trying to upload:
I don't see any error in your code. What I see though is that the URL seems to be completely wrong. Structure of signed url follows this pattern:
https://bucketname.s3.amazonaws.com/filename?AWSAccessKeyId=AKIAIHPOEVTLP7M7CKJA&Content-Type=text%2Fhtml&Expires=1564634878&Signature=rD0zsXlVB7Usax9r12Z
Look at the bucket name, in your case it is something like
clineage-watch-data clineage-watch-data-...PWboty...
Bucket name cannot contain space in it as well as it cannot contain upper case letters. Double check the name of your bucket and make sure that AWS_BUCKET holds the right value. Also, place console log right before you are sending the signed URL from the backend to verify that the URL is correct and diagnose whether there is an error in bucket name or whether something happens on the frontend when you receive the URL.
Also, try to avoid using permanent credentials in your application if possible, it is agains security best practices.

Failed use apigwManagementApi.postToConnection in $connect route

I want to return connectionId to a client after the client connect to aws websocket.
I'm using apigwManagementApi.postToConnection to send a response to a client, but I always get an absurd error message.
I already try to debug & search in google, but I can't find a solution for this.
patch.js
require('aws-sdk/lib/node_loader');
var AWS = require('aws-sdk/lib/core');
var Service = AWS.Service;
var apiLoader = AWS.apiLoader;
apiLoader.services['apigatewaymanagementapi'] = {};
AWS.ApiGatewayManagementApi = Service.defineService('apigatewaymanagementapi', ['2018-11-29']);
Object.defineProperty(apiLoader.services['apigatewaymanagementapi'], '2018-11-29', {
get: function get() {
var model = {
"metadata": {
"apiVersion": "2018-11-29",
"endpointPrefix": "execute-api",
"signingName": "execute-api",
"serviceFullName": "AmazonApiGatewayManagementApi",
"serviceId": "ApiGatewayManagementApi",
"protocol": "rest-json",
"jsonVersion": "1.1",
"uid": "apigatewaymanagementapi-2018-11-29",
"signatureVersion": "v4"
},
"operations": {
"PostToConnection": {
"http": {
"requestUri": "/#connections/{connectionId}",
"responseCode": 200
},
"input": {
"type": "structure",
"members": {
"Data": {
"type": "blob"
},
"ConnectionId": {
"location": "uri",
"locationName": "connectionId"
}
},
"required": [
"ConnectionId",
"Data"
],
"payload": "Data"
}
}
},
"shapes": {}
}
model.paginators = {
"pagination": {}
}
return model;
},
enumerable: true,
configurable: true
});
module.exports = AWS.ApiGatewayManagementApi;
index.js
const AWS = require('aws-sdk');
require('./patch.js');
exports.handler = async(event) => {
const connectionId = event.requestContext.connectionId;
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: event.requestContext.domainName + '/' + event.requestContext.stage
});
await apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: connectionId }).promise();
return {};
};
client.js
const WebSocket = require('ws');
const ws = new WebSocket('wss://****');
ws.on('open', () => {
console.log('connected ===================>')
ws.on('message', data => console.warn(`From server: ${data}`));
});
Error in cloudwatch
{
"errorMessage": "410",
"errorType": "UnknownError",
"stackTrace": [
"Object.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:48:27)",
"Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/rest_json.js:52:8)",
"Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:105:20)",
"Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10)",
"Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14)",
"Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)",
"AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)",
"/var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10",
"Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)",
"Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12)"
]
}
I don't know why, but if I'm trying in a custom route, this code can work.
Does anyone know how to solve this?
I'd suggest to look into this example from AWS, there is on connect response for subprotocol confirmation, but I think any payload can be provided.
The most important bit is the route integration settings in the template, basically, the following two lines in the route integration properties:
IntegrationMethod: POST
ConnectionType: INTERNET
then response will be sent to the connected client.
The only way I've found to make this work is to use a DynamoDB table to store connections, then set up a trigger from the table back to a Lambda function.
There are a few catches though. This Lambda function wont work like your index.js file above. You'll have to use NPM install --save aws-sdk on a folder with your index.js file, zip it and upload it to the lambda function, so that the SDK is localized.
You will also need to set up a user with proper access and put the credentials into a your Lambda function.
Note, if you see a 410 error, that means the connection is no longer there, so you're going in the right direction at that point.
const AWS = require('aws-sdk');
require('./patch.js');
var log = console.log;
AWS.config.update({
accessKeyId: "YOURDATAHERE",
secretAccessKey: "YOURDATAHERE"
});
let send = undefined;
function init() {
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: "HARDCODEYOURENDPOINTHERE"
});
send = async (connectionId, data) => {
await apigwManagementApi.postToConnection({ ConnectionId: connectionId, Data: `${data}` }).promise();
}
}
exports.handler = async (event, context) => {
init();
console.log('Received event:', JSON.stringify(event, null, 2));
for (const record of event.Records) {
//console.log(record.eventID);
console.log(record.eventName);
console.log('DynamoDB Record: %j', record.dynamodb);
if(record.eventName == "INSERT"){
var connectionId = record.dynamodb.NewImage.connectionId.S;
try{
await send(connectionId, connectionId);
}catch(err){
log("Error", err);
}
log("sent");
}
}
return `Successfully processed ${event.Records.length} records.`;
};

loopback rest connection headers authorization

I'm trying to access the Shopify Orders API in a Loopback application. I have the following data source:
"ShopifyRestDataSource": {
"name": "ShopifyRestDataSource",
"connector": "rest",
"operations": [{
"template": {
"method": "GET",
"url": "https://mystore.myshopify.com/admin",
"headers": {
"accepts": "application/json",
"content-type": "application/json"
}
},
"headers": {
"Authorization": "Basic MzdiOD..."
},
"functions": {
"find": []
}
}]
}
And then I attempt a simple call:
var ds = app.dataSources.ShopifyRestDataSource;
ds.find(function(err, response, context) {
if (err) throw err;
if (response.error) {
next('> response error: ' + response.error.stack);
}
console.log(response);
next();
});
I'm getting the following exception message:
Error: {"errors":"[API] Invalid API key or access token (unrecognized login or wrong password)"}
at callback (/order-api/node_modules/loopback-connector-rest/lib/rest-builder.js:529:21)
The Shopify API authenticates by basic HTTP authentication and I'm sure my request works since the same data works with curl. What am I doing wrong?
I couldn't find the "Loopback way" to do this and I couldn't wait, so I just wrote a simple https Node call. I'll paste this in here but I won't accept it as the answer. I'm still hoping someone will provide the right answer.
let response;
const options = {
hostname: 'mystore.myshopify.com',
port: 443,
path: '/admin/orders.json',
method: 'GET',
auth: `${instance.api_key}:${instance.password}`
};
const req = https.request(options, (res) => {
res.setEncoding('utf8');
let body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
let jsonResponse = JSON.parse(body);
// application logic goes here
response = 'ok';
});
});
req.on('error', (e) => {
response = e.message;
});
req.end();