Search

Domino Upgrade

VersionSupport end
5.0
6.0
6.5
7.0
8.0
8.5
Upgrade to 9.x now!
(see the full Lotus lifcyle) To make your upgrade a success use the Upgrade Cheat Sheet.
Contemplating to replace Notes? You have to read this! (also available on Slideshare)

Languages

Other languages on request.

Twitter

Useful Tools

Get Firefox
Use OpenDNS
The support for Windows XP has come to an end . Time to consider an alternative to move on.

About Me

I am the "IBM Collaboration & Productivity Advisor" for IBM Asia Pacific. I'm based in Singapore.
Reach out to me via:
Follow notessensei on Twitter
(posts)
Skype
Sametime
IBM
Facebook
LinkedIn
XING
Amazon Store
Amazon Kindle
NotesSensei's Spreadshirt shop
profile for stwissel on Stack Exchange, a network of free, community-driven Q&A sites

« GIT deploy your static sites - Part 1 | Main| Agile Outsourcing »

SAML and the Command Line

One of the best kept secrets of Connections Cloud S1 is the Traveler API. The API allows interactions that are missing from the Admin UI, like deleting a specific device or implementing an approval workflow.
Unfortunately the API only offers authentication via SAML, OAuth or BasicAuth are missing. So any application interacting with the API needs to do The SAML Dance. That's annoying when you have an UI to use, and a formidable challenge when you have a command line application, like a cron Job running unsupervised at interval.
One lovely step in the process: the IBM IdP returns a HTML page with a hidden form containing the SAML assertion result to be posted back to the application provider. Quite interesting, when your application provider is a command line app. Let's get to work.
The script is written in node.js and uses request and fast-html-parser npm package. The first step is to load the login form (which comes with a first set of cookies)
var requestOptionsTemplate = {
    headers: {
        'Origin': 'https://api.notes.ap.collabserv.com/api/traveler/',
        'User-Agent': 'ancy CommandLine Script',
        'Connection': 'keep-alive',
        'Cache-Control': 'max-age=0',
        'Upgrade-Insecure-Requests': 1
    },
    'method': 'GET'
};

function scLoginPart1() {
    console.log('Authenticating to SmartCloud ...');
    var requestOptions = Object.assign({}, requestOptionsTemplate);
    requestOptions.url = 'https://apps.na.collabserv.com/manage/account/dashboardHandler/input';
    request(requestOptions, scLoginPart2);
}

The function calls the URL where the login form can be found. The result gets delivered to the function scLoginPart2. That function makes use of a global configuration variable config that was created through const config = require("./config.json") and contains all the credentials we need. Step2 submits the form and hands over to Step3.
function scLoginPart2(err, httpResponse, body) {
    if (err) {
        return console.error(err);
    }
    // Capture cookies
    var outgoingCookies = captureCookies(httpResponse);
    var requestOptions = Object.assign({}, requestOptionsTemplate);
    requestOptions.headers.Cookie = outgoingCookies.join('; ');
    requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    requestOptions.method = 'POST';
    requestOptions.url = 'https://apps.ap.collabserv.com/pkmslogin.form';
    requestOptions.form = {
        'login-form-type': 'pwd',
        'error-code': '',
        'username': config.smartcloud.user,
        'password': config.smartcloud.password,
        'show_login': 'showLoginAgain'
    }
    request(requestOptions, scLoginPart3);
}

function captureCookies(response) {
    var incomingCookies = response.headers['set-cookie'];
    var outgoingCookies = [];
    if (incomingCookies) {
        incomingCookies.forEach(function(cookie) {
            outgoingCookies.push(cookie.split(';')[0]);
        });
    }
    // Array, allows for duplicate coolie names
    return outgoingCookies;
}

Part 3 / 4 finally collect all the cookies we need, so to turn attention to getting the API token in step 5
function scLoginPart3(err, httpResponse, body) {
    if (err) {
        console.error('Login failed miserably');
        return console.error(err);
    }
    // Login returns not 200 but 302
    // see https://developer.ibm.com/social/2015/06/23/slight-changes-to-the-form-based-login/
    if (httpResponse.statusCode !== 302) {
        return console.error('Wrong status code received: ' + httpResponse.statusCode);
    }

    var outgoingCookies = captureCookies(httpResponse);
    var redirect = httpResponse.headers.location;

    // This is the 3rd request we need to make to get finally all cookies for app.na
    var requestOptions = Object.assign({}, requestOptionsTemplate);
    requestOptions.headers.Cookie = outgoingCookies.join('; ');
    requestOptions.url = redirect;
    request(requestOptions, scLoginPart4);
}

function scLoginPart4(err, httpResponse, body) {
    if (err) {
        console.error('Login redirect failed miserably');
        return console.error(err);
    }
    var cookieHarvest = captureCookies(httpResponse);
    // Now we have some cookies in app, we need the SAML dance for api.notes
    scLoginPart5(cookieHarvest)
}

In Part 5 we first request the URL with actual data (devices in our case), but get another SAML dance step, since we have apps.na vs api.notes in the URL

function scLoginPart5(incomingCookies) {
    console.log("Executing SAML postback to SmartCloud");
    var requestOptions = Object.assign({}, requestOptionsTemplate);
    requestOptions.headers.Cookie = incomingCookies.join('; ');
    // Here is the first time wa actually request the data we want
    requestOptions.url = 'https://api.notes.ap.collabserv.com/api/traveler/devices';
    request(requestOptions, scLoginPart6);
}

Part 6 is the interesting one. If not authenticated against the api.notes URL yet, the server will return an HTML form with a JavaScript action that posts that form, containing the SAML assertion to the api URL. Since we don't use a browser to handle that automatically, we need to grab the html, extract the form and post it ourselves
function scLoginPart6(err, httpResponse, body) {
    if (err) {
        return console.error(err);
    }
    // Check the content for HTML
    var contentType = httpResponse.headers['content-type'];
    if (contentType.indexOf('html') > 0) {
        var root = htmlparser.parse(body);
        var samlForm = root.querySelector('form');
        // We need action
        var samlAttr = samlForm.attributes;
        var action = samlAttr['action'];
        console.log('SmartCloud login action:' + action);
        // checking if the action is a full qualified URL
        if (action.substring(0, 4) != 'http') {
            // That would be an error condition
            console.error('Authentication to SmartCloud failed');
            process.exit(1);
        }
        var samlFields = samlForm.querySelectorAll('input');
        var postbackform = {};
        samlFields.forEach(function(field) {
            var attr = field.attributes;
            var fName = attr['name'];
            var fValue = attr['value'];
            postbackform[fName] = fValue;
        });

        var newOptions = Object.assign({}, requestOptionsTemplate);
        newOptions.method = 'POST';
        newOptions.form = postbackform;
        newOptions.url = action;
        newOptions.headers['Cookie'] = captureCookies(httpResponse).join('; ');
        request(newOptions, scLoginPart7);
    } else {
        console.error('Authentication to SmartCloud failed');
        process.exit(1);
    }
}

Part 7 then processes an successful redirect to get our first actual payload.
function scLoginPart7(err, response, body) {
    if (err) {
        return console.error(err);
    }
    var resultCookies = captureCookies(response);
    var location = response.headers['location'];
    var nextDance = Object.assign({}, requestOptionsTemplate);
    nextDance.headers['Cookie'] = resultCookies.join('; ');
    nextDance.url = location;
    nextDance.method = 'GET';
    delete nextDance.headers['Content-Type'];

    request(nextDance, function(err, danceResponse, goodBody) {
        // Here is the body of the first page
        doSomethingUseful(goodBody, resultCookies);
    });
}

The error path in the code isn't very well modeled, so there's work left to do. So there were just 7 bridges to cross.
As usual YMMV.

Comments

Disclaimer

This site is in no way affiliated, endorsed, sanctioned, supported, nor enlightened by Lotus Software nor IBM Corporation. I may be an employee, but the opinions, theories, facts, etc. presented here are my own and are in now way given in any official capacity. In short, these are my words and this is my site, not IBM's - and don't even begin to think otherwise. (Disclaimer shamelessly plugged from Rocky Oliver)
© 2003 - 2017 Stephan H. Wissel - some rights reserved as listed here: Creative Commons License
Unless otherwise labeled by its originating author, the content found on this site is made available under the terms of an Attribution/NonCommercial/ShareAlike Creative Commons License, with the exception that no rights are granted -- since they are not mine to grant -- in any logo, graphic design, trademarks or trade names of any type. Code samples and code downloads on this site are, unless otherwise labeled, made available under an Apache 2.0 license. Other license models are available on written request and written confirmation.