8. React deployment on Nodejs and Express.js (2).TSL https, certificate conversions and client certificate authentication
@See namecheap and Andras Sevcsik-Zajácz and SitePoint
0. Prerequisites
- A certificate (in this case I prefer a valid certificate) for authenticate the web server and offer https service
- The certificate's key (a file or a string, depending on the format of the certificate)
- A bundle of the root CA certificate chains of the accepted certificates (not only the one from the supplied certificate)
I have a certificate in jks format (mycert.jks) and a string key "myKey". The root certificate chains of this certificate are accessible from the CA web.
1. Nodejs accepted certificate and key formats.
nodejs admit only pem and pfx.
Take into account that pem extension is equivalent to cert, cer and crt.
Also pfx extension is equivalent to pkcs12, pfx and p12 extensions
To convert from jks to p12 just type
keytool -importkeystore -srckeystore mycert.jks -destkeystore mycert.p12 -srcstoretype jks -deststoretype pkcs12
and ask for the passwords (origin and destination). Now we have the mycert.p12 certificate (keystore) that is admitted by nodejs (version).
If we want to convert p12 to pem certificate and key just type :
openssl pkcs12 -in keystore-to-convert.pfx -out crt.pem -clcerts -nokeys
openssl pkcs12 -in keystore-to-convert.pfx -out key.pem -nocerts -nodes
Now we can choose from pfx(p12) certificate and pem certificate and key pair
2. Getting the CA root chains
Mozilla offers a list of CA certificates that can be downloaded. There are a lot of CA that are not included here, but for the moment it can do the job. I have downloaded it as ".npm.certs.pem"
3. Configuring node: Defining the certificate properties
After creating a typescript application to manage the server, in this folder, a file (I have named "index.js") is used for configuring node/express. For defining the certificates to use with https, this is the process.
I have saved all the certificates in the "keystores" relative folder. Now it's time to set the certificate properties (depending on the two certificate types that node accepts)
for pfx certificate
const httpsOpts = { pfx: fs.readFileSync('./keystores/mycert.p12'), // .p12 is equivalent to .pfx passphrase: 'mypassword' , ca: fs.readFileSync('./keystores/.npm.certs.pem') // In previous versions of node the CA bundle had to be splitted into indiovidual certificates }
for pem certificate
const httpsOpts = { cert: fs.readFileSync('./keystores/cert.pem'), // certificate key: fs.readFileSync('./keystores/key.pem'), // key ca: fs.readFileSync('./keystores/.npm.certs.pem') // CA Root chains bundle }
4. Creating https express server
const fs = require('fs'); // File treatment const https = require('https'); //TSL https const express = require('express'); //Express web server const hostName='ximo.com'; //Define server name const httpsPort= 8443; //Define port name //If using p12 certs const httpsPfxOpts = { pfx: fs.readFileSync('./keystores/mycert.p12'), // .p12 is equivalent to .pfx passphrase: 'mypassword' , ca: fs.readFileSync('./keystores/.npm.certs.pem') // In previous versions of node the CA bundle had to be splitted into indiovidual certificates } //If using pem cert and key (JUST USE ONE CERT TYPE, NOT BOTH) const httpsPemOpts = { cert: fs.readFileSync('./keystores/cert.pem'), // certificate key: fs.readFileSync('./keystores/key.pem'), // key ca: fs.readFileSync('./keystores/.npm.certs.pem') // CA Root chains bundle } const app = express(); // Use express const httpsServer = https.createServer(httpsOfxOptions, app); // create https server // Your app code here // app.get('/', (req,res) => { ...}) httpsServer.listen(httpsPort, hostName);
5. Redirecting from http to https
2 servers are needed, one with https and the other with http. The second will redirect to the first, and the code gets as follows
const fs = require('fs'); // File treatment const http = require('http'); // http const https = require('https'); //TSL https const express = require('express'); //Express web server const hostname='ximo.com'; //Define server name const httpPort= 8080; //Define port name const httpsPort= 8443; //Define port name //If using p12 certs const httpsPfxOpts = { pfx: fs.readFileSync('./keystores/mycert.p12'), // .p12 is equivalent to .pfx passphrase: 'mypassword' , ca: fs.readFileSync('./keystores/.npm.certs.pem') // In previous versions of node the CA bundle had to be splitted into indiovidual certificates } //If using pem cert and key const httpsPemOpts = { cert: fs.readFileSync('./keystores/cert.pem'), // certificate key: fs.readFileSync('./keystores/key.pem'), // key ca: fs.readFileSync('./keystores/.npm.certs.pem') // CA Root chains bundle } const app = express(); // Use express const httpServer = http.createServer(app); // create http server for redirection to https const httpsServer = https.createServer(httpsOfxOptions, app); // create https server //Redirect from http to https app.use ((req, res, next) => { if (req.protocol == 'http'){ res.redirect(301,`https://${req.headers.host}$(req.url}`); } next(); }); // The rest of your app code here //1. Example serving static content from the 'public' directory // app.use(express.static('.public')); //2.Example of processing a request to a custom URL // app.get('/', (req,res) => { res.end('Hello from custom URL');}) httpServer.listen(httpPort, hostName); httpsServer.listen(httpsPort, hostName);6. Client certificate authentication
httpsOpts now should include in addition:
requestCert: true, // Requests client cert rejectUnauthorized: false // If you don't what to reject unauthorised userThe code for authenticating is:
app.get('/', (req, res) => { const cert = req.socket.getPeerCertificate() if (req.client.authorized) { res.send(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`); } else if (cert.subject) { res.status(403).send(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`); } else { res.status(401).send(`Sorry, but you need to provide a client certificate to continue.`); } });If you want to redirect to the aplication, change the redish code with this bluish one
app.get('/', (req, res) => { const cert = req.connection.getPeerCertificate() if (req.client.authorized) { res.writeHead(200, { 'Content-Type':'text/html'}); // html use const html = fs.readFileSync('./index.html'); // read the html file to send res.end(html); // send the content of the file } else if (cert.subject) { res.status(403).send(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`); } else { res.status(401).send(`Sorry, but you need to provide a client certificate to continue.`); } });
7. Running the program
node:internal/tls/secure-context:277 context.loadPKCS12(toBuf(pfx), toBuf(passphrase)); ^ Error: unsupported at configSecureContext (node:internal/tls/secure-context:277:15) at Object.createSecureContext (node:_tls_common:117:3) at Server.setSecureContext (node:_tls_wrap:1352:27) at Server (node:_tls_wrap:1211:8) at new Server (node:https:74:3) at Object.createServer (node:https:112:10) at Object.<anonymous> (/home/edu/TYPESCRIPT/server/index.js:82:7) at Module._compile (node:internal/modules/cjs/loader:1119:14) at Module._extensions..js (node:internal/modules/cjs/loader:1173:10) at Module.load (node:internal/modules/cjs/loader:997:32)
8. Nodejs certificate management
To get the client certificate:
req.socket.getPeerCertificate()
To get the client certificate as X509
req.socket.getPreeX509Certificate()
To get the name of the client
cert.subject.CN
To get the issuer of the client certificate
cert.issuer.CN
To verify that the certificate is OK:(Works only on Mozilla. In Chrome fails!!!
req.client.authorized
Other info as valid dates can be retrieved from the certificate, more info is in Nodejs manual.
9. Summing up
1. HTTP is not needed if we are planning to use certificates. So, using an HTTP server to redirect to HTTPS is a waste of time.
2. If the cert is not accepted, the request's URL may be modified to point to a ERROR treatment URL. The function isCertOK does that (changing to "/badcert" or "/nocert" URLs
3. It is commented the code for HTTP
Here is the code of "index.js" in the main folder of the "server" project
const express = require('express'); //Express web server const fs = require('fs'); // File treatment const https = require('https'); //TSL https //const http = require('http'); // http const path = require('path') const session = require("express-session"); // Certificate definitions const opts = { pfx: fs.readFileSync('keystores/mycert.p12'), // change extension from p12 to pfx (as .p12 is equivalent to pfx) passphrase: 'mypassword' , ca: fs.readFileSync('keystores/cacert.pem') , requestCert: true, rejectUnauthorized: false, } const oneDay=1000 * 60 * 60 * 24 const app = express(); // Use express //Use session app.use( session({ secret: [...Array(30)].map(() => Math.random().toString(36)[2]).join(''), //@See https://stackoverflow.com/a/47496558/7704658 saveUninitialized:true, cookie: { maxAge: oneDay }, resave: false }) ) //Verify is certificate is OK function isCertOK(req, res, next) { if (!req.session.views) { const cert = req.socket.getPeerCertificate() if (cert) { req.session.dni=cert.subject.serialNumber req.session.CN=cert.subject.CN req.session.subjectaltname=cert.subjectaltname //The certificate is OK if (req.client.authorized) { req.session.views = 1; console.log(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`) //CA is not verified (403) } else if (cert.subject) { req.session.views = 1; console.log(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`) //req.url='/badcert' } // And last, they can come to us with no certificate at all (401): } else { console.log(`Sorry, but you need to provide a client certificate to continue.`) req.url='/nocert' } } else { req.session.views++ } const clientip=req.socket.remoteAddress console.log(`UserAgent:${req.header("user-agent")} Referrer: ${req.header("referrer")} ip: ${clientip} ip3: ${req.ip} views; ${req.session.views} secret:${req.session.secret}`) next() // Don't forget } //Redirect from http to https /* app.use ((req, res, next) => { if (req.protocol == 'http'){ res.redirect(301,`https://${req.headers.host}$(req.url}`); } next(); }); */ app.use(isCertOK) //Always execute app.get('/nocert', (req, res) => { res.send('<p> <h2>You need a valid certificate to login!</h2></p>') }) app.get('/badcert', (req, res) => { res.send('<p> <h2>You need a certificate from a valid CA to login!</h2></p>') }) //*********************LAST PART OF ROUTING ************************************ */ // serve up production assets app.use(express.static('client01/build')); // let the react app to handle any unknown routes // serve up the index.html if express does'nt recognize the route app.get('*', (req, res) => { res.sendFile(path.resolve(__dirname, 'client01', 'build', 'index.html')); }); // if not in production use the port 9999 const PORT1 = process.env.PORT || 9999; //https //const PORT2 = process.env.PORT || 8888; //http console.log('server started on port:',PORT1); // Let's create our HTTPS server and we're ready to go. https.createServer(opts, app).listen(PORT1) //http.createServer(app).listen(PORT2)
Comentarios
Publicar un comentario