Home Blog CV Projects Patterns Notes Book Colophon Search

CORS and Fetch with Express and Basic Auth

17 Jan, 2017

First, install basic-auth-connect.

Here is the server code configured with username, password, port and validOrigins (which should be a list of origins where you want to allow to make CORS requests):

const express = require("express");
const basicAuth = require("basic-auth-connect");

const username = "username";
const password = "password";
const port = "8080";
const validOrigins = ["http://localhost:8000"];  // Can"t use * when using credentials

let app = express();
// Middleware
app.use((req, res, next) => {
  let validOrigin = false;
  validOrigins.forEach((origin) => {
    if (origin === req.get("origin")) {
      validOrigin = true;
    }
  });
  if (validOrigin) {
    res.header("Access-Control-Allow-Origin", req.get("origin"));
    res.header("Access-Control-Request-Method", "POST, GET, PUT, DELETE");
    res.header("Access-Control-Allow-Headers", "Authorization");
    res.header("Access-Control-Allow-Credentials", "true");
  }
  if (req.method === "OPTIONS") {
    res.json({preflight: true});
  } else {
    next();
  }
});
// Must come after the CORS middleware above
app.use(basicAuth(username, password));

app.get("/api", (req, res) => {
  res.json({api: true});
});

const server = app.listen(port, () => {
  console.log("App listening on port %s", server.address().port);
  console.log("Press Ctrl+C to quit.");
});

When you run this you'll get a server on port 8080 which responds with CORS headers. On the CORS pre-flight OPTIONS request, it will return with the JSON {preflight: true}. On the subsequent GET, if the username and password are set, you'll see {api: true}.

To run this from a client you'll need this index.html file served somewhere:

<html>
<body>
<div id="msg">Running ...</div>
<script>
const username = "username";
const password = "password";
const url = "http://localhost:8080/api";

const headers = new Headers();
const creds = btoa(username + ":" + password);
headers.append("Authorization", "Basic " + creds );
const options = {
  method: "GET",
  headers: headers,
  credentials: "include",
};
console.log(`Making request to URL with ${creds}`);
fetch(url, options)
.then(function(response) {
  return response.json();
})
.then(function(json) {
  console.log(json);
  document.getElementById("msg").innerHTML = "Success, see console log.";
}).catch((error) => {
  console.error(error);
  document.getElementById("msg").innerHTML = "Failed, see console log.";
});
</script>
</body>
</html>

You can't just load it into Chrome locally because you'll get:

Fetch API cannot load http://localhost:8080/api/. Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://localhost:8000' that is not equal to the supplied origin. Origin 'null' is therefore not allowed access. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This isn't really a CORS problem, it is a security feature of Chrome because it treats the origin as null for local files and won't make CORS requests. Instead you need to access your index.html file from a real hosted URL.

You can do this easily by running this command in the same directory as the index.html file:

python -m SimpleHTTPServer

Now visit http://localhost:8000 everything should work!

This example also works on Firefox, and if you include this polyfill it will work on Safari too:

<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.1/fetch.js"></script>

Update:

Here's another version that use node:

function handler (request, response) {
  let validOrigin = false
  validOrigins.forEach((origin) => {
    if (origin === request.headers.origin) {
      validOrigin = true
    }
  })
  if (validOrigin) {
    response.setHeader('Access-Control-Allow-Origin', request.headers.origin)
    response.setHeader('Access-Control-Request-Method', 'POST, GET, PUT, DELETE')
    response.setHeader('Access-Control-Allow-Headers', 'Authorization')
    response.setHeader('Access-Control-Allow-Credentials', 'true')
  }
  if (request.method === 'OPTIONS') {
    response.writeHead(200, {'Content-Type': 'application/json'})
    response.write(JSON.stringify({preflight: true}, null, 4))
    response.end()
    return
  }
  if (request.url === '/api/cors') {
    response.writeHead(200, {'Content-Type': 'application/json'})
    response.write(JSON.stringify({cors: true}, null, 4))
    response.end()
    return
  }
  // ... Carry on as usual
}

And a test:

const assert = require('assert')
const server = require('./server')

const TEST_ORIGIN = 'http://...example.com'

server.listen(8080, () => {
  console.log('Server is listening')
  
  const valid = fetchHTTP({
    url: 'http://localhost:8080/api/cors',
    headers: {'Origin': TEST_ORIGIN}
  })
    .then((spec) => {
      const {response} = spec
      assert.equal(response.headers['access-control-allow-origin'], TEST_ORIGIN)
      assert.equal(response.headers['access-control-request-method'], 'POST, GET, PUT, DELETE')
      assert.equal(response.headers['access-control-allow-headers'], 'Authorization')
      assert.equal(response.headers['access-control-allow-credentials'], 'true')
      console.log('Passed valid')
    })
    .catch((error) => {
      console.error('Valid test FAILED', error)
      console.log(JSON.stringify(error, null, 4))
    })
    
  const invalid = fetchHTTP({
    url: 'http://localhost:8080/api/cors',
    headers: {'Origin': 'http://stolen-origin.example.com'}
  })
    .then((spec) => {
      const {response} = spec
      assert(!('access-control-allow-origin' in response.headers))
      assert(!('access-control-request-method' in response.headers))
      assert(!('access-control-allow-headers' in response.headers))
      assert(!('access-control-allow-credentials' in response.headers))
      console.log('Passed invalid')
    })
    .catch((error) => {
      console.error('Invalid test FAILED', error)
      console.log(JSON.stringify(error, null, 4))
    })
  Promise.all([
    valid,
    invalid
  ])
  .then((res) => {
    console.log('All done')
    server.close()
  })
})  

Copyright James Gardner 1996-2020 All Rights Reserved. Admin.