facebook / create-react-app

Set up a modern web app by running one command.
https://create-react-app.dev
MIT License
102.73k stars 26.85k forks source link

Enabling 2-way PKI authentication #2759

Open bitsandbytes opened 7 years ago

bitsandbytes commented 7 years ago

I need 2-way authentication to be configurable because my company requires all internal webapps to use it.

The proposal is three-fold. I want the following to be configurable:

Background on this:

Without the above I can't easily test my passport setup and can't easily test my custom authentication code until I build for production.

My open questions are:

craineum commented 7 years ago

I also found myself wanting this feature. I am rolling progressive web app features onto a long lived project. I wanted to make sure that the issues I was seeing were not related to the browser ssl trust errors.

I completely get the convention, heuristics, or interactivity over configuration methodology (and appreciate it very much). In the case of SSL, I am already configuring my local to do the HTTPS thing. In my case specifically I have already configured my Rails app to use the certs that I generated, so I am reusing the two variables that I am already using for my Rails app.

I have code that accomplishes this. It basically is the third bullet point from @bitsandbytes original post: Make the webapps' certificate and key configurable.

I am reading in SSL_KEY_PATH and SSL_CERT_PATH from environment variables, and using those to configure the http server as document here on webpack devServer.

I have all the code in react-scripts/config/webpackDevServer.config.js, but it might better to move it to react-dev-utils/WebpackDevServerUtils.js and have a few minor tweaks to react-scripts.

I can do a PR (after a little code cleanup), but didn't know if the maintainers think this breaks the too much configuration rule.

dbk91 commented 7 years ago

@craineum looks like your implementation was similar to my approach. The only issue is that https does need to be a boolean because create-react-app ultimately sends that to webpack-dev-server, which expects the type to be a boolean. Please correct me if I'm missing any additional changes you made.

I have the pathnames to the certs in package.json rather than environment variables, though. Not sure if one or the other aligns more with create-react-app's way of convention, heuristics, and interactivity over configuration (which I, too, understand and think makes create-react-app a great tool!).

I initially tried leveraging as much of the proxy object configuration as I could in package.json because, according to the docs: "You may also specify any configuration value http-proxy-middleware or http-proxy supports.". In reality, we're constrained by the fact that this is configured in a JSON file, not in Javascript, so we can't read in things like buffers and functions. So that's where putting in pathnames to the cert and key makes sense. Additionally, if you wanted to send something like the DN to the API, you would need to write a function for that as you would for http-proxy-middleware.

So, really it appears there needs to be a PR for webpack-dev-server to accept the requestCert method before create-react-app could support fetching the client cert (if it aligns with the philosophy).

craineum commented 7 years ago

@dbk91 webpack-dev-server https can take an object as well. I also initially tried the proxy approach and ran into the same issues as you.

dbk91 commented 7 years ago

@craineum Ah, I see now. My mistake. I must have been thinking of my original issue with create-react-app where the webpackDevServer config file only can send a boolean instead of either a boolean or an object. https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/config/webpackDevServer.config.js#L81

jamesmfriedman commented 7 years ago

I have the same issue, I need to pass a certificate file in to webpackDevServer like so


https: {
  key: path/to/key/file,
  cert: path/to/pem/file
}
jamesmfriedman commented 7 years ago

Ok, this is nuts. I didn't even know this was possible with Node, but I found the hack when digging through the code in react-app-rewired. Monkey patch for the win...

if (process.env.NODE_ENV === 'development') {
  const devServerConfigPath = 'react-scripts/config/webpackDevServer.config';
  const devServerConfig = require(devServerConfigPath);
  require.cache[require.resolve(devServerConfigPath)].exports = (
    proxy,
    allowedHost
  ) => {
    const conf = devServerConfig(proxy, allowedHost);
    conf.https = {
      key: fs.readFileSync('./path/to/keyfile.key'),
      cert: fs.readFileSync('./path/to/pemfile.pem')
    };

    return conf;
  };
}
tobias74 commented 7 years ago

@jamesmfriedman where exactly do I put this snippet of code?

jamesmfriedman commented 7 years ago

@tobias74 Sorry for the lack of context. A couple of options for you...

Makr91 commented 6 years ago

I got the react-app-rewired working, but when I got to add your conf and modify the key and cert names, it fails to start, I am assuming I have them in the wrong directory, can you clarify if these should be inside of the react-scripts/config folder?

The override works without the :

conf.https = { key: fs.readFileSync( path.join(root, 'config', 'quickcrashapp.local.key') ), cert: fs.readFileSync( path.join(root, 'config', 'quickcrashapp.local.pem') ) };

jamesmfriedman commented 6 years ago

@Makr91 root is just a variable for a path, you'll have to define the appropriate path to your file. I'll edit my previous comment to reflect this.

gaearon commented 6 years ago

Does anyone want to submit a PR to support this in some way without monkeypatching? I haven't seen a specific proposal for how it should work so it's hard to say anything.

AlexanderHolman146 commented 6 years ago

@jamesmfriedman That's a great suggestion with React App Rewired. I am using a cert/key from a local CA (which I manually added to my keychain). I have localhost:3000 (react server) running via chrome successfully (green lock :) ). However, when I try to navigate to localhost:5000/auth/googleoauth/signup (starting the oauth flow), I get the following error:

Proxy error: Could not proxy request /api/auth/googleoauth/signup from localhost:3000 to https://localhost:5000 (UNABLE_TO_VERIFY_LEAF_SIGNATURE).

My set up is pretty standard. I am using the same key / cert for the react server as my express / node server, but I assume that's ok because one is proxying the other.

Do you have any ideas of how I can fix this? Did you see this in your travels?

lukewlms commented 6 years ago

@AlexanderHolman146 How did you get the green lock with create-react-app? I'm still stuck there.

jamesmfriedman commented 6 years ago

@AlexanderHolman146 it took me forever to figure this out... Replace the stuff in caps with whatever makes sense for your setup.

Make a file called ssl.conf.

This file has to be in the directory you are running the bash command from (or you need to update the config path in the bash script)

[ req ]
distinguished_name  = req_distinguished_name
req_extensions = v3_req

[ req_distinguished_name ]
0.organizationName      = Organization Name (eg, company)
commonName      = Common Name (e.g. server FQDN or YOUR name)
commonName_max      = 64

[ v3_req ]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyCertSign
subjectAltName = @alt_names

[alt_names]
DNS.1 = YOUR-LOCAL-DOMAIN-Name.local
DNS.2 = localhost
openssl req \
    -new \
         -x509 \
    -days 3650 \
    -sha256 \
    -nodes \
    -extensions v3_req \
    -out ./config/YOUR-OUT-FILE-NAME.local.pem \
    -newkey rsa:2048 \
    -keyout ./config/YOUR-OUT-FILE-NAME.local.key \
    -config <( cat ./ssl.conf )
brownbl1 commented 6 years ago

I have a similar use case that motivated me to go down this road. I'm developing a tab app for Microsoft Teams. Their tabs are essentially iframed web apps, but they don't allow http://localhost, even for development (you can upload a dev manifest that points to your local site but it just can't be localhost). So I've been modifying my hosts file to something like 127.0.0.1 myapp-local.com. Setting HTTPS=true allows me to access https://myapp-local.com:3000 and everything works.

Except not quite, because the generated SSL cert is regenerated every time you restart the development server. But the bigger problem is that Teams is an electron app, so there is no way to dangerously bypass the invalid SSL warning that is accessible in chrome. This leaves me with the need to add the generated cert to my root CA store for my local machine. This works if I am hitting https://localhost:3000 but I'm not hitting localhost, I'm hitting https://myapp-local.com:3000. But it seems that the certificate is generated for localhost and not the value specified in the HOST variable. Hmm..

So I was able to get the monkey patching approach working as laid out by @jamesmfriedman (thanks for that as a short term solution).

I also used his openssl script to generate my certs (I may never have figured that out). The key point seems to be that if your goal is to turn the browser lock icon green, it's not enough to just generate a normal ssl cert. You need it to be a CA with the correct DNS specifiers. I could be all wrong about this, but I couldn't get anything other than this to work.

But I agree with @gaearon that this monkey patching isn't ideal, and I'd love to help out with a PR (or direction towards one) if I can.

From my experience, I see a few things that would be steps in the right direction:

The last option is more flexible, but ultimately isn't what I really am after here. And honestly, it's a lot more work to specify your own cert than to have those other first two points just work. So I would shoot for those first two points as a first good goal that would increase usability and leave the open-ended configuration piece left for last.

And finally, I'm not convinced all of this makes sense. There may be a better way or something I'm missing so please chime in on this aging thread if you want to steer my thinking in a different direction.

Thanks!

hansena commented 5 years ago

@gaearon is this still something you would be open to a PR for? The change would be contained to packages/react-scripts/config/webpackDevServer.config.js. Based on devServer.https it could go something like replacing

const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';

with

let https;
try {
  const { ca, cert, key } = JSON.parse(process.env.HTTPS);
  if (ca || cert || key)
    https = {
      ...(ca && { ca: fs.readFileSync(ca) }),
      ...(cert && { cert: fs.readFileSync(cert) }),
      ...(key && { key: fs.readFileSync(key) }),
    };
} catch (e) {
  if (process.env.HTTPS) throw new Error(e);
}
if (process.env.HTTPS === 'true') https = true;

And

module.exports = function(proxy, allowedHost) {
-  https: protocol === 'https',
+  ...(https && { https }),
dbk91 commented 5 years ago

@hansena Naive assumption here, but I'm inclined to believe that version 2 of create-react-app probably handles all of the use-cases mentioned in this issue. They added a nice feature to configure and override the development proxy server: https://facebook.github.io/create-react-app/docs/proxying-api-requests-in-development#configuring-the-proxy-manually

Provided you can pass in the desired SSL options (which it looks like you can just interact with the normal http-proxy-middleware object), this would probably be the way to go as you would not have to rely on passing file paths in package.json.

I'll try to see if I can get a working example this weekend to confirm that this is possible in the latest version.

nearwood commented 5 years ago

@dbk91 Did you get an example working? This would be really useful for my project.

mikekellyio commented 5 years ago

@dbk91 I'm also very interested in a working example

dbk91 commented 5 years ago

Okay, so I finally got the chance to look into it. Unfortunately, as far as I can tell, my assumption was both naive and incorrect.

I was assuming you could configure the server on the fly through the src/setupProxy.js like so...

const fs = require('fs');
const path = require('path');
const proxy = require('http-proxy-middleware');

// Just reading certificates from outside src/
const key = fs.readFileSync(path.resolve(__dirname, '..', 'localhost-key.pem'));
const cert = fs.readFileSync(path.resolve(__dirname, '..', 'localhost.pem'));

module.exports = function(app) {
  app.use(proxy('*', { target: 'localhost:4000', ssl: { key, cert } }));
};

However, this is for the proxy middleware, not the dev server configuration itself. I don't think any of the configuration parameters a user can pass in could override it. Here's the spot that tells the dev server to use self-signed certificates. I believe that's because passing a boolean creates self-signed certificates using webpack-dev-server. If there was a way to override that https variable with an object containing the paths to the key and cert rather than a boolean, you'd have a dev server using you certs of choice (I did confirm this using certs of my own).

It may still be possible to have something override the config, but if there isn't, it's going to take a pull request to get this feature in. I'd say it's probably low-priority for the CRA team, and the way one implements it is important. But I'd say this is a useful feature. I think the lowest effort thing to do would be to allow the user to plop a directory with a specific name, and if there is a key and a cert in there, CRA will use it as the development certificates. This would probably live next to src/setupProxy.js in both implementation and documentation.