Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Working with a compressed base64 query parameter with NodeJS

Tags:

node.js

zlib

I'm writing a Node based web service that is going expecting a third party to send a SAML request that's deflated and base64 encoded. After flailing around for the better part of the day I fear I'm not sure I even know what to ask (I apologize if this is a dupe).

The docs say:

The SAMLRequest is first DEFLATE-compressed, then Base 64 encoded, then URL encoded. It must be decoded and parsed

The Webstorm Console log has this query string:

GET /login?SAMLRequest=fZJRa9swEMff9ymE3m1LipM4InbJ0mwrNCM0bqF9Gap8cQW2lOnksH37ObFbWgZ9lLj76X%2F30%2FLqT9uQE3g0zuaUx4wSsNpVxtY5vS%2B%2FRRm9Kr4sUbWNOMpVF17sHfzuAANZIYIPfd%2FaWexa8HvwJ6Ph%2Fu42py8hHFEmCV%2BImM%2BymMWCswRBd96Ev1GrrKrBJ2eueuXokUPJdc83VoVLqDPqA4lPmJwwxpLG1cZScnOd018HJeaLSsNkqhfAqql6nsyyVFQiEweWqnlfhrv%2BKXOCnB5Ug3C%2BwQ5uLAZlQ04F42nE5hHPSj6VKZc8jfmcP1Gy8y447Zqvxg6L6byVTqFBaVULKIOW%2B9X2VoqYyeehCOWPstxFq36yg9LhAjmZCvzPviOn352rGyAbG8AfvUEg%2B3E3ZDvshpKHVy3irKUXZVEOIj5PcBzj0mLwJi9z%2BveEzwFvRmgxLr%2B%2BxI21axN4i5zUqJIyjTaP23SzftyK9Wwm%2FnO8TN6HKMbjx79U%2FAM%3D

Output of

console.log(req.query.SAMLRequest)

shows the URL decoding is automatically done:

fZJRa9swEMff9ymE3m1LipM4InbJ0mwrNCM0bqF9Gap8cQW2lOnksH37ObFbWgZ9lLj76X/30/LqT9uQE3g0zuaUx4wSsNpVxtY5vS+/RRm9Kr4sUbWNOMpVF17sHfzuAANZIYIPfd/aWexa8HvwJ6Ph/u42py8hHFEmCV+ImM+ymMWCswRBd96Ev1GrrKrBJ2eueuXokUPJdc83VoVLqDPqA4lPmJwwxpLG1cZScnOd018HJeaLSsNkqhfAqql6nsyyVFQiEweWqnlfhrv+KXOCnB5Ug3C+wQ5uLAZlQ04F42nE5hHPSj6VKZc8jfmcP1Gy8y447Zqvxg6L6byVTqFBaVULKIOW+9X2VoqYyeehCOWPstxFq36yg9LhAjmZCvzPviOn352rGyAbG8AfvUEg+3E3ZDvshpKHVy3irKUXZVEOIj5PcBzj0mLwJi9z+veEzwFvRmgxLr++xI21axN4i5zUqJIyjTaP23SzftyK9Wwm/nO8TN6HKMbjx79U/AM=

So I assume I must decode the base64 then INFLATE it. (I've tried to inflate and then decode but that still failed and this makes the most sense):

var zlibJS = require('zlibjs');  //I've tried Node's own zlib but that only appears to work with call backs
...
var b = new Buffer(samlReq, 'base64');
var decoded = b.toString();
var inflated = zlibJS.inflateSync(decoded); //https://github.com/imaya/zlib.js/blob/master/test/node-test.js
//I get an Unsupported compression method 
...//relooking at the test it appears it wants a buffer so switch the above line for this
var inflated = zlibJS.inflateSync(b); // same 500 error

Looking through the github source this error is thrown when a switch falls through to the default when looking for a compression method however, I don't see where I'm expected to set that. I've tried to log like crazy(stripped out for readability) and put each step into base64 decoder (http://www.motobit.com/util/base64-decoder-encoder.asp), url decoder (http://meyerweb.com/eric/tools/dencoder/), and a string uncompressor (http://i-tools.org/gzip) with inconclusive results (the uncompressor tool fails to uncompress its own compression so...)

Thanks for any advice, tips and or tools that might help

like image 956
David Avatar asked Jul 18 '14 16:07

David


3 Answers

I had seen another SO answer mention pako and wikipedia mentions it in the ZLib article so I gave it a whirl and it worked second try:

var pako = require('pako'); 
...
var b = new Buffer(samlReq, 'base64');
var inflated = pako.inflateRaw(b, {to:'string'}); 
like image 67
David Avatar answered Oct 24 '22 23:10

David


Since you are using server-side JavaScript with Node.js, encoding and decoding of SAML Request and Response payloads can easily be done with no external dependencies by making use of Node's built-in zlib and Buffer support. There's no need for an external system like pako (even though it's pretty cool).

I've done just that with my saml-encode-decoder-js NPM package. It's used like this for decoding:

var saml = require('saml');

saml.decodeSamlRedirect(xml, function(err, xml) {
  if (!err) {
    console.log("Redirect decoded XML", xml);
  }
}

saml.decodeSamlPost(xml, function(err, xml) {
  if (!err) {
    console.log("POST decoded XML", xml);
  }
}

That NPM library is just a shortcut for the use of the built-in functions. For example, this is how saml-encoder-decoder-js decodes a SAML redirect binding payload:

var zlib = require('zlib');

function decodeSamlRedirect(encoded, cb) {
  if (encoded == null || encoded == "") {
    return cb(new Error('Cannot decode null string'));
  }
  var deflated = new Buffer(decodeURIComponent(encoded), 'base64');

  zlib.inflateRaw(deflated, function (err, inflated) {
    if (!err) {
      return cb(null, inflated.toString('ascii'));
    } else {
      return cb(err);
    }
  });
}

The trick is the use of zlib.deflateRaw() and zlib.inflateRaw() for encoding and decoding. You can't use deflate() and unzip().

If you are using SAML POST bindings, then really this is just a wrapper for using Node's Buffer for Base64 encoding/decoding, like here:

function decodeSamlPost(encoded, cb) {
  if (encoded == null || encoded == "") {
    return cb(new Error('Cannot decode null string'));
  }
  return cb(null, new Buffer(encoded, 'base64').toString('ascii'));
}
like image 45
Michael Oryl Avatar answered Oct 25 '22 01:10

Michael Oryl


From your question I derived that you're trying to make the the SAML Request that should be deflated, base64 encoded and afterwards URL encoded (last part only because it its used as part of an url)

I'm not sure if async can be used to solve your synchronous problem, but I would go with something similar to this:

var async = require('async');
var zlib = require('zlib');
var url = require('url');

var samlRequest = '<?xml version="1.0" encoding="UTF-8"?>'
  + '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">'
  + '...'
  + '</samlp:AuthnRequest>';

var buffer = new Buffer(samlRequest);

async.waterfall(
  [
    function deflate(callback) {
      zlib.deflate(buffer, function deflated(err, resultBuffer) {
        if (err) {
          callback(err);
          return;
        }

        callback(null, resultBuffer);
      });
    },
    function encode(buffer, callback) {
      callback(null, buffer.toString('base64'));
    },
    function urlEncode(request, callback) {
      callback(null, encodeURIComponent(request));
    }
  ], function handleResult(err, urlEncodedRequest) {
    if (err) {
      console.log(err);
      return;
    }

    var baseUrl = 'http:xxx.salesforce.com/idp/endpoint/HttpRedirect?SAMLRequest=';
    console.log(baseUrl + urlEncodedRequest);
  }
);

The samlRequest is of course just an example. I used three function which definitely is overkill, but I wanted to show you async in the process as well, and it nicely demonstrates all the steps.

like image 28
Kevin Sandow Avatar answered Oct 25 '22 00:10

Kevin Sandow