I am using CloudFront as cache in front of my Symfony web application. To get a cache based on a user's role (admin, customer,...) I generate a user role based hash in a Lambda#Edge Viewer Request trigger. I pass that hash on as a request header to my origin as X-User-Context-Hash.
My problem is now that I need to pass the PHPSESSID cookie on to my origin to get the right response for caching, but I do not want to base the cache on the value of PHPSESSID. I do only need my cached response to be based on the value of X-User-Context-Hash but not on my session cookie.
The image below should explain my problem in detail
Is there any possibility to accomplish that?
Would appreciate any help.
Here's my Lambda#Edge Viewer Request trigger:
'use strict';
function parseCookies(headers) {
const parsedCookie = {};
if (headers.cookie) {
console.log(`${headers.cookie[0].value}`);
headers.cookie[0].value.split(';').forEach((cookie) => {
if (cookie) {
const parts = cookie.split('=');
parsedCookie[parts[0].trim()] = parts[1].trim();
}
});
}
return parsedCookie;
}
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const https = require('https');
// Read session cookie
const parsedCookies = parseCookies(headers);
let cookie = '';
if (parsedCookies) {
if(parsedCookies['PHPSESSID']) {
cookie = `PHPSESSID=${parsedCookies['PHPSESSID']}`;
}
}
console.log(`Cookie: ${cookie}`);
// Send request to origin host at /_fos_user_context_hash
// passing the original session cookie
const options = {
hostname: `${request.headers.host[0].value}`,
port: 443,
path: '/_fos_user_context_hash',
method: 'HEAD',
headers: {
'Cookie': cookie,
'Accept': 'application/vnd.fos.user-context-hash',
'Vary' : 'Cookie'
}
};
const req = https.request(options, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
// Read the X-User-Context-Hash from the hash endpoint
const headerName = 'X-User-Context-Hash';
let hash = 'anonymous';
if (res.headers[headerName.toLowerCase()]) {
hash = res.headers[headerName.toLowerCase()];
}
// Append X-User-Context-Hash before passing request on to CF
request.headers[headerName.toLowerCase()] = [{ key: headerName, value: hash }];
callback(null, request);
}).on('error', (e) => {
console.error(e);
// Forward request anyway
callback(null, request);
});
req.end();
}
;
Here's how I finally solved my problem:
CloudFront behavior
I configured the behavior not to forward any cookies to the origin, but only cache based on the headers Host and X-User-Context-Hash (see screenshot).
The following image explains my lambda#edge process:
In the "Viewer Request" trigger I read the user-based cookies named PHPSESSID and REMEMBERME and pass those values via the X-Session-Cookies header on.
If the there's a match for my request url and the given Host and X-User-Context-Hash headers, Cloud-Front returns the cached item and stops here.
If there's no match the "Origin Request" trigger is fired. When that event fires the custom header X-Session-Cookies is available. So I take the value from the X-Session-Cookies header and set the value of request.headers.cookie to that value. This step ensures that the PHPSESSID and REMEMBERME cookie are both passed to the origin before the page gets cached.
My Lambda#Edge functions:
The Viewer Request trigger:
'use strict';
function parseCookies(headers) {
const parsedCookie = {};
if (headers.cookie) {
console.log(`${headers.cookie[0].value}`);
headers.cookie[0].value.split(';').forEach((cookie) => {
if (cookie) {
const parts = cookie.split('=');
parsedCookie[parts[0].trim()] = parts[1].trim();
}
});
}
return parsedCookie;
}
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const https = require('https');
let sessionId = '';
// Read session cookie
const parsedCookies = parseCookies(headers);
let cookie = '';
if (parsedCookies) {
if(parsedCookies['PHPSESSID']) {
cookie = `PHPSESSID=${parsedCookies['PHPSESSID']}`;
}
if(parsedCookies['REMEMBERME']) {
if (cookie.length > 0) {
cookie += ';';
}
cookie += `REMEMBERME=${parsedCookies['REMEMBERME']}`;
}
}
console.log(`Cookie: ${cookie}`);
// Send request to origin host at /_fos_user_context_hash
// passing the original session cookie
const options = {
hostname: `${request.headers.host[0].value}`,
port: 443,
path: '/_fos_user_context_hash',
method: 'HEAD',
headers: {
'Cookie': cookie,
'Accept': 'application/vnd.fos.user-context-hash',
'Vary' : 'Cookie'
}
};
const req = https.request(options, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
// Read the X-User-Context-Hash from the hash endpoint
const headerName = 'X-User-Context-Hash';
let hash = 'anonymous';
if (res.headers[headerName.toLowerCase()]) {
hash = res.headers[headerName.toLowerCase()];
}
// Append X-User-Context-Hash before passing request on to CF
request.headers[headerName.toLowerCase()] = [{ key: headerName, value: hash }];
const sessionHeaderName = 'X-Session-Cookies';
request.headers[sessionHeaderName.toLowerCase()] = [{ key: sessionHeaderName, value: cookie }];
callback(null, request);
}).on('error', (e) => {
console.error(e);
// Forward request anyway
callback(null, request);
});
req.end();
}
;
The Origin Request trigger:
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const sessionHeaderName = 'X-Session-Cookies';
let cookie = '';
if (request.headers[sessionHeaderName.toLowerCase()]) {
console.log(request.headers[sessionHeaderName.toLowerCase()]);
cookie = request.headers[sessionHeaderName.toLowerCase()][0].value;
}
request.headers.cookie = [{ key : 'Cookie', value : cookie }];
callback(null, request);
};
AWS recently introduced cache and origin request policies, allowing more customization.
You can now set cache behavior based on "All-except" list of cookies/query string params by setting appropriate cache policy and set origin request policy to forward only necessary data:
The fundamental problem:
If you configure CloudFront to forward cookies to your origin, CloudFront caches based on cookie values. This is true even if your origin ignores the cookie values in the request...
http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Cookies.html
This is by design. Cookies you forward are always part of the cache key.
There is no clean/simple/obvious workaround.
You could add the session cookie to the query string in the viewer request trigger, and configure that parameter to forward but not be used for caching, and then your origin would need to find it there and interpret it as a cookie. Query string parameters, unlike cookies, can be configured for forwarding, but not caching.
You could potentially replace the cookie in the actual request with a dummy/placeholder value, one per user class, so that it would be forwarded to the origin and used for caching, and then use a viewer-response trigger to prevent any Set-Cookie response from the origin (or cache) from exposing that magic cookie to any viewers.
Really, though, it sounds as if you may be trying to solve a problem in one place that really needs to be solved in another. Your application has a limitation in its design that is not cache friendly for certain resources. Those resources need to be designed to interact in a cache-friendly way, which is of course a fundamentally tricky proposition when access to the resource requires authenticated identification of a user, role, group, permission, etc.
Something similar I did to be able to forward cookies to an ALB configured for sticky sessions without cloudfront using the cookies to cache. The reason for this is because cloudfront will use the cookies and their values when matching requests and cached responses but because the ALB creates a new session cookie on each response, the request never matches the cache since the viewer always has a new cookie value set.
I too moved the cookie to a custom header in the viewer request and then pulled it out of the header and placed in back in the cookie in the origin request. I used the same lambda function for the viewer request and origin request and just checked the config property to determine which trigger it was. I prefer this pattern so that I can follow the logic from viewer request to origin request when reading/writing the code since the callback response from the viewer requests becomes the event request for the origin request and I could run my tests on a single function. The logic is based on three flows:
where there is no AWSALB cookie or customer header at all - in that case do nothing
where there is the AWSALB cookie but no other cookies in the request
where this is the AWSALB cookie along with other cookies
Using these three use cases the function was able to work properly.
Here's the function:
exports.handler = (event, context, callback) => {
// TODO implement
const util = require('util');
const COOKIE_TO_FORWARD = 'AWSALB';
let hasTheHeader = (request, headerKey) => {
if (request.headers[headerKey]) {
return true;
}
else return false;
}
//Returns the cookie key name from the value of the cookie header in the request
//let getCookieKey = cookieString => cookieString.slice(0,cookieString.indexOf("="));
const request = event.Records[0].cf.request
if(event.Records[0].cf.config.eventType == 'viewer-request') {
console.log('Viewer Request');
console.log(`viewer request – ${util.inspect(event, {showHidden: false, depth: null})}`);
hasTheHeader(event.Records[0].cf.request, 'cookie') ? console.log(`This request has cookies`) : console.log(`This request does NOT have cookies`);
// First check – If no cookies in Viewer Request, do nothing
if (!hasTheHeader(request, 'cookie')) {
console.log('viewer request first check evaluated - no cookies');
//pass request onto cloudfront cacheing layer or origin request
callback(null, request);
return;
}
// else there is a cookie header so get the list of cookies and put them in an array
let cookieList = request.headers.cookie[0].value.split('; ');
console.log(cookieList);
// Second check - If only the COOKIE_TO_FORWARD cookie exists and no other cookies, move it to a custom header and delete the cookie header
if ( (cookieList.length == 1) && (cookieList[0].startsWith(COOKIE_TO_FORWARD)) ) {
console.log('viewer request second check evaluated - only the COOKIE_TO_FORWARD cookie exists, no other cookies')
//move awsalb to custom header - format is important
request.headers.awsalbkey = [{'key': 'awsAlbKey', 'value': cookieList[0]}];
//remove cookie header
delete request.headers.cookie;
console.log(util.inspect(request, {showHidden: false, depth: null}));
//pass request onto cloudfront cacheing layer or origin request
callback(null, request);
return;
}
// Third check - If there are multiple cookies including the COOKIE_TO_FORWARD cookie, move only the COOKIE_TO_FORWARD cookie to a custom header and delete the cookie COOKIE_TO_FORWARD cookie
// get awsAlb cookie
const indexOfAwsALbCookie = cookieList.findIndex(element => element.startsWith('AWSALB='));
if ( (cookieList.length > 1) && (indexOfAwsALbCookie > -1) ) {
console.log('viewer request third check evaluated - the COOKIE_TO_FORWARD cookie exists along with other cookies')
//put awsAlb cookie value to custom header - format is important
request.headers.awsalbkey = [{'key': 'awsAlbKey', 'value': cookieList[indexOfAwsALbCookie]}];
//remove awsAlb cookie from list off cookies in request
cookieList.splice(indexOfAwsALbCookie,1);
let cookieListString = cookieList.join('; ');
request.headers.cookie[0].value = cookieListString;
console.log(util.inspect(request, {showHidden: false, depth: null}));
//pass request onto cloudfront cacheing layer or origin request
callback(null, request);
return;
}
}
else if(event.Records[0].cf.config.eventType == 'origin-request') {
console.log('Origin Request');
console.log(`origin request – ${util.inspect(event, {showHidden: false, depth: null})}`);
hasTheHeader(request, 'cookie') ? console.log(`This request has cookies`) : console.log(`This request does NOT have cookies`);
// First check – If no cookies in Viewer Request AND no awsalbkey header, do nothing as this is the first request to the origin
if (!hasTheHeader(request, 'cookie') && !hasTheHeader(request, 'awsalbkey')) {
console.log('origin request first check evaluated - no cookies and no awsalbkey header');
//send request to origin
callback(null, request);
return;
}
//Second check, if no cookie header AND COOKIE_TO_FORWARD customer header exists, then add the cookie header and cookie and remove the COOKIE_TO_FORWARD custom header
if (!hasTheHeader(request, 'cookie') && hasTheHeader(request, 'awsalbkey')) {
console.log('origin request second check evaluated - no cookies and has the awsalbkey header')
//add the cookie header and the cookie obtained from the custom header
request.headers.cookie = [];
var length = request.headers.cookie.push({'key': 'Cookie', 'value': request.headers.awsalbkey[0].value});
//remove the custom header
delete request.headers.awsalbkey;
console.log(util.inspect(request, {showHidden: false, depth: null}));
//send request to origin
callback(null, request);
return;
}
//else cookie list exists
let cookieListOrigin = request.headers.cookie[0].value.split('; ');
console.log(cookieListOrigin);
// Third check - If there are multiple cookies excluding the COOKIE_TO_FORWARD cookie and there's an COOKIE_TO_FORWARD custom header, move the COOKIE_TO_FORWARD custom header to the list of cookies and remove the COOKIE_TO_FORWARD custom header
let originIndexAwsAlbCookie = cookieListOrigin.findIndex(element => element.startsWith(COOKIE_TO_FORWARD));
if ( (originIndexAwsAlbCookie < 0) && (cookieListOrigin.length > 0) && (request.headers.awsalbkey) ) {
console.log('origin request third check evaluated - cookies exist without the awsalb cookie and has the awsalbkey header')
//add the awsalb customer header value to a new cookie in the cookie array
var length = cookieListOrigin.push(request.headers.awsalbkey[0].value);
let cookieListOriginString = cookieListOrigin.join('; ');
request.headers.cookie[0].value = cookieListOriginString;
//remove the custom header
delete request.headers.awsalbkey;
console.log(util.inspect(request, {showHidden: false, depth: null}));
//send request to origin
callback(null, request);
return;
}
}
callback(null, request);
};
Related
I am using nextjs-auth0. I created a Regular Web Application in Auth0, nextjs-auth0 adds a state parameter to the URL, upon successful login, auth0 redirects back to my Next.js application, is is where the error "state missing from the response" occurs, I cannot see the state in the url and in the console I see
Cookie “state” has been rejected because it is already expired. callback
Cookie “_state” has been rejected because it is already expired. callback
Cookie “max_age” has been rejected because it is already expired. callback
Cookie “_max_age” has been rejected because it is already expired. callback
Cookie “code_verifier” has been rejected because it is already expired. callback
Cookie “_code_verifier” has been rejected because it is already expired. callback
Cookie “nonce” has been rejected because it is already expired. callback
Cookie “_nonce” has been rejected because it is already expired.
I tried to create a new tenant with a boilerplate universal login page and that seems to work
<script>
// Decode utf8 characters properly
var config = JSON.parse(decodeURIComponent(escape(window.atob('##config##'))));
config.extraParams = config.extraParams || {};
var connection = config.connection;
var prompt = config.prompt;
var languageDictionary;
var language;
if (config.dict && config.dict.signin && config.dict.signin.title) {
languageDictionary = { title: config.dict.signin.title };
} else if (typeof config.dict === 'string') {
language = config.dict;
}
var loginHint = config.extraParams.login_hint;
var lock = new Auth0LockPasswordless(config.clientID, config.auth0Domain, {
auth: {
redirectUrl: config.callbackURL,
responseType: (config.internalOptions || {}).response_type ||
(config.callbackOnLocationHash ? 'token' : 'code'),
params: config.internalOptions
},
configurationBaseUrl: config.clientConfigurationBaseUrl,
overrides: {
__tenant: config.auth0Tenant,
__token_issuer: config.authorizationServer.issuer
},
assetsUrl: config.assetsUrl,
allowedConnections: connection ? [connection] : null,
rememberLastLogin: !prompt,
language: language,
languageBaseUrl: config.languageBaseUrl,
languageDictionary: languageDictionary,
theme: {
//logo: 'YOUR LOGO HERE',
//primaryColor: 'green'
},
closable: false
});
lock.show();
</script>
However for my existing universal login page it does not, (trimmed):
var params = Object.assign({
domain: config.auth0Domain,
clientID: config.clientID,
redirectUri: config.callbackURL,
scope: 'openid',
audience: audience,
responseType: 'token',
}, config.internalOptions)
var webAuth = new auth0.WebAuth(params)
webAuth.passwordlessVerify(values, function(err, res) {
hide(loader)
if (err) return postError(err)
})
How can I make sure the state parameter is sent so nextjs-auth0 can pick it up?
I need a way to set the headers of the dart http Request object to application/JSON.
I want to build a Request object to send to my backend API. I set the body to my JSON object, but when it gets sent, it defaults the headers to text/html instead of application/json.
I have tried using the built-in method
http.post(url,dynamic body);
but unfortunately this method places the body in the parameters of the URL and I need it in the actual body of the request.
So instead I built an http Request object, and manually set the URL and body but like I said, it sets the headers to text/html.
I have read the docs for https://pub.dev/documentation/http/latest/http/Request-class.html, but unfortunately, I haven't found a way to set the headers.
postRequest(uri) async {
Uri url = Uri.tryParse("https://ptsv2.com/t/umt4a-1569012506/post");
http.Request request = new http.Request("post", url);
request.body = '{mediaItemID: 04b568fa, uri: https://www.google.com}';
var letsGo = await request.send();
print(letsGo.statusCode);
}
Much thanks for any possible solutions!
Ps. this is my first ask on Stack Overflow so I apologize if I made any errors in posting.
Solved!
postRequest(uri) async {
Uri url = Uri.tryParse("https://ptsv2.com/t/umt4a-1569012506/post");
http.Request request = new http.Request("post", url);
request.headers.clear();
request.headers.addAll({"content-type":"application/json; charset=utf-8"});
request.body = '{mediaItemID: 04b568fa, uri: https://www.google.com}';
var letsGo = await request.send();
print(letsGo.statusCode);
}
I was having some issues with the Request object default setting the encoding.
By manually specifying utf-8, the server I am contacting accepts it.
for the post or get any request you can Add Header like this -
var permAddUrl = 'your requested url';
var bodyParameters = {
'Email': email,
'MobileNo': mobileNumber,
};
await http.post(
requesturl,
headers: { 'Content-Type': 'application/x-www-form-urlencoded',
"Authorization":"$token",
},
body: bodyParameters,).then((response) {
var data = json.encode(response.body);
print(data);
setState(() {
if(response.statusCode == 200){
//var statesList = data['data'];
UtilAction.showSnackBar(context, " Details Submitted Successfully");
}
});
});
This is my client side code:
function signIn(){
var email = document.getElementById("username").value;
var password = document.getElementById("password").value;
// As httpOnly cookies are to be used, do not persist any state client side.
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
// When the user signs in with email and password.
firebase.auth().signInWithEmailAndPassword(email, password).then(user => {
// Get the user's ID token as it is needed to exchange for a session cookie.
return firebase.auth().currentUser.getIdToken().then(idToken => {
// Session login endpoint is queried and the session cookie is set.
// CSRF protection should be taken into account.
// ...
var csrfToken = getCookie('_csrf')
return postIdTokenToSessionLogin('/sessionLogin', idToken, csrfToken);
});
}).then(() => {
// A page redirect would suffice as the persistence is set to NONE.
return firebase.auth().signOut();
}).then(() => {
window.location.assign('/profile');
});
}
I'm sending the idToken and csrfToken to generate a sessionId. Using this sessionId, I'm able to assign session cookies.
Here is my server side code:
app.post("/sessionLogin", (req, res) => {
// Get ID token and CSRF token.
var idToken = req.body.idToken.toString();
var csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (!req.cookies || csrfToken !== req.cookies._csrf) {
res.status(401).send('UNAUTHORIZED REQUEST!');
return;
}
// Set session expiration to 5 days.
var expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the
process.
// The session cookie will have the same claims as the ID token.
// We could also choose to enforce that the ID token auth_time is recent.
firebase.auth().verifyIdToken(idToken).then(function(decodedClaims) {
// In this case, we are enforcing that the user signed in in the last 5
minutes.
if (new Date().getTime() / 1000 - decodedClaims.auth_time < 5 * 60) {
return firebase.auth().createSessionCookie(idToken, {expiresIn:
expiresIn});
}
throw new Error('UNAUTHORIZED REQUEST!');
})
.then(function(sessionCookie) {
// Note httpOnly cookie will not be accessible from javascript.
// secure flag should be set to true in production.
var options = {maxAge: expiresIn, path: "/", httpOnly: false, secure: true
/** to test in localhost */};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
})
.catch(function(error) {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
});
app.get("/profile", (req, res) => {
console.log('Cookies: ', req.cookies); //Empty object, 'Cookies: {}'
res.render("profile");
});
app.post("/profile", (req, res) => {
res.send(req.body.name);
console.log('Cookies: ', req.cookies); //Cookies object with csrf and
session token
});
Now, this is working fine and I'm able to pass the cookies to the server with every POST request. An unauthenticated user cannot send POST requests. However, I was hoping to authenticate users and serve the user-related data. So, How can I use these session cookies to serve routes on GET requests as well? Right now, my client side does not send these cookies on GET requests.
I've followed these Firebase documents and GitHub Repos
Will it be the right approach? If not, I'd appreciate your guidance in the right direction. Thank you in advance.
I am trying to post to my server from twilio, but I am getting a 403 error. Basically my parse-heroku serve is rejecting any request from twilio. I am working with TWIMLAPP and masked numbers. I am having trouble posting to a function in my index file when a text goes through. In my TWIMLAPP my message url is https://parseserver.herokuapp.com/parse/index/sms Any help is appreciated. These are the errors in twilio
var app = express();
app.use(require('body-parser').urlencoded());
app.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', 'https://www.twilio.com');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
// Set to true if you need the website to include cookies in the requests sent
// to the API (e.g. in case you use sessions)
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader("X-Parse-Master-Key", "xxxxxxx");
res.setHeader("X-Parse-Application-Id", "xxxxxx");
// Pass to next layer of middleware
next();
});
app.post('/sms', twilio.webhook({ validate: false }), function (req, res) {
console.log("use-sms")
from = req.body.From;
to = req.body.To;
body = req.body.Body;
gatherOutgoingNumber(from, to)
.then(function (outgoingPhoneNumber) {
var twiml = new twilio.TwimlResponse();
twiml.message(body, { to: outgoingPhoneNumber });
res.type('text/xml');
res.send(twiml.toString());
});
});
Can some body tell me how to read received headers in Angular 2?
i have mad a request, for login and password, and there should be sent back headers with Token. I need the token for further workaround.
here is part of the code:
sendLogin(username, password) {
let body = JSON.stringify({"username": username, "password": password});
let headers = new Headers({'Content-Type': 'application/json'});
let options = new RequestOptions({headers: headers});
return this.http.post(this.loginUrl, body, options)
.map(res => res.json())
.map((res) => {
if (res.ok) {
// at least how to console.log received headers?
console.log( res.headers); //undefined
this.loggedIn = res.ok;
} return res.ok;
});
};
thank you.
Most of the time such an issue is related to CORS. You need to explicitly enable allowed headers in the response headers.
You're only be able to see the header in the map only if it's enabled by CORS.
Your server needs to return the following in headers:
Access-Control-Allow-Headers: X-SomeHeader