Technical Articles
HTTP request with client certificate in Node.js
Need to fire request with client certificate?
This blog post contains sample code (node.js) showing how to execute an HTTP request that authenticates with client certificate instead of user/password.
There are 4 samples:
1. using axios
2. using node-fetch
3. using native https module
4. using @sap/xssec module
Scenario
As example setup, we’re using the SAP Business Technology Platform (SAP BTP, fka SAP Cloud Platform).
The scenario:
We’re executing an HTTP request from client to server.
The server which receives our HTTP request is the XSUAA service.
The HTTP request we’re sending is used to fetch a token.
The credentials which we need (certificate) are given to us by the service instance.
The HTTP request is successful (and we’re satisfied) if the request returns a valid response (containing a token).
I’ve chosen this scenario because it is real use case and it is easy to reproduce for anybody.
Our client code is just a script which we can run from our local laptop.
We don’t need to deploy an app, as such we can keep the code minimalistic.
However, if desired, it is easy to modify the code such that it runs in the cloud.
I’m posting 4 samples that are based on different libraries, including the native way.
I hope to somewhat meet your expectations….
If not, please add your sample code in the comment section.
Prerequisites
This blog post assumes that you’re Node.js user and a bit familiar with certificates.
You can find basic information about certificates in this blog post.
In order to follow the description, you need access to SAP BTP, and permission/quota for creating the service instance.
This tutorial is based on trial account in SAP BTP.
Everyone can create such trial account.
However, the code should work with any other server as well, so you can just copy the snippets and don’t need extra cloud access.
Preparation
For those who wish to follow the example, here’s the description:
We need to create an instance of the XSUAA service, which will act as server and which will provide us with the required credentials.
We want to run our code locally, to make things easier.
However, this means that we need to create a service key in addition to the service instance.
Below is the description for creating both.
The description is based on the command line tool for Cloud Foundry.
Alternatively, the cockpit of SAP BTP can be used to create the service instance and service key.
Create Project
To host our files, we create a folder, e.g. C:\app
It contains
– the config file for node
package.json
– The 4 scripts containing the 4 samples
script_axios.js
script_fetch.js
script_https.js
script_xssec.js
– the config file for the xsuaa instance
xs-security.json
The content of the files can be found in the appendix.
For your convenience, I’m pasting a screenshot:
Local installation
After creating the files, we need to install the modules locally.
We open command prompt, jump into c:\app and run npm install
Create instance of xsuaa service
Now that we have the config file for XSUAA in place, we can create the instance.
We jump into c:\app and execute the following command:
cf cs xsuaa application myXsuaaInstance -c xs-security.json
Note:
We need the config file in order to configure the XSUAA instance to use client certificate:
{
"xsappname": "clicertxsappname",
"oauth2-configuration": {
"credential-types": ["x509"]
}
}
See appendix for all snippets
After the xsuaa instance is created, we need to create a service key.
The service key is used to access the credentials of the service instance.
This is required, because we’re working locally.
If you’re going to deploy your app to the cloud, you’ll get the credentials in the binding and then you don’t need a service key.
The command:
cf csk myXsuaaInstance sk
We need to view the content of the service key:
cf service-key myXsuaaInstance sk
The credentials are printed to the console:
We can see that it is a JSON object.
We want to use this JSON object in our code.
As such, we copy the whole JSON object into our clipboard, to paste it later into the code.
If we have a closer look, we’ll see all config information that we need:
URL, certificate, key, etc
Yes, the URL is the url of the server to which we send our request with certificate.
The etc contains lot of information that we don’t need.
Note:
You’ll need the deletion commands:
cf dsk myXsuaaInstance sk -f
cf ds myXsuaaInstance -f
OK.
After we have all the info, now we can start with the code.
Request 1 using axios lib
We open the file c:\app\script_axios.js.
We copy the content of the service key into the variable CREDENTIALS.
It will look like this:
Note:
In my screenshot, I’ve removed all unnecessary properties, to make it more comprehensive.
Axios is configured with an agent that contains the certificate and the private key, which we take from the credentials object.
And that’s already all what I wanted to show.
In my XSUAA-example, we need to specify the request as POST with request body as form. That’s required by the client credentials oauth flow.
We send the request to the token endpoint of XSUAA server.
const options = {
url: CREDENTIALS.certurl + '/oauth/token',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
method: 'POST',
data : `grant_type=client_credentials&response_type=token&client_id=${CREDENTIALS.clientid}`,
httpsAgent : new https.Agent({
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key
})
}
See appendix 1 for full sample code.
To execute the script, we change to the command prompt and run
node script_axios.js
The result should look like this:
It looks strange – but it is what we wanted, so it is successful.
Request 2 using node-fetch lib
Now we copy the credentials JSON object into the file c:\app\script_fetch.js.
Using the node-fetch module looks pretty much the same as above.
The request is configured with an agent that contains the certificate and the key.
With respect to the request body, we’re now using the native Now we copy the credentials JSON object into theURLSearchParams object which takes care to properly build the request body, hence less error prone.
The header x-www-form-urlencoded can be removed, if the URLSearchParams is used. But I leave it to make things more explicit.
// configure request
const url = CREDENTIALS.certurl + '/oauth/token'
const params = new URLSearchParams()
params.append('grant_type', 'client_credentials')
params.append('response_type', 'token')
params.append('client_id', CREDENTIALS.clientid)
const options = {
method: 'POST',
body: params,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
agent : new https.Agent({
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key
})
}
See appendix 2 for full sample code.
Now we execute the script
node script_fetch.js
and get the same result as above.
Request 3 using native https module
Usually I’m using native code in my tutorials, to avoid depending on dependencies, which means I’m free of adapting dependencies when they become deprecated or similar.
And here is the snippet for the request with the built-in node module https, in file c:\app\script_https.js
const options = {
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key,
host: CREDENTIALS.certurl.replace('https://', ''),
path: '/oauth/token',
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
See appendix 3 for full sample code.
Request 4 using @sap/xssec lib
The last code sample is dedicated to the xssec library.
It has been tailored for my use case: requesting JWT token from XSUAA.
As such, I cannot omit this sample, but it might not be interesting for you, if you have different use case.
I’m not angry if you stop reading here….
The code is very short:
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, null, null, (error, token)=>{
console.log(`Result: ${token}`)
})
We open the file c:\app\script_axios.js
See appendix 4 for full sample code
That’s all.
The library will check if the given credentials contain a certificate, then it will build the request accordingly and execute it and hand the token over to the callback.
Summary
In this blog post we’ve learned how to pass client certificate and private key to an HTTP request.
Links
https://github.com/bitinn/node-fetch
https://nodejs.org/api/https.html
https://www.npmjs.com/package/@sap/xssec
Appendix 0: Sample Code preparation
xs-security.json
{
"xsappname": "clicertxsappname",
"oauth2-configuration": {
"credential-types": ["x509"]
}
}
package.json
{
"dependencies": {
"axios": "0.24.0",
"@sap/xssec": "3.2.11",
"node-fetch": "2.6.2"
}
}
Appendix 1: Sample Code axios
script_axios.js
const https = require('https')
const axios = require('axios');
// credentials containing client certificate
const CREDENTIALS = {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIRAL6nYMD34UBydTLYLj81+MQwDQYJKoZIhvcNAQELBQAw\neTELMAkGA1UEBhMCREUxDTALBgNVBAcMBFVsjDhyuGaEMCQLQciRfFu8aw6UiVu6lHKyKWdDNb4po7QcAjTCQyALJI7qWRQ\n+uae8UrAx4ZHklT62U1w8JENcckO443jXlTrFvemc47e0rsrZuvsNrjWomz8AZ7D\nMv94YSJQwOZCNDzoYiDf76eqy6y7dEGZzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGYDCCBEigAwIBAgITcAAAAAinzst7Sn3MVgAAAAAACDANBgkqhkiG9w0BAQsF\nADBNMQswCQYDVQQGEwJERTERMA8GA1UEBwwDSBK4w2B+bom+dp\nwiokUHs3zqcnJimjoV5+bYaQuA8KEDpUoSyWbu0CnvqiFn4UUvh5/7RM8xlNYAbf\n/VvkzA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQGHcPvmUGa79M6pM42bGFYjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJERTERMA8GA1UEBwwIV2FhNDM3rMsLu06agF4JTbO8ANYtWQTx0PVrZKJu+8fcIaUp7MVBIVZ\n-----END CERTIFICATE-----\n",
"certurl": "https://1234abcdtrial.authentication.cert.us10.hana.ondemand.com",
"clientid": "sb-clicertxsappname!t12345",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAoG0ENBX+IxI+eFYeg0HeQe+WUUbcj6m5kdu2EQpC76yIYXxf\nBKsdBDZvL2HU/zL0F95n6ePslpmCiRhvC8oYAwXf7CCQJFRczSCRPSMc+HvU7iBmMcSkDfXfX/\n1OAvPsVkkoExhlL9S8hS2ie/Fq07rtfGR6M0ZU2Uahafyz7q/ewu\n-----END RSA PRIVATE KEY-----\n"
}
// configure request
const options = {
url: CREDENTIALS.certurl + '/oauth/token',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
method: 'POST',
data : `grant_type=client_credentials&response_type=token&client_id=${CREDENTIALS.clientid}`,
httpsAgent : new https.Agent({
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key
})
}
// execute request
axios(options).then(result => {
console.log(`Result: ${JSON.stringify(result.data)}`)
})
Appendix 2: Sample Code node-fetch
script_fetch.js
const https = require('https')
const fetch = require('node-fetch')
// credentials with client certificate
const CREDENTIALS = {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIRAL6nYMD34UBydTLYLj81+MQwDQYJKoZIhvcNAQELBQAw\neTELMAkGA1UEBhMCREUxDTALBgNVBAcMBFV47e0rsrZuvsNrjWomz8AZ7D\nMv94YSJQwOZCNDzoYiDf76eqy6y7dEGZzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGYDCCBEigAwIBAgITcAAAAAinzst7Sn3MVgAAAAAACDANBgkqhkiG9w0BAQsF\nADBNMQswCQYDVQQGEwJERTERMA8GA1UEBwwDSBK4w2B+bom+dp\nwiokUHs3zqcnJimjoV5+bYaQuA8KEDpUoSyWbu0CnvqiFn4UUvh5/7RM8xlNYAbf\n/VvkzA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQGHcPvmUGa79M6pM42bGFYjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJERTERMA8GA1UEBwwIV2FhNDM3rMsLu06agF4JTbO8ANYtWQTx0PVrZKJu+8fcIaUp7MVBIVZ\n-----END CERTIFICATE-----\n",
"certurl": "https://abcd1234trial.authentication.cert.us10.hana.ondemand.com",
"clientid": "sb-clicertxsappname!t12345",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAoG0ENBX+IxI+eFYeg0HeQe+WUUbcj6m5kdu2EQpC76yIYXxf\nBKsdBDZvL2HU/zL0F95n6ePslpmCiRhvC8oYAwXf7CCQJFRczSCRPSMc+HvU7iBmMcSkDfXfX/\n1OAvPsVkkoExhlL9S8hS2ie/Fq07rtfGR6M0ZU2Uahafyz7q/ewu\n-----END RSA PRIVATE KEY-----\n"
}
// configure request
const url = CREDENTIALS.certurl + '/oauth/token'
const params = new URLSearchParams()
params.append('grant_type', 'client_credentials')
params.append('response_type', 'token')
params.append('client_id', CREDENTIALS.clientid)
const options = {
method: 'POST',
body: params,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
agent : new https.Agent({
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key
})
}
// execute request
fetch(url, options).then(response => {
response.json().then (data => {
console.log(`Result: ${JSON.stringify(data)}`)
})
})
Appendix 3: Sample Code native
script_https.js
const https = require('https')
const CREDENTIALS = {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIRAL6nYMD34UBydTLYLj81+MQwDQYJKoZIhvcNAQELBQAw\neTELMAkGA1UEBhMCREUxDTALBgNVBAcMBFV47e0rsrZuvsNrjWomz8AZ7D\nMv94YSJQwOZCNDzoYiDf76eqy6y7dEGZzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGYDCCBEigAwIBAgITcAAAAAinzst7Sn3MVgAAAAAACDANBgkqhkiG9w0BAQsF\nADBNMQswCQYDVQQGEwJERTERMA8GA1UEBwwDSBK4w2B+bom+dp\nwiokUHs3zqcnJimjoV5+bYaQuA8KEDpUoSyWbu0CnvqiFn4UUvh5/7RM8xlNYAbf\n/VvkzA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQGHcPvmUGa79M6pM42bGFYjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJERTERMA8GA1UEBwwIV2FhNDM3rMsLu06agF4JTbO8ANYtWQTx0PVrZKJu+8fcIaUp7MVBIVZ\n-----END CERTIFICATE-----\n",
"certurl": "https://1234abcdtrial.authentication.cert.us10.hana.ondemand.com",
"clientid": "sb-clicertxsappname!t12345",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAoG0ENBX+IxI+eFYeg0HeQe+WUUbcj6m5kdu2EQpC76yIYXxf\nBKsdBDZvL2HU/zL0F95n6ePslpmCiRhvC8oYAwXf7CCQJFRczSCRPSMc+HvU7iBmMcSkDfXfX/\n1OAvPsVkkoExhlL9S8hS2ie/Fq07rtfGR6M0ZU2Uahafyz7q/ewu\n-----END RSA PRIVATE KEY-----\n"
}
const options = {
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key,
host: CREDENTIALS.certurl.replace('https://', ''),
path: '/oauth/token',
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
const requestBody = `client_id=${CREDENTIALS.clientid}&grant_type=client_credentials&response_type=token`
const req = https.request(options, (res) => {
let response = ''
res.on('data', (chunk) => {
response += chunk
})
res.on('end', ()=>{
console.log(`Result: ${response}`)
})
})
req.write(requestBody)
req.end()
Appendix 4: Sample Code xssec
script_xssec.js
const xssec = require('@sap/xssec')
const CREDENTIALS = {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIRAL6nYMD34UBydTLYLj81+MQwDQYJKoZIhvcNAQELBQAw\neTELMAkGA1UEBhMCREUxDTALBgNVBAcMBFV47e0rsrZuvsNrjWomz8AZ7D\nMv94YSJQwOZCNDzoYiDf76eqy6y7dEGZzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGYDCCBEigAwIBAgITcAAAAAinzst7Sn3MVgAAAAAACDANBgkqhkiG9w0BAQsF\nADBNMQswCQYDVQQGEwJERTERMA8GA1UEBwwDSBK4w2B+bom+dp\nwiokUHs3zqcnJimjoV5+bYaQuA8KEDpUoSyWbu0CnvqiFn4UUvh5/7RM8xlNYAbf\n/VvkzA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFZjCCA06gAwIBAgIQGHcPvmUGa79M6pM42bGFYjANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJERTERMA8GA1UEBwwIV2FhNDM3rMsLu06agF4JTbO8ANYtWQTx0PVrZKJu+8fcIaUp7MVBIVZ\n-----END CERTIFICATE-----\n",
"certurl": "https://1234abcdtrial.authentication.cert.us10.hana.ondemand.com",
"clientid": "sb-clicertxsappname!t45183",
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAoG0ENBX+IxI+eFYeg0HeQe+WUUbcj6m5kdu2EQpC76yIYXxf\nBKsdBDZvL2HU/zL0F95n6ePslpmCiRhvC8oYAwXf7CCQJFRczSCRPSMc+HvU7iBmMcSkDfXfX/\n1OAvPsVkkoExhlL9S8hS2ie/Fq07rtfGR6M0ZU2Uahafyz7q/ewu\n-----END RSA PRIVATE KEY-----\n",
"url": "https://1234abcdtrial.authentication.us10.hana.ondemand.com",
}
// request JWT token
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, null, null, (error, token)=>{
console.log(`Result: ${token}`)
})
Hey Carlos,
Thanks for writing this up! Super helpful with getting started with using client certificates.
I am trying to implement certificate based oauth in my node client (native https), but am running into some issues. When I use the options object as you define it, I get the error:
401 Unauthorized: TLS handshake failed.
ssl_c_err: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY (20)
ssl_c_ca_err: X509_V_OK (0)
ssl_c_verify: X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE (21)
This is the same error I get in postman when I remove the "PEM file" in my Settings -> Certificates (so only sending CRT and KEY files). When I add the PEM file in postman, however, I am able to successfully get an access token. I did some searching and found that I could add a "ca" field to the options object to send the chained certificate:
const options = {
cert: CREDENTIALS.certificate,
key: CREDENTIALS.key,
ca: CREDENTIALS.ca,
host: CREDENTIALS.certurl.replace('https://', ''),
path: '/oauth/token',
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
But now in my node client I am getting a 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' error and I have not been able to find anything helpful online (threads have mentioned VPN could be an issue, but my requests succeed in postman with VPN turned on so I don't think that's it). Do you have any ideas for how I can fix this?
Edit: I was able to resolve the issue by adding this line of code:
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0
This seems to completely disable the verification of the certificate and will work for my development, but doesn't seem like a great long term solution.