Using JavaScript to properly sign a string using HmacSHA256 - javascript

In the Houndify API Docs for Authentication, you have the following block of content:
An Example of Authenticating a Request
Let's assume we have the following information:
UserID: ae06fcd3-6447-4356-afaa-813aa4f2ba41
RequestID: 70aa7c25-c74f-48be-8ca8-cbf73627c05f
Timestamp: 1418068667
ClientID: KFvH6Rpy3tUimL-pCUFpPg==
ClientKey: KgMLuq-k1oCUv5bzTlKAJf_mGo0T07jTogbi6apcqLa114CCPH3rlK4c0RktY30xLEQ49MZ-C2bMyFOVQO4PyA==
Concatenate the UserID string, RequestID string, and TimeStamp string in the following format: {user_id};{request_id}{timestamp}
With the values from the example, the expected output would be in this case: ae06fcd3-6447-4356-afaa-813aa4f2ba41;70aa7c25-c74f-48be-8ca8-cbf73627c05f1418068667
Sign the message with the decoded ClientKey. The result is a 32-byte binary string (which we can’t represent visually). After base-64 encoding, however, the signature is: myWdEfHJ7AV8OP23v8pCH1PILL_gxH4uDOAXMi06akk=
The client then generates two authentication headers Hound-Request-Authentication and Hound-Client-Authentication.
The Hound-Request-Authentication header is composed by concatenating the UserID and RequestID in the following format: {user-id};{request-id}. Continuing the example above, the value for this header would be:
Hound-Request-Authentication: ae06fcd3-6447-4356-afaa-813aa4f2ba41;70aa7c25-c74f-48be-8ca8-cbf73627c05f
The Hound-Client-Authentication header is composed by concatening the ClientID, the TimeStamp string and the signature in the following format: {client-id};{timestamp};{signature}. Continuing the example above, the value for this header would be: Hound-Client-Authentication: KFvH6Rpy3tUimL-pCUFpPg==;1418068667;myWdEfHJ7AV8OP23v8pCH1PILL_gxH4uDOAXMi06akk=
For Number 3, it says "Sign the message with the decoded ClientKey". The "message" and "ClientKey" are two distinct strings.
My question(s): How do you sign one string with another string i.e. what exactly does that mean? And how would you do that in JavaScript?
var message = 'my_message';
var key = 'signing_key';
//??what next??
I'm trying to figure all this out so I can create a pre-request script in Postman to do a proper HmacSHA256 hash.

According to the documentation, if you're using one of their SDKs, it will automatically authenticate your requests:
SDKs already handle authentication for you. You just have to provide
the SDK with the Client ID and Client Key that was generated for your
client when it was created. If you are not using an SDK, use the code
example to the right to generate your own HTTP headers to authenticate
your request.
However, if you want to do it manually, I believe you need to compute the HMAC value of the string they describe in the link in your question and then send it base64 encoded as part of the Hound-Client-Authentication header in your requests. They provide an example for node.js:
var uuid = require('node-uuid');
var crypto = require('crypto');
function generateAuthHeaders (clientId, clientKey, userId, requestId) {
if (!clientId || !clientKey) {
throw new Error('Must provide a Client ID and a Client Key');
}
// Generate a unique UserId and RequestId.
userId = userId || uuid.v1();
// keep track of this requestId, you will need it for the RequestInfo Object
requestId = requestId || uuid.v1();
var requestData = userId + ';' + requestId;
// keep track of this timestamp, you will need it for the RequestInfo Object
var timestamp = Math.floor(Date.now() / 1000),
unescapeBase64Url = function (key) {
return key.replace(/-/g, '+').replace(/_/g, '/');
},
escapeBase64Url = function (key) {
return key.replace(/\+/g, '-').replace(/\//g, '_');
},
signKey = function (clientKey, message) {
var key = new Buffer(unescapeBase64Url(clientKey), 'base64');
var hash = crypto.createHmac('sha256', key).update(message).digest('base64');
return escapeBase64Url(hash);
},
encodedData = signKey(clientKey, requestData + timestamp),
headers = {
'Hound-Request-Authentication': requestData,
'Hound-Client-Authentication': clientId + ';' + timestamp + ';' + encodedData
};
return headers;
};

So basically, signing [in this specific case] simply means to create a hash of the string using a hash algorithm in addition to a key, as opposed to a keyless hash [like MD5]. For example:
var message = 'my_message';
var key = 'signing_key';
var hashed_message = hash_func(message, key);
where hash_func is a hashing algorithm like HmacSHA256 (the hashing algorithm in question).
The reason I was trying to figure this out was to test authentication for the Houndify API using Postman. Fortunately, Postman has a nice feature called pre-request scripts [complete with hashing algorithms] that helps if you need to pre-generate values that need to be sent along with your request.
After much fiddling around, I managed to create a pre-request script that properly authenticates to this API (see code below).
var unescapeBase64Url = function (key) {
return key.replace(/-/g, '+').replace(/_/g, '/');
},
escapeBase64Url = function (key) {
return key.replace(/\+/g, '-').replace(/\//g, '_');
},
guid = function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
};
var client_id_str = environment["client-id"];
var client_key_str = environment["client-key"];
var user_id_str = environment["user-id"];
var request_id_str = guid();
var timestamp_str = Math.floor(Date.now() / 1000);
var client_key_dec_str = CryptoJS.enc.Base64.parse(unescapeBase64Url(client_key_str));
var message_str = user_id_str+";"+request_id_str+timestamp_str;
var signature_hash_obj = CryptoJS.HmacSHA256(message_str, client_key_dec_str);
var signature_str = signature_hash_obj.toString(CryptoJS.enc.Base64);
var hound_request_str = user_id_str+";"+request_id_str;
var hound_client_str = client_id_str+";"+timestamp_str+";"+escapeBase64Url(signature_str);
postman.setEnvironmentVariable("hound-request-authentication", hound_request_str);
postman.setEnvironmentVariable("hound-client-authentication", hound_client_str);
Note that you are going to have to create environment variables in Postman for client-id, client-key, and user-id, as well as for the header variables hound-request-authentication and hound-client-authentication to hold the final values that will be referenced when defining headers.
Hope it helps.

Related

Magento 2 REST API Oauth signature invalid - working in Postman but not in Node.js

I am trying to call a Magneto 2 REST API with Oauth authentication. The call works in Postman, but when I call the API from Node-Red it says: "The signature is invalid. Verify and try again."
Here is a sample of the script that I created to generate the authorization header:
//Variables required from the function calling this flow
var p_http_method = "GET";
var p_url = "https://watchguard24.co.za/rest/V1/modules/";
//Static variables for Magento authorization
const p_oauth_version = "1.0";
const p_signature_method = "HMAC-SHA256";
const p_nonce = "17TTbZZM0fi" //encodeURIComponent(uuid.v4()); //"35WpXRWuoQp" //uuid.v4();
const p_consumer_key = "wypklfmtf6m53b0rfclxikr2xibopftu";
const p_access_token = "umnmupjtpidzuzz47vuo5s2ianxmq7cf";
const p_consumer_secret = encodeURIComponent("fw17d1k3i70zldcy7xvuvtjjykrzw286");
const p_token_secret = encodeURIComponent("lroq3mg3vteogfisez7hu2zwbu7fmq2x");
const p_timestamp = 1668081726 //Math.floor(Date.now() / 1000);
//Combining OAuth parameters
const parameters = {
oauth_consumer_key: p_consumer_key,
oauth_nonce: p_nonce,
oauth_signature_method: p_signature_method,
oauth_timestamp: p_timestamp,
oauth_version: p_oauth_version,
oauth_token: p_access_token
}
//Ordering and encoding the variables
let ordered = {};
Object.keys(parameters).sort().forEach(function (key) {
ordered[key] = parameters[key];
});
let encodedParameters = '';
for (var k in ordered) {
const encodedValue = encodeURIComponent(ordered[k]);
const encodedKey = encodeURIComponent(k);
if (encodedParameters === '') {
encodedParameters += encodeURIComponent(`${encodedKey}=${encodedValue}`)
}
else {
encodedParameters += encodeURIComponent(`&${encodedKey}=${encodedValue}`);
}
}
//Base signature string
const encodedUrl = encodeURIComponent(p_url);
encodedParameters = encodeURIComponent(encodedParameters);
const signature_base_string = encodeURIComponent(`${p_http_method}&${encodedUrl}&${encodedParameters}`)
//Create the oauth signature
const signing_key = `${p_consumer_secret}&${p_token_secret}`
const oauth_signature = crypto.createHmac("sha256", signing_key).update(signature_base_string).digest('base64');
//Encode oauth signature
const encoded_oauth_signature = encodeURIComponent(oauth_signature);
msg.headers = {
"Authorization":
'OAuth oauth_consumer_key=' + p_consumer_key
+ ',oauth_token=' + p_access_token
+ ',oauth_signature_method=' + p_signature_method
+ ',oauth_timestamp=' + p_timestamp
+ ',oauth_nonce=' + p_nonce
+ ',oauth_signature=' + encoded_oauth_signature
+ ',oauth_version=' + p_oauth_version
}
return msg;
I then changed the parameters in the script to match those of Postman but I am not getting the same signature that Postman is generating.
Here is the Postman authorization header details:
OAuth
oauth_consumer_key="wypklfmtf6m53b0rfclxikr2xibopftu",
oauth_token="d07jwep9s79srvy3dmjltlfafnpl9d1d",
oauth_signature_method="HMAC-SHA256",
oauth_timestamp="1668081726",
oauth_nonce="17TTbZZM0fi",
oauth_version="1.0",
oauth_signature="yktg8XIKp%2Fsjk2Wzr4Qk7Mxs6B7kY%2Fjx3040UvwHuY8%3D"
Here is the Node-Red (Node.js) authorization header generated:
OAuth
oauth_consumer_key=wypklfmtf6m53b0rfclxikr2xibopftu,
oauth_token=d07jwep9s79srvy3dmjltlfafnpl9d1d,
oauth_signature_method=HMAC-SHA256,
oauth_timestamp=1668081726,
oauth_nonce=17TTbZZM0fi,
oauth_version=1.0,
oauth_signature=8GPa253AV9I%2FIGB9N2s2Urbg7Km%2FUcjhzM%2BeRjuEag4%3D
Can anyone please assist with any potential fixes for my script that the correct oauth signature is generated? I have followed the Magento 2 documentation (https://developer.adobe.com/commerce/webapi/get-started/authentication/gs-authentication-oauth/) regarding the signature generation. I then looked at the following guidance but I still get the same error: https://pandeysoni.medium.com/how-to-create-oauth-1-0a-signature-in-node-js-7d477dead170
I need to be able to generate the same oauth signature as Postman to successfully call the REST API's and not get an invalid signature error.

Generate JWT form header and payload

I'm Using node js to create a jwt in my backend server.
I'm using a library to sign/verify a JWT and it work fine. once one jwt.io i paste the token that i got when i sign in and i can see my data in the payload.
So the problem is that I'm trying to generate the signature from header and the payload that i got back in jwt.io
here is what i tryed to do but it did'nt work and i'm confuse a bit.
the algorith used to sign is the default one HS256.
const crypto = require("crypto");
// encode base64 the header
let jsonHeader = JSON.stringify({
alg: "HS256",
typ: "JWT",
});
let bs64header = Buffer.from(jsonHeader).toString("base64").split("=")[0];
console.log("bs64header :>>\n ", bs64header); //look the same as the token i got
// encode vase64 the payload
let jsonPayload = JSON.stringify({
id: "5eb20004ac94962628c68b91",
iat: 1589125343,
exp: 1589989343,
jti: "37743739b1476caa18ca899c7bc934e1aba63ba1",
});
let bs64payload = Buffer.from(jsonPayload).toString("base64").split("=")[0];
console.log("bs64Payload :>> \n", bs64payload); //look the same as the token i got
// TRY to generate the signature from the Base64Header and Base64Payload
// with the secret code that i used to sign the JWT
let secret = "0d528cb666023eee0d44e725fe9dfb751263d2f68f07998ae7388ff43b1b504f";
let signature = bs64header + "." + bs64payload;
let hashed = crypto
.createHash("sha256", secret)
.update(signature)
.digest("hex");
console.log("hashed :>> \n", hashed);
let bs64signature = Buffer.from(hashed).toString("base64").split("=")[0];
console.log("bs64signature>>", bs64signature); //This is where i got stuck.
// let jwt = bs64header + "." + bs64payload + "." + bs64signature;
// console.log("jwt>>", jwt);
I have modified your code a lot to make it less repetitive and easier to read. I am not entirely sure if this will work, so please comment if there are any errors.
I have tested it in runkit and have also checked what the output should be using jwt.io. The output appears to be the same, so I am pretty certain that this works.
Changes
Created a function to base64 encode objects and strings.
Created a function to make base64 strings use the URL safe character set.
Changed crypto.createHash() to crypto.createHmac(), so that a secret key can actually be used.
// base64 encode the data
function bs64encode(data) {
if (typeof data === "object") {
data = JSON.stringify(data);
}
return bs64escape(Buffer.from(data).toString("base64"));
}
// modify the base64 string to be URL safe
function bs64escape(string) {
return string.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
// base64 encode the header
let bs64header = bs64encode({
alg: "HS256",
typ: "JWT"
});
console.log("bs64header :>>\n ", bs64header);
// base64 encode the payload
let bs64payload = bs64encode({
id: "5eb20004ac94962628c68b91",
iat: 1589125343,
exp: 1589989343,
jti: "37743739b1476caa18ca899c7bc934e1aba63ba1"
});
console.log("bs64payload :>> \n", bs64payload);
// generate the signature from the header and payload
let secret = "0d528cb666023eee0d44e725fe9dfb751263d2f68f07998ae7388ff43b1b504f";
let signature = bs64header + "." + bs64payload;
let bs64signature = bs64escape(crypto
.createHmac("sha256", secret)
.update(signature)
.digest("base64"));
console.log("bs64signature>>", bs64signature);
let jwt = bs64header + "." + bs64payload + "." + bs64signature;
console.log("jwt>>", jwt);

Kraken API private request authentication {"error":["EAPI:Invalid key"]} - Google Script

I have been trying to communicate with the private API on kraken. The error I get suggests {"error":["EAPI:Invalid key"]} that the encryption/decryption steps are correct. I have tried creating new keys, does not help. I'm wondering if the 'format' of the signature variable is wrong, even though correct in nature.
function balance () {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("API_read_only");
var key = sheet.getRange("B5").getValue()
var secret = sheet.getRange("B6").getValue()
// (API method, nonce, and POST data)
var path = "/0/private/TradeBalance"
var nonce = new Date () * 1000
var postdata = "nonce=" + nonce
//Algorithms
//Calculate the SHA256 of the nonce and the POST data
// using goolge script lib
// using more succint function from https://stackoverflow.com/questions/16216868/get-back-a-string-representation-from-computedigestalgorithm-value-byte
function SHA_256 (str) {
return Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, str).reduce(function(str,chr){
chr = (chr < 0 ? chr + 256 : chr).toString(16);
return str + (chr.length==1?'0':'') + chr;
},'');
}
var api_sha256 = SHA_256(nonce + postdata)
//Decode the API secret (the private part of the API key) from base64 // need to stringyfy
var base64 = Utilities.base64Decode(secret)
var base64s = Utilities.newBlob(base64).getDataAsString()
//Calculate the HMAC of the URI path and the SHA256, using SHA512 as the HMAC hash and the decoded API secret as the HMAC key
var hamc512_uri = Utilities.computeHmacSha256Signature(path + api_sha256,base64s)
var hamc512_uris = Utilities.newBlob(hamc512_uri).getDataAsString()
//Encode the HMAC into base64
var signature = Utilities.base64Encode(hamc512_uris)
Logger.log(signature)
//An example of the algorithm using the variables shown above is as follows:
//Base64Encode(HMAC-SHA512 of ("/0/private/TradeBalance" + SHA256("1540973848000nonce=1540973848000&asset=xxbt")) using Base64Decode("FRs+gtq09rR7OFtKj9BGhyOGS3u5vtY/EdiIBO9kD8NFtRX7w7LeJDSrX6cq1D8zmQmGkWFjksuhBvKOAWJohQ==") as the HMAC key
//The result is the API-Sign value / signature.
// connect
var url = "https://api.kraken.com" + path;
var options = {
method: 'post',
headers: {
'API-Key': key,
'API-Sign': signature
},
payload: postdata
};
var response = UrlFetchApp.fetch (url, options);
json = response.getContentText ();
Logger.log(json)
}
While I cannot spot what's wrong with your code I faced the same problem as well (thinking I have everything correct but getting a EAPI:Invalid key) with different libraries.
The approach that helped me was:
Take some posted working solution, e.g. https://stackoverflow.com/a/43081507/672008 (in Java)
Check that it really works
Fix the nonce parameter to get a stable HMAC end results
Massage my code until I get then same intermediate & end results
In the end I was successful using this library: https://www.npmjs.com/package/jssha
The code:
import jssha from 'jssha';
const secret = '...';
const nonce = 1642383717038;
const message = '';
const path = '/0/private/Balance';
const data = 'nonce=' + nonce;
const dataHash = new jssha('SHA-256', 'TEXT');
dataHash.update(nonce + data + message);
let utf8Encode = new TextEncoder();
const hmacHash = new jssha('SHA-512', 'UINT8ARRAY', { hmacKey: { value: secret, format: 'B64' } });
hmacHash.update(utf8Encode.encode(path));
hmacHash.update(dataHash.getHash('UINT8ARRAY'));
console.log('hmac', hmacHash.getHash('B64'));

How to sign a JWT with a private key (pem) in CryptoJS?

I am trying to create a signed JWT in postman with the following code
function base64url(source) {
// Encode in classical base64
encodedSource = CryptoJS.enc.Base64.stringify(source);
// Remove padding equal characters
encodedSource = encodedSource.replace(/=+$/, '');
// Replace characters according to base64url specifications
encodedSource = encodedSource.replace(/\+/g, '-');
encodedSource = encodedSource.replace(/\//g, '_');
return encodedSource;
}
function addIAT(request) {
var iat = Math.floor(Date.now() / 1000) + 257;
data.iat = iat;
return data;
}
var header = {
"typ": "JWT",
"alg": "HS256"
};
var data = {
"fname": "name",
"lname": "name",
"email": "email#domain.com",
"password": "abc123$"
};
data = addIAT(data);
var secret = 'myjwtsecret';
// encode header
var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);
// encode data
var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);
// build token
var token = encodedHeader + "." + encodedData;
// sign token
var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);
var signedToken = token + "." + signature;
postman.setEnvironmentVariable("payload", signedToken);
Code taken from https://gist.github.com/corbanb/db03150abbe899285d6a86cc480f674d .
I've been trying to input the PEM as the secret but does not work. Also can't find any HmacSHA256 overload that takes a PEM.
How can that be done?
The mention of postman changed this. I have a solution for you, but it's not exactly a clean way by any mean.
You'll need to create a request that you will need to execute whenever you open postman. Go as follows:
The purpose of this request is to side-load jsrsasign-js and storing it in a global Postman variable.
Once this is done, you can then use this content elsewhere. For every request you need a RSA256 JWT signature, the following pre-request script will update a variable (here, token) with the token:
var navigator = {};
var window = {};
eval(pm.globals.get("jsrsasign-js"));
function addIAT(request) {
var iat = Math.floor(Date.now() / 1000) + 257;
data.iat = iat;
return data;
}
var header = {"alg" : "RS256","typ" : "JWT"};
var data = {
"fname": "name",
"lname": "name",
"email": "email#domain.com",
"password": "abc123$"
};
data = addIAT(data);
var privateKey = "-----BEGIN RSA PRIVATE KEY----- \
MIIBOQIBAAJAcrqH0L91/j8sglOeroGyuKr1ABvTkZj0ATLBcvsA91/C7fipAsOn\
RqRPZr4Ja+MCx0Qvdc6JKXa5tSb51bNwxwIDAQABAkBPzI5LE+DuRuKeg6sLlgrJ\
h5+Bw9kUnF6btsH3R78UUANOk0gGlu9yUkYKUkT0SC9c6HDEKpSqILAUsXdx6SOB\
AiEA1FbR++FJ56CEw1BiP7l1drM9Mr1UVvUp8W71IsoZb1MCIQCKUafDLg+vPj1s\
HiEdrPZ3pvzvteXLSuniH15AKHEuPQIhAIsgB519UysMpXBDbtxJ64jGj8Z6/pOr\
NrwV80/EEz45AiBlgTLZ2w2LjuNIWnv26R0eBZ+M0jHGlD06wcZK0uLsCQIgT1kC\
uNcDTERjwEbFKJpXC8zTLSPcaEOlbiriIKMnpNw=\
-----END RSA PRIVATE KEY-----";
var sHeader = JSON.stringify(header);
var sPayload = JSON.stringify(data);
var sJWT = KJUR.jws.JWS.sign(header.alg, sHeader, sPayload, privateKey);
pm.variables.set('token', sJWT);
In order:
I define mock window and navigator objects as jsrsasign-js needs them.
I then eval() the content of what we fetched earlier in order to rehydrate everything
The rest of your code is simple usage of jsrsasign-js. Your token info is there, and I've defined a private key there. You can change this or use an environment variable; it's just there for demo purposes. I then simply use the rehydrated library to sign it, and set the variable to the value of the signed JWT.
A PEM, as you refer to it, is a container format specifying a combination of public and/or private key. You're using it to sign using HMAC-SHA256, which operates on a shared secret. This obviously isn't going to work (unless you take the poor man's approach and use your public key as the shared secret).
Fortunately enough, there are other signature methods defined in the RFCs. For instance, there is a way to sign using RSA, and a very convenient way of defining a public key as a JSON web key (JWK). We're going to be leveraging both.
I've generated a key pair for testing, they're named out and out.pub. Generation tool is genrsa (and as such, they're an RSA keypair).
In order to sign, we're going to have to change a few things:
We're changing algorithms from HS256 to RS256, as explained above
We're going to need a new library to do the signing itself, as crypto-js does not support asymmetric key crypto. We'll fall back to the native crypto module, though there are pure-JS alternatives
The code:
var CryptoJS = require("crypto-js");
var keyFileContent = require("fs").readFileSync("./out");
var pubkey = require("fs").readFileSync("./out.pub");
var base64url = require("base64url");
var nJwt = require("njwt");
function addIAT(request) {
var iat = Math.floor(Date.now() / 1000) + 257;
data.iat = iat;
return data;
}
var header = {
"typ": "JWT",
"alg": "RS256"
};
var data = {
"fname": "name",
"lname": "name",
"email": "email#domain.com",
"password": "abc123$"
};
data = addIAT(data);
// encode header
var stringifiedHeader = JSON.stringify(header);
var encodedHeader = base64url(stringifiedHeader);
// encode data
var stringifiedData = JSON.stringify(data);
var encodedData = base64url(stringifiedData);
// build token
var token = encodedHeader + "." + encodedData;
// sign token
var signatureAlg = require("crypto").createSign("sha256");
signatureAlg.update(token);
var signature = signatureAlg.sign(keyFileContent);
signature = base64url(signature);
var signedToken = token + "." + signature;
console.log(signedToken);
// Verify
var verifier = new nJwt.Verifier();
verifier.setSigningAlgorithm('RS256');
verifier.setSigningKey(pubkey);
verifier.verify(signedToken, function() {
console.log(arguments);
});
And that's it! It's quite literally that simple, although I would not recommend rewriting the sign() function from crypto from scratch. Leave it to a library that has had thorough inspection by the community, and crypto is pretty serious business.

Get and Set a Single Cookie with Node.js HTTP Server

I want to be able to set a single cookie, and read that single cookie with each request made to the nodejs server instance. Can it be done in a few lines of code, without the need to pull in a third party lib?
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}).listen(8124);
console.log('Server running at http://127.0.0.1:8124/');
Just trying to take the above code directly from nodejs.org, and work a cookie into it.
There is no quick function access to getting/setting cookies, so I came up with the following hack:
const http = require('http');
function parseCookies (request) {
const list = {};
const cookieHeader = request.headers?.cookie;
if (!cookieHeader) return list;
cookieHeader.split(`;`).forEach(function(cookie) {
let [ name, ...rest] = cookie.split(`=`);
name = name?.trim();
if (!name) return;
const value = rest.join(`=`).trim();
if (!value) return;
list[name] = decodeURIComponent(value);
});
return list;
}
const server = http.createServer(function (request, response) {
// To Read a Cookie
const cookies = parseCookies(request);
// To Write a Cookie
response.writeHead(200, {
"Set-Cookie": `mycookie=test`,
"Content-Type": `text/plain`
});
response.end(`Hello World\n`);
}).listen(8124);
const {address, port} = server.address();
console.log(`Server running at http://${address}:${port}`);
This will store all cookies into the cookies object, and you need to set cookies when you write the head.
If you're using the express library, as many node.js developers do, there is an easier way. Check the Express.js documentation page for more information.
The parsing example above works but express gives you a nice function to take care of that:
app.use(express.cookieParser());
To set a cookie:
res.cookie('cookiename', 'cookievalue', { maxAge: 900000, httpOnly: true });
To clear the cookie:
res.clearCookie('cookiename');
RevNoah had the best answer with the suggestion of using Express's cookie parser. But, that answer is now 3 years old and is out of date.
Using Express, you can read a cookie as follows
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express();
app.use(cookieParser());
app.get('/myapi', function(req, resp) {
console.log(req.cookies['Your-Cookie-Name-Here']);
})
And update your package.json with the following, substituting the appropriate relatively latest versions.
"dependencies": {
"express": "4.12.3",
"cookie-parser": "1.4.0"
},
More operations like setting and parsing cookies are described here
and here
As an enhancement to #Corey Hart's answer, I've rewritten the parseCookies() using:
RegExp.prototype.exec - use regex to parse "name=value" strings
Here's the working example:
let http = require('http');
function parseCookies(str) {
let rx = /([^;=\s]*)=([^;]*)/g;
let obj = { };
for ( let m ; m = rx.exec(str) ; )
obj[ m[1] ] = decodeURIComponent( m[2] );
return obj;
}
function stringifyCookies(cookies) {
return Object.entries( cookies )
.map( ([k,v]) => k + '=' + encodeURIComponent(v) )
.join( '; ');
}
http.createServer(function ( request, response ) {
let cookies = parseCookies( request.headers.cookie );
console.log( 'Input cookies: ', cookies );
cookies.search = 'google';
if ( cookies.counter )
cookies.counter++;
else
cookies.counter = 1;
console.log( 'Output cookies: ', cookies );
response.writeHead( 200, {
'Set-Cookie': stringifyCookies(cookies),
'Content-Type': 'text/plain'
} );
response.end('Hello World\n');
} ).listen(1234);
I also note that the OP uses the http module.
If the OP was using restify, he can make use of restify-cookies:
var CookieParser = require('restify-cookies');
var Restify = require('restify');
var server = Restify.createServer();
server.use(CookieParser.parse);
server.get('/', function(req, res, next){
var cookies = req.cookies; // Gets read-only cookies from the request
res.setCookie('my-new-cookie', 'Hi There'); // Adds a new cookie to the response
res.send(JSON.stringify(cookies));
});
server.listen(8080);
Let me repeat this part of question that answers here are ignoring:
Can it be done in a few lines of code, without the need to pull in a third party lib?
Reading Cookies
Cookies are read from requests with the Cookie header. They only include a name and value. Because of the way paths work, multiple cookies of the same name can be sent. In NodeJS, all Cookies in as one string as they are sent in the Cookie header. You split them with ;. Once you have a cookie, everything to the left of the equals (if present) is the name, and everything after is the value. Some browsers will accept a cookie with no equal sign and presume the name blank. Whitespaces do not count as part of the cookie. Values can also be wrapped in double quotes ("). Values can also contain =. For example, formula=5+3=8 is a valid cookie.
/**
* #param {string} [cookieString='']
* #return {[string,string][]} String Tuple
*/
function getEntriesFromCookie(cookieString = '') {
return cookieString.split(';').map((pair) => {
const indexOfEquals = pair.indexOf('=');
let name;
let value;
if (indexOfEquals === -1) {
name = '';
value = pair.trim();
} else {
name = pair.substr(0, indexOfEquals).trim();
value = pair.substr(indexOfEquals + 1).trim();
}
const firstQuote = value.indexOf('"');
const lastQuote = value.lastIndexOf('"');
if (firstQuote !== -1 && lastQuote !== -1) {
value = value.substring(firstQuote + 1, lastQuote);
}
return [name, value];
});
}
const cookieEntries = getEntriesFromCookie(request.headers.Cookie);
const object = Object.fromEntries(cookieEntries.slice().reverse());
If you're not expecting duplicated names, then you can convert to an object which makes things easier. Then you can access like object.myCookieName to get the value. If you are expecting duplicates, then you want to do iterate through cookieEntries. Browsers feed cookies in descending priority, so reversing ensures the highest priority cookie appears in the object. (The .slice() is to avoid mutation of the array.)
Settings Cookies
"Writing" cookies is done by using the Set-Cookie header in your response. The response.headers['Set-Cookie'] object is actually an array, so you'll be pushing to it. It accepts a string but has more values than just name and value. The hardest part is writing the string, but this can be done in one line.
/**
* #param {Object} options
* #param {string} [options.name='']
* #param {string} [options.value='']
* #param {Date} [options.expires]
* #param {number} [options.maxAge]
* #param {string} [options.domain]
* #param {string} [options.path]
* #param {boolean} [options.secure]
* #param {boolean} [options.httpOnly]
* #param {'Strict'|'Lax'|'None'} [options.sameSite]
* #return {string}
*/
function createSetCookie(options) {
return (`${options.name || ''}=${options.value || ''}`)
+ (options.expires != null ? `; Expires=${options.expires.toUTCString()}` : '')
+ (options.maxAge != null ? `; Max-Age=${options.maxAge}` : '')
+ (options.domain != null ? `; Domain=${options.domain}` : '')
+ (options.path != null ? `; Path=${options.path}` : '')
+ (options.secure ? '; Secure' : '')
+ (options.httpOnly ? '; HttpOnly' : '')
+ (options.sameSite != null ? `; SameSite=${options.sameSite}` : '');
}
const newCookie = createSetCookie({
name: 'cookieName',
value: 'cookieValue',
path:'/',
});
response.headers['Set-Cookie'].push(newCookie);
Remember you can set multiple cookies, because you can actually set multiple Set-Cookie headers in your request. That's why it's an array.
Note on external libraries:
If you decide to use the express, cookie-parser, or cookie, note they have defaults that are non-standard. Cookies parsed are always URI Decoded (percent-decoded). That means if you use a name or value that has any of the following characters: !#$%&'()*+/:<=>?#[]^`{|} they will be handled differently with those libraries. If you're setting cookies, they are encoded with %{HEX}. And if you're reading a cookie you have to decode them.
For example, while email=name#domain.com is a valid cookie, these libraries will encode it as email=name%40domain.com. Decoding can exhibit issues if you are using the % in your cookie. It'll get mangled. For example, your cookie that was: secretagentlevel=50%007and50%006 becomes secretagentlevel=507and506. That's an edge case, but something to note if switching libraries.
Also, on these libraries, cookies are set with a default path=/ which means they are sent on every url request to the host.
If you want to encode or decode these values yourself, you can use encodeURIComponent or decodeURIComponent, respectively.
References:
Cookie Syntax
Set-Cookie Syntax
Additional information:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
You can use the "cookies" npm module, which has a comprehensive set of features.
Documentation and examples at:
https://github.com/jed/cookies
To get a cookie splitter to work with cookies that have '=' in the cookie values:
var get_cookies = function(request) {
var cookies = {};
request.headers && request.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
return cookies;
};
then to get an individual cookie:
get_cookies(request)['my_cookie']
Cookies are transfered through HTTP-Headers
You'll only have to parse the request-headers and put response-headers.
Here's a neat copy-n-paste patch for managing cookies in node. I'll do this in CoffeeScript, for the beauty.
http = require 'http'
http.IncomingMessage::getCookie = (name) ->
cookies = {}
this.headers.cookie && this.headers.cookie.split(';').forEach (cookie) ->
parts = cookie.split '='
cookies[parts[0].trim()] = (parts[1] || '').trim()
return
return cookies[name] || null
http.IncomingMessage::getCookies = ->
cookies = {}
this.headers.cookie && this.headers.cookie.split(';').forEach (cookie) ->
parts = cookie.split '='
cookies[parts[0].trim()] = (parts[1] || '').trim()
return
return cookies
http.OutgoingMessage::setCookie = (name, value, exdays, domain, path) ->
cookies = this.getHeader 'Set-Cookie'
if typeof cookies isnt 'object'
cookies = []
exdate = new Date()
exdate.setDate(exdate.getDate() + exdays);
cookieText = name+'='+value+';expires='+exdate.toUTCString()+';'
if domain
cookieText += 'domain='+domain+';'
if path
cookieText += 'path='+path+';'
cookies.push cookieText
this.setHeader 'Set-Cookie', cookies
return
Now you'll be able to handle cookies just as you'd expect:
server = http.createServer (request, response) ->
#get individually
cookieValue = request.getCookie 'testCookie'
console.log 'testCookie\'s value is '+cookieValue
#get altogether
allCookies = request.getCookies()
console.log allCookies
#set
response.setCookie 'newCookie', 'cookieValue', 30
response.end 'I luvs da cookies';
return
server.listen 8080
Using Some ES5/6 Sorcery & RegEx Magic
Here is an option to read the cookies and turn them into an object of Key, Value pairs for client side, could also use it server side.
Note: If there is a = in the value, no worries. If there is an = in the key, trouble in paradise.
More Notes: Some may argue readability so break it down as you like.
I Like Notes: Adding an error handler (try catch) wouldn't hurt.
const iLikeCookies = () => {
return Object.fromEntries(document.cookie.split('; ').map(v => v.split(/=(.+)/)));
}
const main = () => {
// Add Test Cookies
document.cookie = `name=Cookie Monster;expires=false;domain=localhost`
document.cookie = `likesCookies=yes=withARandomEquals;expires=false;domain=localhost`;
// Show the Objects
console.log(document.cookie)
console.log('The Object:', iLikeCookies())
// Get a value from key
console.log(`Username: ${iLikeCookies().name}`)
console.log(`Enjoys Cookies: ${iLikeCookies().likesCookies}`)
}
What is going on?
iLikeCookies() will split the cookies by ; (space after ;):
["name=Cookie Monster", "likesCookies=yes=withARandomEquals"]
Then we map that array and split by first occurrence of = using regex capturing parens:
[["name", "Cookie Monster"], ["likesCookies", "yes=withARandomEquals"]]
Then use our friend `Object.fromEntries to make this an object of key, val pairs.
Nooice.
If you don't care what's in the cookie and you just want to use it, try this clean approach using request (a popular node module):
var request = require('request');
var j = request.jar();
var request = request.defaults({jar:j});
request('http://www.google.com', function () {
request('http://images.google.com', function (error, response, body){
// this request will will have the cookie which first request received
// do stuff
});
});
var cookie = 'your_cookie';
var cookie_value;
var i = request.headers.indexOf(cookie+'=');
if (i != -1) {
var eq = i+cookie.length+1;
var end = request.headers.indexOf(';', eq);
cookie_value = request.headers.substring(eq, end == -1 ? undefined : end);
}
I wrote this simple function just pass
req.headers.cookie and cookie name
const getCookieByName =(cookies,name)=>{
const arrOfCookies = cookies.split(' ')
let yourCookie = null
arrOfCookies.forEach(element => {
if(element.includes(name)){
yourCookie = element.replace(name+'=','')
}
});
return yourCookie
}
I know that there are many answer to this question already, but here's a function made in native JS.
function parseCookies(cookieHeader) {
var cookies = {};
cookieHeader
.split(";")
.map(str => str.replace("=", "\u0000")
.split("\u0000"))
.forEach(x => cookies[x[0]] = x[1]);
return cookies;
}
It starts by taking in the document.cookie string. Every key-value pair is separated by a semicolon (;). Therefore the first step is to divide the string up each key-value pair.
After that, the function replaces the first instance of "=" with a random character that isn't in the rest of the string, for this function I decided to use the NULL character (\u0000). The key-value pair can now be split into just two pieces. The two pieces can now be combined into JSON.
You can use cookie lib to parse incoming multiple cookies, so that you won't have to worry about exceptions cases:
var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2');
// { foo: 'bar', equation: 'E=mc^2' }
To write a cookie you can do like this:
response.writeHead(200, {
"Set-Cookie": `mycookie=cookie`,
"Content-Type": `text/plain`
});
First one needs to create cookie (I have wrapped token inside cookie as an example) and then set it in response.To use the cookie in following way install cookieParser
app.use(cookieParser());
The browser will have it saved in its 'Resource' tab and will be used for every request thereafter taking the initial URL as base
var token = student.generateToken('authentication');
res.cookie('token', token, {
expires: new Date(Date.now() + 9999999),
httpOnly: false
}).status(200).send();
To get cookie from a request on the server side is easy too.You have to extract the cookie from request by calling 'cookie' property of the request object.
var token = req.cookies.token; // Retrieving Token stored in cookies

Categories

Resources