Cloudflare Workers set cookie with Rails and Devise


#1

I have the following script to set some custom cookies inside of a Cloudflare worker when a user visits my site:

addEventListener('fetch', event => event.respondWith(fetchAndApply(event.request)));

async function fetchAndApply(request) {
  const cookies = request.headers.get('Cookie') || '';
  const country = request.headers.get('CF-IpCountry');
  const url = new URL(request.url);
  if (url.pathname.indexOf('.aspx') > -1) {
    return new Response(
          "Page not found.",
          { status: 404 });
  }

  if (cookies.includes("cf:request-eu")) {
    return fetch(request);
  }

  let response = await fetch(request);
  response = new Response(response.body, response);

  const list = [
    // EU 28:
    "AT", "Austria",
    "BE", "Belgium",
    "BG", "Bulgaria",
    "HR", "Croatia",
    "CY", "Cyprus",
    "CZ", "Czech Republic",
    "DK", "Denmark",
    "EE", "Estonia",
    "FI", "Finland",
    "FR", "France",
    "DE", "Germany",
    "GR", "Greece",
    "HU", "Hungary",
    "IE", "Ireland, Republic of (EIRE)",
    "IT", "Italy",
    "LV", "Latvia",
    "LT", "Lithuania",
    "LU", "Luxembourg",
    "MT", "Malta",
    "NL", "Netherlands",
    "PL", "Poland",
    "PT", "Portugal",
    "RO", "Romania",
    "SK", "Slovakia",
    "SI", "Slovenia",
    "ES", "Spain",
    "SE", "Sweden",
    "GB", "United Kingdom (Great Britain)",

    // Outermost Regions (OMR)
    // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Outermost_regions
    "GF", "French Guiana",
    "GP", "Guadeloupe",
    "MQ", "Martinique",
    "ME", "Montenegro",
    "YT", "Mayotte",
    "RE", "RĂ©union",
    "MF", "Saint Martin",
    // No Code, Azores
    // No Code, Canary Islands
    // No Code, Madeira

    // Special Cases: Part of EU
    // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Special_cases_in_Europe
    "GI", "Gibraltar",
    "AX", "Ă…land Islands",
    // No Code, BĂĽsingen am Hochrhein
    // No Code, Campione d'Italia and Livigno
    // No Code, Ceuta and Melilla
    // No Code, UN Buffer Zone in Cyprus
    // No Code, Helgoland
    // No Code, Mount Athos

    // Overseas Countries and Territories (OCT)
    // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Overseas_countries_and_territories
    "PM", "Saint Pierre and Miquelon",
    "GL", "Greenland",
    "BL", "Saint Bartelemey",
    "SX", "Sint Maarten",
    "AW", "Aruba",
    "CW", "Curacao",
    "WF", "Wallis and Futuna",
    "PF", "French Polynesia",
    "NC", "New Caledonia",
    "TF", "French Southern Territories",
    "AI", "Anguilla",
    "BM", "Bermuda",
    "IO", "British Indian Ocean Territory",
    "VG", "Virgin Islands, British",
    "KY", "Cayman Islands",
    "FK", "Falkland Islands (Malvinas)",
    "MS", "Montserrat",
    "PN", "Pitcairn",
    "SH", "Saint Helena",
    "GS", "South Georgia and the South Sandwich Islands",
    "TC", "Turks and Caicos Islands",

    // Microstates
    // https://en.wikipedia.org/wiki/Microstates_and_the_European_Union
    "AD", "Andorra",
    "LI", "Liechtenstein",
    "MC", "Monaco",
    "SM", "San Marino",
    "VA", "Vatican City",

    // Other (Not sure how these fit in)
    "JE", "Jersey",
    "GG", "Guernsey",
    "GI", "Gibraltar"
  ];

  response.headers.set("Set-Cookie", `cf:request-country=${country}`);
  response.headers.set("Set-Cookie", `cf:request-eu=${list.indexOf(country) > -1}`);
  return response;
}

The problem I am having is that upon the first log in attempt, Rails throws out the form submission because of an issue with the CSRF token authenticity. If I resubmit the form a second time, then the request passes as normal, which makes sense (since the request cookies contain one I care about, then the request is passed on to the client without modifying the headers).

I can’t think of another way to set a cookie and have form data work properly.


#2

Hi @dennis1, one issue jumps out at me:

  response.headers.set("Set-Cookie", `cf:request-country=${country}`);
  response.headers.set("Set-Cookie", `cf:request-eu=${list.indexOf(country) > -1}`);

This will result in a single Set-Cookie: cf:request-eu=true (or false) header, because Headers.set() overwrites all existing values of the given header. You probably want to use Headers.append().

If your Rails application is setting a session cookie on the initial response, and then you overwrite that with the cf:request-eu cookie with Headers.set(), then perhaps that would explain the CSRF token authentication problem?

Harris


#3

@harris that makes sense. I’ll give that a try. Right now, I modified my worker to make an initial XHR request to an endpoint to save the information and the app then caches the information. And in my worker, I check for a cookie, if it exists, then I just pass the request on.

But, if the above experiment doesn’t work, then I’ll look into modifying my initial script with your suggestion


#4

@harris so here is my modified CF worker:

const badPaths = [
  '.aspx'
];
const countries = [
  // EU 28:
  "AT", "Austria",
  "BE", "Belgium",
  "BG", "Bulgaria",
  "HR", "Croatia",
  "CY", "Cyprus",
  "CZ", "Czech Republic",
  "DK", "Denmark",
  "EE", "Estonia",
  "FI", "Finland",
  "FR", "France",
  "DE", "Germany",
  "GR", "Greece",
  "HU", "Hungary",
  "IE", "Ireland, Republic of (EIRE)",
  "IT", "Italy",
  "LV", "Latvia",
  "LT", "Lithuania",
  "LU", "Luxembourg",
  "MT", "Malta",
  "NL", "Netherlands",
  "PL", "Poland",
  "PT", "Portugal",
  "RO", "Romania",
  "SK", "Slovakia",
  "SI", "Slovenia",
  "ES", "Spain",
  "SE", "Sweden",
  "GB", "United Kingdom (Great Britain)",

  // Outermost Regions (OMR)
  // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Outermost_regions
  "GF", "French Guiana",
  "GP", "Guadeloupe",
  "MQ", "Martinique",
  "ME", "Montenegro",
  "YT", "Mayotte",
  "RE", "RĂ©union",
  "MF", "Saint Martin",
  // No Code, Azores
  // No Code, Canary Islands
  // No Code, Madeira

  // Special Cases: Part of EU
  // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Special_cases_in_Europe
  "GI", "Gibraltar",
  "AX", "Ă…land Islands",
  // No Code, BĂĽsingen am Hochrhein
  // No Code, Campione d'Italia and Livigno
  // No Code, Ceuta and Melilla
  // No Code, UN Buffer Zone in Cyprus
  // No Code, Helgoland
  // No Code, Mount Athos

  // Overseas Countries and Territories (OCT)
  // https://en.wikipedia.org/wiki/Special_member_state_territories_and_the_European_Union#Overseas_countries_and_territories
  "PM", "Saint Pierre and Miquelon",
  "GL", "Greenland",
  "BL", "Saint Bartelemey",
  "SX", "Sint Maarten",
  "AW", "Aruba",
  "CW", "Curacao",
  "WF", "Wallis and Futuna",
  "PF", "French Polynesia",
  "NC", "New Caledonia",
  "TF", "French Southern Territories",
  "AI", "Anguilla",
  "BM", "Bermuda",
  "IO", "British Indian Ocean Territory",
  "VG", "Virgin Islands, British",
  "KY", "Cayman Islands",
  "FK", "Falkland Islands (Malvinas)",
  "MS", "Montserrat",
  "PN", "Pitcairn",
  "SH", "Saint Helena",
  "GS", "South Georgia and the South Sandwich Islands",
  "TC", "Turks and Caicos Islands",

  // Microstates
  // https://en.wikipedia.org/wiki/Microstates_and_the_European_Union
  "AD", "Andorra",
  "LI", "Liechtenstein",
  "MC", "Monaco",
  "SM", "San Marino",
  "VA", "Vatican City",

  // Other (Not sure how these fit in)
  "JE", "Jersey",
  "GG", "Guernsey",
  "GI", "Gibraltar"
];
const API_URL = "https://api.someexample.com";
addEventListener('fetch', event => event.respondWith(fetchAndApply(event.request)));
    
async function fetchAndApply(request) {
  let cookies = request.headers.get('Cookie') || ""
  // EU information captured, just send them on through
  if (cookies.includes("cf-eu-request-captured=true")) {
    return fetch(request);
  }

  const ipAddress = request.headers.get('cf-connecting-ip');
  const country = request.headers.get('CF-IpCountry');
  const isEU = countries.indexOf(country) > -1;
  const data = { 
    visitor: { 
      country,
      eu_request: true,
      ip_address: ipAddress,
    } 
  };
  const headers = { 'Content-Type': 'application/json' };
  const init = {
    method: 'PUT',
    headers,
    body: JSON.stringify(data)
  };
  const response = await fetch(API_URL, init);
  return fetch(request);
} 

And in my rails controller for the API route I care about I have this:

cookies["cf-eu-request-captured"] = true

But for some reason, when in the request Cookies, its not there, but I can see the cookie in my cURL example and even in my POSTman requests. I’m sure I am missing something, but not sure what…


#5

I’m not sure I understand. Does this code cause the origin server to save the visitor’s country, eu_request, and ip_address information from the first request’s body, then correlate that information (still on the origin server) with the second request’s CF-Connecting-IP header in order to set the cookie?