NodeJS SOAP client request with certificate - node.js

I'm trying to connect to a SOAP service which requires a certificate on my NodeJS project.
I'm using soap and this is how I'm trying to access to it:
const soap = require('soap');
(...)
let sslOptions = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath)
};
let sslHeaders = {
Authorization: {
user: 'Z100079',
pass: password
}
};
Service.GetLossRatio = function (contractID, start, end, asOf, cb) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
let args = {
contractID: contractID,
start: start,
end: end,
asOf: asOf
};
soap.createClientAsync(url,
{
//request : specialRequest,
wsdl_options: sslOptions,
wsdl_headers: sslHeaders
})
.then((client) => {
client.setSecurity(
new soap.ClientSSLSecurityPFX(
certCerPath
)
);
client.LossratioService.LossratioPort.calculate(args, (err, res) => {
if (err) {
console.log(err);
return cb(err);
}
console.log(res);
return cb(null, res);
});
})
.catch((e) => {
console.log(e);
cb(e);
});
};
And I'm getting a "Wrong Tag" Error when the LossratioPort.calculate() occurs.
I've no idea what that error means, I can't find much documentation about this specific situation, the "soap" documentation only show a brief explenation on how to make a request with certificates but there's not much more
I know the certificates are valid and I've tried with all the generated certificates from the .pem file (.p12, and .cer). I just want to be sure I'm getting something from the service. Either what I really want, or an error from the server, not from the api.
Any help?
Thanks in advance.
UPDATE:
I'm able to get the service description thorugh client.describe() though:
{"LossratioService":{"LossratioPort":{"calculate":{"input":"tns:calculate","output":"tns:calculateResponse"},"ping":{"input":"tns:ping","output":"tns:pingResponse"},"shakedownTest":{"input":"tns:shakedownTest","output":"tns:shakedownTestResponse"}}}}
I've also confirmed the inputs, and I'm sending as the service requires.

Related

Not able to get Apple Pay payment session

We are trying to implement ApplePay on the web in my project. As per apple documentation i am getting the validation url from applejs api onvalidatemerchant function and i am passing this validation url to a node js route to get the apple payment session. This is the documentation i am following to get the apple payment session(https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session).
The below is the custom node js code i have written to get the apple payment session. The validation url ie)req.query.url passed to this node js route code is https://apple-pay-gateway-cert.apple.com/paymentservices/startSession.
app.get('/getAppleSession2', function (req, res) {
var endpointURL = req.query.url;
var cert_path = './apple_pay.pem';
var cert = fs.readFileSync(cert_path);
console.log('endpointURL is ' + endpointURL);
console.log('cert_path is ' + cert_path);
console.log('cert is'+cert);
const options = {
url: endpointURL,
method: 'post',
cert: cert,
key: cert,
body: {
merchantIdentifier: "xxxxx.xxxxx.xxxxx.xxxxx.xxxxx",
displayName: "xxxxx.xxxxx.xxxxx.xxxxx.xxxxx",
initiative: "web",
initiativeContext: "xxxxx.xxxx.xxxx.xxxx.com"
},
json: true,
};
//console.log("body" + body);
request(options, function (error, response, body) {
console.log('body of getAppleSession' + body);
console.log('Response from getAppleSession' + response);
console.error('Error object ' + error);
res.send(body);
});
});
But this is the response i am getting for this route
body of getAppleSession undefined
Response from getAppleSession undefined
Error object Error: error:0906D06C:PEM routines:PEM_read_bio:no start line
Not sure what is wrong here as i am doing this as per apple's documentation. I suspect if it has anything to do with how i am passing the certs(Merchant Identity certificate)to this nodejs route. I generated the certificate by downloading the Merchant Identity certificate in .cer format from Apple Development portal and converted the certificate i downloaded from Apple portal to .pem format by importing the .cer file in KeyChain access in my mac and exporting it to .pem in keychain access. I then placed the .pem file('./apple_pay.pem') in same directory of my node js route. Is there anything wrong in how i am generating the certificates or passing them in my node js route?
Not sure what is wrong here. Any code sample or pointers will be really helpful.
Seems like this is due to certificate validity related issue. Would you please ensure whether a self-signed certificate is valid.
Hope this may helps.
I might be late, but leaving a working solution here for anyone else who stumbles upon this same issue:
Before beginning with the creation of api for requesting the Apple Pay Session, you need to the create the payment processing and merchant identifier certificates. A detailed explanation can be found at this link:
At the end of the merchant identifier certificate process, you will be left with a .cer file. Double click on this file to add it to your keychain app.
After this, Go to your keychain, right click on certificate, and export it as PKCS #12 (.p12). You can then use openssl to convert this to a .pem file or you can use the .p12 directly (in nodejs with request. You need to set agentOptions as {pfx: p12File, and passphrase: '***'}.
My working solution for NodeJS is as below:
async validateMerchant(ctx) {
let response = {};
try {
const options = {
url: ctx.query.validationURL,
agentOptions: {
pfx: fs.readFileSync(
path.resolve(__dirname, 'MerchantIDCertificate.p12')
),
passphrase: '********',
},
method: 'post',
body: {
merchantIdentifier: 'merchant.***.***.*****',
displayName: 'Your Store Name',
initiative: 'web',
initiativeContext: 'Verified.domain.com',
},
json: true,
};
response = await this.promisifyRequest(options);
} catch (error) {
logger.error(error);
}
return response;
}
promisifyRequest(options) {
return new Promise((resolve, reject) => {
request(options, (error, response, body) => {
if (body) {
console.log(response);
return resolve(body);
}
if (error) {
return reject(error);
}
});
});
}
Look here, how they use apple pay certificate:
Plus their docs are great, look here
Looks like we can also generate .pem file without .p12 file (according to their docs), which was an issue for me (because macOS disabled this format export in KeyChain Access, don't know why).
Another good tutorial:
https://ionutghisoi.medium.com/apple-pay-example-payments-2-86ff893fdc9c
and their example of code how to get payment session:
app.post('/validateSession', async (req, res) => {
const { appleUrl } = req.body;
// use set the certificates for the POST request
httpsAgent = new https.Agent({
rejectUnauthorized: false,
cert: fs.readFileSync(path.join(__dirname, './certificate.pem')),
key: fs.readFileSync(path.join(__dirname, './sandbox.key')),
});
response = await axios.post(
appleUrl,
{
merchantIdentifier: 'your.apple.merchant.id',
domainName: 'yourdomain.com',
displayName: 'Your Shop Name',
},
{
httpsAgent,
}
);
res.send(response.data);
});

Authentication Error when Retrieving and Editing Device Configuration on IoT-Core

I'm trying to use a backend nodeJS server to access (and edit) the device configuration on IoT-Core referring to this API docs
However, I keep getting error:
code 401 with error message "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED".
I created a service account and a key from Google IAM, and gave it Cloud IoT Device Controller permissions, which could update device configurations but not create or delete. Subsequently, I changed it to Cloud IoT Admin and even Project Editor permissions, but still saw the same error message. Am I getting the keys all wrong, or not doing something else I should be doing?
Code below was how I invoked the request
function createJwt (projectId, privateKeyFile, algorithm) {
// Create a JWT to authenticate this device. The device will be disconnected
// after the token expires, and will have to reconnect with a new token. The
// audience field should always be set to the GCP project ID.
const token = {
'iat': parseInt(Date.now() / 1000),
'exp': parseInt(Date.now() / 1000) + 20 * 60, // 20 minutes
'aud': projectId
};
const privateKey = fs.readFileSync(privateKeyFile);
return jwt.sign(token, privateKey, { algorithm: algorithm });
}
app.get('/', function(req, res){
let authToken = createJwt('test-project', './keys/device-config.pem', 'RS256');
const options = {
url: 'https://cloudiot.googleapis.com/v1/projects/test-project/locations/us-central1/registries/dev-registry/devices/test-device',
headers: {
'authorization': 'Bearer ' + authToken,
'content-type': 'application/json',
'cache-control': 'no-cache'
},
json: true
}
request.get(options, function(error, response){
if(error) res.json(error);
else res.json(response);
})
});
For backend servers to interact with IoT-Core, the authentication method is not the same as for device MQTT or HTTP connections. Reference: https://cloud.google.com/iot/docs/samples/device-manager-samples#get_a_device
I was able to retrieve and update device configurations using the code below
function getClient (serviceAccountJson, cb) {
const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountJson));
const jwtAccess = new google.auth.JWT();
jwtAccess.fromJSON(serviceAccount);
// Note that if you require additional scopes, they should be specified as a
// string, separated by spaces.
jwtAccess.scopes = 'https://www.googleapis.com/auth/cloud-platform';
// Set the default authentication to the above JWT access.
google.options({ auth: jwtAccess });
const DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest';
const API_VERSION = 'v1';
const discoveryUrl = `${DISCOVERY_API}?version=${API_VERSION}`;
google.discoverAPI(discoveryUrl, {}, (err, client) => {
if (err) {
console.log('Error during API discovery', err);
return undefined;
}
cb(client);
});
}
function getDevice (client, deviceId, registryId, projectId, cloudRegion) {
const parentName = `projects/${process.env.GCP_PROJECT_ID}/locations/${cloudRegion}`;
const registryName = `${parentName}/registries/${registryId}`;
const request = {
name: `${registryName}/devices/${deviceId}`
};
const promise = new Promise(function(resolve, reject){
client.projects.locations.registries.devices.get(request, (err, data) => {
if (err) {
console.log('Could not find device:', deviceId);
console.log(err);
reject(err);
} else {
console.log(data.config.binaryData);
resolve(data);
}
});
});
return promise;
}
app.get('/', function(req, res){
const cb = function(client){
getDevice(client, 'test-device', 'dev-registry', process.env.GCP_PROJECT_ID, 'us-central1')
.then(function(response){
let decoded = new Buffer(response.config.binaryData, 'base64').toString();
res.json(decoded);
})
.catch(function(error){
res.json(error);
})
}
getClient(serviceAccountJson, cb);
});
I think what you're looking to do is best accomplished using the client library for NodeJS.
First, retrieve an API client object as done in the sample. This will take in the service account credentials you used and will authenticate against Google API Core servers.
At the point in the referenced code where cb(client); is invoked, you'll have your client object and are ready to update your device. Add the imports and API constants from the sample and replace the code where you have a client object with the following code and you should be set.
Use some strings for your device identifiers:
const projectId = 'my-project';
const cloudRegion = 'us-central1';
const registryId = 'my-registry';
const deviceId = 'my-device;
const config = '{fan: 800}';
Next, form your device String:
const deviceId = `projects/${projectId}/locations/${cloudRegion}/registries/${registryId}/devices/${deviceId}`;
const binaryData = Buffer.from(config).toString('base64');
Now you form your request object and execute:
const request = {
name: `${registryName}`,
versionToUpdate: 0,
binaryData: binaryData
};
console.log(request);
client.projects.locations.registries.devices
.modifyCloudToDeviceConfig(
request,
(err, data) => {
if (err) {
console.log('Could not update config:', deviceId);
console.log('Message: ', err);
} else {
console.log('Success :', data);
}
});
Your configuration is updated. If your device is subscribed to the config topic on MQTT it will receive the latest configuration, otherwise, you can poll for the configuration with HTTP from your device.
Just to confirm, when you created the SSL key pair, and when you registered the device with the Cloud IoT Core registry, did you match the type of key created with the radio button you registered it with?
Also to confirm, you put the Google root certificate on the device in the same directory as the private key: ./keys/device-config.pem ? If not you can fetch it with: wget https://pki.google.com/roots.pem.

Consuming DynamicsNAV WebService with Node.JS node-soap

I want to consume the WebService from Microsoft Dynamics NAV 2009 with a small node.js application. The Service itself works fine, I am using it with a c# application, now I want to get data into my nodejs/expressjs application but, I always get Invalid WSDL URL as an error message.
Here is the WSDL as my Browser sees it.
Now I tried to connect with node-soap, following the documentation, by normal and by basic auth, but everytime I get an Invalid WSDL URL error.
Here are the methods I tried for a test connection:
var url = "http://navsrv:7047/DynamicsNAV2/WS/Produktiv/Page/WDCETA";
var auth = "Basic " + new Buffer("*********" + ":" + ****************").toString("base64");
soap.createClient(url, function(err, client) {
console.log('Without basic out:');
if (err)
{
console.log('[ERROR] -> ');
console.log(err);
}
console.log(client);
});
soap.createClient(url, {wsdl_headers: {Authorization: auth} }, function(err, client) {
console.log('With basic out:');
if (err)
{
console.log('[ERROR] -> ');
console.log(err);
}
console.log(client);
});
And this is the response I get:
Without basic out:
[ERROR] ->
[Error: Invalid WSDL URL: http://navsrv:7047/DynamicsNAV2/WS/Produktiv/Page/WDDCETA
Code: 401
Response Body: ]
undefined
With basic out:
[ERROR] ->
[Error: Invalid WSDL URL: http://navsrv:7047/DynamicsNAV2/WS/Produktiv/Page/WDDCETA
Code: 401
Response Body: ]
undefined
As it turned out, the build in HTTP-Server from DyanmicsNAV requires SPNEGO or NTLM as authentication. After some tries creating a proper SPNEGO request with nodejs/node-soap I turned off SPNEGO and enabled NTLM.
With the help of soap-ntlm and httpntlm I could retrieve the wsdl.
This is some testing code how I could manage to retrieve the WSDL file. For now I am happy, but I guess when it comes to invoke function there will be some other issues :)
var soap = require('soap-ntlm');
var fs = require('fs');
var httpntlm = require('httpntlm');
var url = 'http://navsrv:7047/DynamicsNAV2/WS/Produktiv/Page/WDCETA';
var username = '*******';
var password = '***********';
httpntlm.get({
url: url,
password: password,
username: username
}, function(err, wsdl) {
if (err)
{
console.log('ERR: -> ');
console.log(err);
return;
}
fs.writeFile('wsdl_cache/WDCETA.wsdl', wsdl.body, function() {
soap.createClient(__dirname + '/wsdl_cache/WDCETA.wsdl', function(err, client) {
if (err) {
console.log('SOAP ERR: ->');
console.log(err);
return;
}
client.setSecurity(new soap.NtlmSecurity(username, password));
console.log(client);
});
})
});

How to retrieve user's additional information from Azure Mobile/App Services?

I need to get the user's extra information from social accounts like Facebook and Google+. When I first read about Azure Mobile Services I thought it to be the holy grail of social authentication. Well, after a full week of hair pulling I'm starting to reconsider my first impression. It does authenticate as easily as it could possibly do. I configured Google+ and FB to work with Azure, configured Azure to use the key/secret from each provider and it all just worked. I was able to login perfectly. The problem started when I tried to get information from the logged user, which I honestly think is basic!
Azure Mobile Services returns the UserId and a Token that you can not use to request the extra info on the selected provider. So even if I were to create a second request using FB's graph API for instance, that wouldn't work (I've tried!). That token is Azure's own token. So I found out from several Carlos Figueira (SE at Azure) posts that I should customize my Azure script, make a request to Azure and then I'd be able to get it working.
I've also read several posts from Carlos Figueira on how to implement that extra functionality and even though that was not what I was looking for (customizing the server) I decided to work with that. But my return type is a MobileServiceUser and that type only has 2 properties: UserId and MobileServiceAuthenticationToken. So even after adding the server script from Carlos I couldn't retrieve the extra information from my Xamarin App.
I've read a lot of things, researched a lot and couldn't find an answer =/ By the way this is not the answer:
How to get user name, email, etc. from MobileServiceUser?
Did anyone manage to make it work?
PS: I'm not posting any code here because it's working. If you think checking some part of my code would help decipher the problem just let me know.
Thanks in advance!
EDIT:
Script
function insert(item, user, request) {
item.UserName = "<unknown>"; // default
user.getIdentities({
success: function (identities) {
var url = null;
var oauth = null;
if (identities.google) {
var googleAccessToken = identities.google.accessToken;
url = 'https://www.googleapis.com/oauth2/v3/userinfo?access_token=' + googleAccessToken;
} else if (identities.facebook) {
var fbAccessToken = identities.facebook.accessToken;
url = 'https://graph.facebook.com/me?access_token=' + fbAccessToken;
} else if (identities.microsoft) {
var liveAccessToken = identities.microsoft.accessToken;
url = 'https://apis.live.net/v5.0/me/?method=GET&access_token=' + liveAccessToken;
} else if (identities.twitter) {
var userId = user.userId;
var twitterId = userId.substring(userId.indexOf(':') + 1);
url = 'https://api.twitter.com/1.1/users/show.json?user_id=' + twitterId;
var consumerKey = process.env.MS_TwitterConsumerKey;
var consumerSecret = process.env.MS_TwitterConsumerSecret;
oauth = {
consumer_key: consumerKey,
consumer_secret: consumerSecret,
token: identities.twitter.accessToken,
token_secret: identities.twitter.accessTokenSecret
};
}
if (url) {
var requestCallback = function (err, resp, body) {
if (err || resp.statusCode !== 200) {
console.error('Error sending data to the provider: ', err);
request.respond(statusCodes.INTERNAL_SERVER_ERROR, body);
} else {
try {
var userData = JSON.parse(body);
item.UserName = userData.name;
request.execute();
} catch (ex) {
console.error('Error parsing response from the provider API: ', ex);
request.respond(statusCodes.INTERNAL_SERVER_ERROR, ex);
}
}
}
var req = require('request');
var reqOptions = {
uri: url,
headers: { Accept: "application/json" }
};
if (oauth) {
reqOptions.oauth = oauth;
}
req(reqOptions, requestCallback);
} else {
// Insert with default user name
request.execute();
}
}
});
}
You're talking about the token on the client side correct? That token is specific only to the client. If you're using Server Side flow, the server is the only one with that token. If you want to send that to the client, you need to do that via a custom API you create.
This class you're talking about does only contain those two properties. But on your server side, your ServiceUser can access the different identity provider tokens in order to speak to those servers APIs. Your linked post is correct in how you access the token, you're mistaken on where you can access that token, it's only on the server side (if you use the server directed login flow).
Here is the custom API Script I had working in Mobile Services to return the profile of the logged in user. I am working on updating to Mobile Apps as some environment variables appear to have changed. Would love to know if anyone has gotten it to work with Mobile Apps.
exports.get = function (request, response) {
var user = request.user;
user.getIdentities({
success: function (identities) {
var req = require('request');
var url = null;
var oauth = null;
var userId = user.userId.split(':')[1];
console.log('Identities: ', identities);
if (identities.facebook) {
url = 'https://graph.facebook.com/me?access_token=' +
identities.facebook.accessToken;
} else if (identities.google) {
url = 'https://www.googleapis.com/oauth2/v3/userinfo' +
'?access_token=' + identities.google.accessToken;
} else if (identities.microsoft) {
url = 'https://apis.live.net/v5.0/me?access_token=' +
identities.microsoft.accessToken;
} else if (identities.twitter) {
var consumerKey = process.env.MS_TwitterConsumerKey;
var consumerSecret = process.env.MS_TwitterConsumerSecret;
oauth = {
consumer_key: consumerKey,
consumer_secret: consumerSecret,
token: identities.twitter.accessToken,
token_secret: identities.twitter.accessTokenSecret
};
url = 'https://api.twitter.com/1.1/users/show.json?' +
'user_id=' + userId + '&include_entities=false';
} else {
response.send(500, { error: 'No known identities' });
return;
}
if (url) {
var reqParams = { uri: url, headers: { Accept: 'application/json' } };
if (oauth) {
reqParams.oauth = oauth;
}
req.get(reqParams, function (err, resp, body) {
if (err) {
console.error('Error calling provider: ', err);
response.send(500, { error: 'Error calling provider' });
return;
}
if (resp.statusCode !== 200) {
console.error('Provider call did not return success: ', resp.statusCode);
response.send(500, { error: 'Provider call did not return success: ' + resp.statusCode });
return;
}
try {
var userData = JSON.parse(body);
response.send(200, userData);
} catch (ex) {
console.error('Error parsing response: ', ex);
response.send(500, { error: ex });
}
});
} else {
response.send(500, { error: 'Not implemented yet', env: process.env });
}
}
});
};

Node soap, consume password protected WSDL

I'm trying to build a SOAP client with Node, I'm using "soap" package (https://www.npmjs.org/package/soap) trying to consume a user/password protected WSDL.
I can't find how to pass those credentials before creating the client by "soap.createClient", and of course, I can't retrieve the WSDL if I don't provide the right credentials.
I've tried doing:
soap.security.WSSecurity('user', 'pass');
and then calling "createClient" but to no avail.
Also, I've tried to do it with the node-soap-client, with this client I (apparently) can connect to the WSDL, but after that, I've no idea where to go (how to invoke methods).
What am I doing wrong?
Thanks for all your help!
Username and password credentials can be passed like this:
var soap = require('soap');
var url = 'your WSDL url';
var auth = "Basic " + new Buffer("your username" + ":" + "your password").toString("base64");
soap.createClient(url, { wsdl_headers: {Authorization: auth} }, function(err, client) {
});
(derived from https://github.com/vpulim/node-soap/issues/56, thank you Gabriel Lucena https://github.com/glucena)
If its password protected you also need to check the correct security mechanism. I spend a day trying to figure out that the service used NTLM security(it was a clients project and I only got username and password to access the wsdl). In that case, you would need to pass the correct wsdl_options object
var wsdl_options = {
ntlm: true,
username: "your username",
password: "your password",
domain: "domain",
workstation: "workstation"
}
soap.createClient(data.credentials[data.type], {
wsdl_options
},
function(err, client) {
console.log(client.describe());
});
Also, you would need to setSecurity on the client before using any service.
the link to complete explanation: https://codecalls.com/2020/05/17/using-soap-with-node-js/
when I added the auth to the headers I still had a problem. After reading the code and a number of articles I found this to work.
// or use local wsdl if security required
let url = 'http://service.asmx?wsdl'
let wsdl = 'wsdl.wsdl';
let soap = require('soap');
let util = require('util')
soap.createClient(wsdl, function(err, client) {
//don't forget to double slash the string or else the base64 will be incorrect
client.setSecurity(new soap.BasicAuthSecurity('admin\\userName', 'password'));
client.MethodFromWSDL(args, function (err, result) {
console.log(util.inspect(result,{depth: null}))
});
});
This worked for me, the API required the auth parameters as
<UserDetails xmlns="http://url/">';
<userName>{$Username}</userName>
<password>{$Password}</password>
<program>{$program}</program>
</UserDetails>
After lots of trial and error - this ended working
const soapHeader = {
UserDetails: {
userName: process.env.userName,
password: process.env.password,
program: process.env.program
}
}
...
soap.createClient(path, function (err, client) {
if (err) {
console.log('Error creating SOAP client: ' + err);
}
client.addSoapHeader(soapHeader, "", "tns", process.env.URN);
client[process.env.FUNCTION](sargs, function (err, result, rawResponse, soapHeader, rawRequest) {
if (err) {
console.log('Error call SOAP function ' + process.env.FUNCTION + ': ', err);
}
else {
console.log(result);
}
...

Resources