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

The host and port are defined. The requests to this server are placed in the comment zone.

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 user

The 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

Maybe your cert has not enough key size for OpenSSL v3. So if you are using node v17 or superior you need to use the option openssl-legacy-provider.  If you start your application using:

 node index.js

and get his error:
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)

Then to start the application use

node --openssl-legacy-provider index.js

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

Entradas populares de este blog

15. Next.js Tutorial (2). Fetching data. Async functions getStaticProps, getServerSideProps, getStaticPaths

14. Next.js Tutorial (1)

10. React deployment on Nginx (5). Passing client certificate DN to react client