CloudFront responds with 413 after AWS Cognito redirect - amazon-cognito

I have a React app built using Serverless NextJS and served behind AWS CloudFront. I am also using AWS Cognito to do authentication of our users.
After a user successfully authenticates through AWS Cognito, they are redirected to my React App with a query string containing OAuth tokens (id_token, access_token, refresh_token, raw[id_token], raw[access_token], raw[refresh_token], raw[expires_in], raw[token_type]).
It seems that the query string is simply larger than AWS CloudFront's limits and it is throwing the following error below:
413 ERROR
The request could not be satisfied.
Bad request. We can't connect to the server for this app...
Generated by cloudfront (CloudFront)
Request ID: FlfDp8raw80pAFCvu3g7VEb_IRYbhHoHBkOEQxYyOTWMsNlRjTA7FQ==
This error has been encountered before by many other users (see example). Keen to know:
Are there any workarounds? Perhaps is there a way to configure AWS Cognito to reduce the number of tokens that it is passing in the query string by default?
Is it possible to configure AWS CloudFront to ignore enforcing its default limits on certain pages (and not cache theme)?
What's the suggestion going forward? The only thing I can imagine is not to use AWS CloudFront.

After analysing the query fields that AWS Cognito sends to a callback URL, I was able to determine that not all fields are required for my usecase. Particularly the raw OAuth token fields.
With that information, I solved the problem by writing a "middleware" to intercept my backend system redirecting to my frontend (that is sitting behind CloudFront) and trimming away query string fields that I do not need to complete authentication.
In case this could inspire someone else stuck with a similar problem, here is what my middleware looks like for my backend system (Strapi):
module.exports = (strapi) => {
return {
initialize() {
strapi.app.use(async (ctx, next) => {
await next();
if (ctx.request.url.startsWith("/connect/cognito/callback?code=")) {
// Parse URL (with OAuth query string) Strapi is redirecting to
const location = ctx.response.header.location;
const { protocol, host, pathname, query } = url.parse(location);
// Parse OAuth query string and remove redundant (and bloated) `raw` fields
const queryObject = qs.parse(query);
const trimmedQueryObject = _.omit(queryObject, "raw");
// Reconstruct original redirect Url with shortened query string params
const newLocation = `${protocol}//${host}${pathname}?${qs.stringify(
trimmedQueryObject
)}`;
ctx.redirect(newLocation);
}
});
},
};
};

Related

Identifying an authenticated user in back-end page requested via window.open

We use IdentityServer4 in our .NET solution, which also includes AspNetCore Web API and Angular front-end app.
There are standard middleware settings to setup identity like app.UseAuthentication() / app.UseAuthorization() / etc. (it's actually an ABP framework-based solution). As a result of these settings, all authentication tokens (access_token, refresh_token, etc.) are stored in Local Storage (I have not found where exactly I can select between Local Storage and other kinds, BTW).
Anyway, it has worked somehow in our DEV environment.
But suddenly the need to use window.open from Angular app popped up. This page is a Hangfire dashboard. Which accesses other resources related to dashboard functionality. And it caused a lot of headache: now, to identify user in server page called from window.open we need to use cookies (URL is not considered of course).
does it mean we have to switch fully from Local Storage to Cookies for storing tokens?
how and where to set it up? Or, if it's not too wild and senseless to just copy existing access_token to Cookies - when and where to do that? My idea was to copy access_token when it is created in Local Storage to Cookies and delete when a user logs off and probably under bunch of different conditions, like browser window is closed, etc. (which ones exactly?)
probably to set refresh_token to be stored in Cookies and read it in the mentioned server page, then obtain access_token by it (if it makes sense at all)
UPDATE: i've finally came up with the following, but it does not work. The idea is: when Angular app makes request to back-end and authentication token is already present - the token needs to be saved to cookies. I see the cookies is added. But later on - on next request - it falls into the condition again, because actually the given cookie is not saved:
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (context.SecurityToken is JwtSecurityToken accessToken && !context.HttpContext.Request.Cookies.ContainsKey(accessTokenCookieName))
{
context.HttpContext.Response.Cookies.Append(
accessTokenCookieName,
accessToken.RawData,
new CookieOptions
{
Domain = context.HttpContext.Request.Host.Host,
Path = "/",
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Secure = true,
MaxAge = TimeSpan.FromMinutes(60)
});
}
return Task.CompletedTask;
},
...
}
All apps are web application sharing the same domain. And CORS is set up. AllowCredentials() is called as well when setting up middleware.

How To Add 'Authorization' Header to S3.upload() Request?

My code uses the AWS Javascript SDK to upload to S3 directly from a browser. Before the upload happens, my server sends it a value to use for 'Authorization'.
But I see no way in the AWS.S3.upload() method where I can add this header.
I know that underneath the .upload() method, AWS.S3.ManagedUpload is used but that likewise doesn't seem to return a Request object anywhere for me to add the header.
It works successfully in my dev environment when I hardcode my credentials in the S3() object, but I can't do that in production.
How can I get the Authorization header into the upload() call?
Client Side
this posts explains how to post from a html form with a pre-generated signature
How do you upload files directly to S3 over SSL?
Server Side
When you initialise the S3, you can pass the access key and secret.
const s3 = new AWS.S3({
apiVersion: '2006-03-01',
accessKeyId: '[value]',
secretAccessKey: '[value]'
});
const params = {};
s3.upload(params, function (err, data) {
console.log(err, data);
});
Reference: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html
Alternatively if you are running this code inside AWS services such as EC2, Lambda, ECS etc, you can assign a IAM role to the service that you are using. The permissions can be assigned to the IAM Role
I suggest that you use presigned urls.

AWS Cognito React Native Webview Auth

In my react native app, I want to pass along a user’s AWS cognito credentials to a WebView inside the app so that it can be used to access files which are stored on a private S3 bucket.
So basically I have the following working:
- log into Cognito (via aws-amplify’s Auth class)
- Security on the S3 bucket allowing only logged in users to have access to its content.
I have tried to send the headers to the Webview
<WebView
source={{
uri: source,
headers: {
Authorization:
"AWS4-HMAC-SHA256 …”
}
}}
But that does not seem to work. Does anyone know how to do this?
Ok, after many emails to AWS entreprise support team members, and many hours of hair pulling; I have found out that S3 does not currently support passing along credentials from Cognito.
What we can do is:
Place CloudFront in front of S3, and use Origin Access Identity (OAI) to protect the data. This works well to securitize the access, HOWEVER it does not allow me to pass along the credentials to S3. This is because the communication between CloudFront and S3 now pas the OAI which means a single identity for all users.
Sign each of the S3 access URLs that you need to access.
I used the latter as I need to restrict user access. The code to sign the URLs that I used was:
In react-native:
import AWS, { Auth, Storage } from "aws-amplify";
Storage.get("image.jpg").then(result => {
console.log(result);
}).catch(err => {
console.log(err);
});
In node.js:
import AWS from "aws-sdk";
AWS.config.update({ accessKeyId, secretAccessKey, region });
const s3 = new AWS.S3();
s3.getSignedUrl("getObject", {
Bucket: "s3-bucket-name",
Key: "my/path/image.jpg",
Expires: 60 * 5 * 1000, // 5 Minutes
});
I hope it can help others.

AWS API Gateway as proxy for HTTP AUTH

I am dealing with some legacy applications and want to use Amazon AWS API Gateway to mitigate some of the drawbacks.
Application A, is able to call URLs with parameters, but does not support HTTP basic AUTH. Like this:
https://example.com/api?param1=xxx&param2=yyy
Application B is able to handle these calls and respond. BUT application B needs HTTP basic authentication.
The question is now, can I use Amazon AWS API Gateway to mitigate this?
The idea is to create an API of this style:
http://amazon-aws-api.example.com/api?authcode=aaaa&param1=xxx&param2=yyy
Then Amazon should check if the authcode is correct and then call the API from Application A with all remaining parameters while using some stored username+password. The result should just be passed along back to Application B.
I could also give username + password as a parameter, but I guess using a long authcode and storing the rather short password at Amazon is more secure. One could also use a changing authcode like the ones used in 2-factor authentications.
Path to a solution:
I created the following AWS Lambda function based on the HTTPS template:
'use strict';
const https = require('https');
exports.handler = (event, context, callback) => {
const req = https.get(event, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => body += chunk);
res.on('end', () => callback(null, body));
});
req.on('error', callback);
req.end();
};
If I use the Test function and provide it with this event it works as expected:
{
"hostname": "example.com",
"path": "/api?param1=xxx&param2=yyy",
"auth": "user:password"
}
I suppose the best way from here is to use the API gateway to provide an interface like:
https://amazon-aws-api.example.com/api?user=user&pass=pass&param1=xxx&param2=yyy
Since the params of an HTTPs request are encrypted and they are not stored in Lambda, this method should be pretty secure.
The question is now, how to connect the API gateway to the Lambda.
You can achieve the scenario mentioned with AWS API Gateway. However it won't be just a proxy integration, rather you need to have a Lambda function which will forward the request by doing the transformation.
If the credentials are fixed credentials to invoke the API, then you can use the environmental variables in Lambda to store them, encrypted by using AWS KMS Keys.
However if the credentials are sent for each user (e.g logged into the application from a web browser) the drawbacks of this approach is that you need to store username and password while also retrieving it. Its not encourage to store passwords even encrypted. If this is the case, its better to passthrough (Also doing the transformations) rather storing and retrieving in between.

Authentication in GraphQL servers

How to properly handle authentication in GraphQL servers?
Is it ok to pass a JWT token at the Authorization header of query/mutation requests?
Should I use something from GraphQL specification?
Stateless solutions is preferable.
Thanks.
A while ago I was wondering the same thing for sometime,
but apparently authentication is out of the scope of what GraphQL is trying to accomplish (see the conversations on Github).
But there are solutions such as this which handles it with sessions.
Assuming you use express-graphql, here is what you can do.
import graphQLHTTP from 'express-graphql'
app.use(`/graphql`, [aValidationFunction, graphQLHTTP(options)])
function aValidationFunction(req, res, next) {
const { authorization } = req.headers
// Do your validation here by using redis or whatever
if (validUser) {
return next()
} else {
return res.status(403)
}
}
It depends on whether your GraphQL consumer is a webapp or mobileapp.
If it is a webapp, then I would recommend sticking with session-cookie-based authentication since most popular web frameworks support this, and you also get CSRF protection.
If it is a mobileapp, then you will want JWT. You can try manually getting a cookie header from login response, and put stuff this "cookie" in your next request, but I had problem that some proxy servers strip off this "cookie", leaving your request unauthenticated. So as you said, including JWT in every authenticated request (GraphQL request) is the way to go.