OSDB  OSDB REST API - Authentication

Contents

See Also

Introduction

OSDB uses WebID to authenticate users and, via ACLs, authorize access to the OSDB UI and REST API. The client X.509 certificate required to establish a secure HTTPS connection must contain the user’s WebID in the Subject Alternative Name (SAN) field.

When accessing the OSDB UI from a browser, the browser handles creation of the HTTPS connection and certificate exchange. On clicking a ‘Login’ link and assuming the user successfully authenticates, the browser receives a session cookie, osdb.sid, containing the user’s WebID. The session cookie identifies the user for the duration of the session.

When accessing the OSDB REST API from a non-browser client, the client is responsible for supplying the client certificate, retrieving the OSDB session cookie on successful authentication and supplying the cookie with each subsequent request. The REST API provides two methods to start and end a session:

GET /osdb/api/v1/login
GET /osdb/api/v1/logout

The examples below illustrate using these methods from curl and a simple Node.js client application. Two versions of the client are shown, one using the third-party ‘request’ module for making HTTP requests, the other using the Node.js core module ‘http’.

The examples use certificate cmsb-170223-crt.pem (or its .p12 equivalent) containing the WebID http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity, as shown by the certificate dump below.

openssl x509 -text -noout -in cmsb-170223-crt.pem

produces

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            95:29:79:9a:f9:4b:4d:5e
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: C=US, ST=MA, L=Boston, O=OpenLink Software, OU=Buxton, CN=ca/emailAddress=buxton_noexist@openlinksw.com
        Validity
            Not Before: Feb 24 11:02:52 2017 GMT
            Not After : Feb 22 11:02:52 2027 GMT
        Subject: C=US, ST=Massachusetts, L=Burlington, O=OpenLink Software, CN=Carl Blakeley
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:bd:0f:07:19:c0:3c:b3:58:be:aa:9f:93:85:fd:
            			...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                URI:http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity
        ...

Example: Using Curl

curl -ik --cert-type P12  --cert "./cmsb-170223-crt.p12:password" https://localhost:8000/osdb/api/v1/login

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 179
ETag: W/"b3-SJalHC4WMMLoRTaVQbahBg"
set-cookie: osdb.sid=s%3ATT5wTYHe4vFO-lLtn_3DANY3oiFuS47w.q9Z1gHIV8DKOg5NjA0De3GB7t64dHzBxyX691Trp98w; Domain=localhost; Path=/; Expires=Sat, 17 Jun 2017 11:31:19 GMT; HttpOnly; Secure
Date: Fri, 16 Jun 2017 11:31:19 GMT
Connection: keep-alive

{"status":"success","method":"login","api":"/osdb/api/v1/login","response":{"user":"http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity"}}

curl -ik --cookie "osdb.sid=s%3ATT5wTYHe4vFO-lLtn_3DANY3oiFuS47w.q9Z1gHIV8DKOg5NjA0De3GB7t64dHzBxyX691Trp98w" https://localhost:8000/osdb/api/v1/services

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 3060
ETag: W/"bf4-apjpsjKDVNlzwUWjXSKjJQ"
set-cookie: osdb.sid=s%3ATT5wTYHe4vFO-lLtn_3DANY3oiFuS47w.q9Z1gHIV8DKOg5NjA0De3GB7t64dHzBxyX691Trp98w; Domain=localhost; Path=/; Expires=Sat, 17 Jun 2017 11:35:14 GMT; HttpOnly; Secure
Date: Fri, 16 Jun 2017 11:35:14 GMT
Connection: keep-alive

{"status":"success","method":"list_services","api":"/osdb/api/v1/services","response":[{"service_id":"csv_transformer","service_name":"csv_transformer" ...

curl -ik --cookie "osdb.sid=s%3ATT5wTYHe4vFO-lLtn_3DANY3oiFuS47w.q9Z1gHIV8DKOg5NjA0De3GB7t64dHzBxyX691Trp98w" https://localhost:8000/osdb/api/v1/logout

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 181
ETag: W/"b5-++kUIBttioK5g6NrJqqfgw"
Date: Fri, 16 Jun 2017 11:36:21 GMT
Connection: keep-alive

{"status":"success","method":"logout","api":"/osdb/api/v1/logout","response":{"user":"http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity"}}

curl -ik --cookie "osdb.sid=s%3ATT5wTYHe4vFO-lLtn_3DANY3oiFuS47w.q9Z1gHIV8DKOg5NjA0De3GB7t64dHzBxyX691Trp98w" https://localhost:8000/osdb/api/v1/services 
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 138
ETag: W/"8a-MzyLp5f5pu8M5WqlU7Hsag"
Date: Fri, 16 Jun 2017 11:37:11 GMT
Connection: keep-alive

{"status":"error","api":"/osdb/api/v1/services","response":"Access to https://localhost:8000/osdb/api/v1/services requires authorization"}

Example: Node.js Client v1

This first Node.js client, using module ‘request’, exercises an Uber API via the OSDB server. The client outputs which follow illustrate the OSDB responses under three different authentication/authorization scenarios.

"use strict"
 
var async = require('async');
var cookie = require('cookie');
var fs = require('fs'); 
var request = require('request');
var url = require('url');

function osdb_api_response_hndlr (err, response, body)
{
  if (err) {
    console.log(err);
  } else {
    console.log(`Response from: ${response.request.method} ${response.request.uri.href}`);
    console.log('status code:', response.statusCode);
    console.log('content type:', response.headers['content-type']);
    console.log('data:');
    console.log(body);
    console.log();
  }
};

function osdb_tasks(client_key, client_cert, server_cert, tasks_cb) {
  let osdb_session_cookie = null;
  let cookies = [];
  let key = client_key ? fs.readFileSync(client_key) : null;
  let cert = client_cert ? fs.readFileSync(client_cert) : null;
  let ca = server_cert ? fs.readFileSync(server_cert) : null; 
  // ca is required if using a self-signed certificate

  async.series([
    function (callback) {
      let options = { cert: cert, key: key, ca: ca, rejectUnauthorized: false, headers: {} };
      options.url = 'https://localhost:8000/osdb/api/v1/login';
      options.method = "GET";

      request(options, function(err, response, body) {
        if (err) {
          return callback(err);
        }

        // Extract OSDB session key
        let setcookie = response.headers["set-cookie"];
        if (setcookie) {
          setcookie.forEach(
            function (cookiestr) {
             cookies.push(cookie.parse(cookiestr));
             let parsed_cookie = cookie.parse(cookiestr);
             if (parsed_cookie['osdb.sid']) {
               osdb_session_cookie = `osdb.sid=${parsed_cookie['osdb.sid']}`;
             }
            }
          );
        }

        tasks_cb(err, response, body);
        if (response.statusCode >= 400)
          callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${body}`));
        else
          callback(null, 'login');
      });
    },
    function (callback) {
      let options = { cert: cert, key: key, ca: ca, rejectUnauthorized: false, headers: {} };
      if (osdb_session_cookie)
        options.headers['Cookie'] = osdb_session_cookie;
      options.url = 'https://localhost:8000/osdb/api/v1/actions/uber/products/exec';
      options.method = "POST";
      options.json = { latitude: "37.7759792", longitude: "-122.41823" };

      request(options, function(err, response, body) {
        if (err) {
          return callback(err);
        }
        tasks_cb(err, response, body);
        if (response.statusCode >= 400)
          callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${body}`));
        else
          callback(null, 'execute action');
      });
     },
     function (callback) {
      let options = { cert: cert, key: key, ca: ca, rejectUnauthorized: false, headers: {} };
      if (osdb_session_cookie)
        options.headers['Cookie'] = osdb_session_cookie;
      options.url = 'https://localhost:8000/osdb/api/v1/logout';
      options.method = "GET";

      request(options, function(err, response, body) {
        if (err) {
          return callback(err);
        }
        tasks_cb(err, response, body);
        if (response.statusCode >= 400)
          callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${body}`));
        else {
          osdb_session_cookie = null;
          callback(null, 'logout');
        }
      });
     }
   ], 
   function (err, results) {
     if (err)
      console.log('#osdb_tasks:', err);
     else
      console.log('#osdb_tasks: Completed tasks:' , results);
   });
 }

// ==========================================================================

let myKey = 'cmsb-170223-key.pem';
let myCrt = 'cmsb-170223-crt.pem'; 
let serverSelfSignedCert = 'server.crt';

osdb_tasks(myKey, myCrt, serverSelfSignedCert, osdb_api_response_hndlr);

Output: Client not authenticated

Sample output when the client hasn’t logged in.

Response from: POST https://localhost:8000/osdb/api/v1/actions/uber/products/exec
status code: 401
content type: application/json; charset=utf-8
data:
{ status: 'error',
  api: '/osdb/api/v1/actions/uber/products/exec',
  response: 'Access to https://localhost:8000/osdb/api/v1/actions/uber/products/exec requires authorization' }

Output: Client authenticated and authorized

Sample output when the client has logged in and has ACL permissions to access the relevant OSDB REST API endpoints.

Response from: GET https://localhost:8000/osdb/api/v1/login
status code: 200
content type: application/json; charset=utf-8
data:
{"status":"success","method":"login","api":"/osdb/api/v1/login","response":{"user":"http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity"}}

Response from: POST https://localhost:8000/osdb/api/v1/actions/uber/products/exec
status code: 200
content type: application/json; charset=utf-8
data:
{ products: 
   [ { capacity: 4,
       product_id: '57c0ff4e-1493-4ef9-a4df-6b961525cf92',
       price_details: [Object],
       image: 'http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-uberselect.png',
       cash_enabled: false,
       shared: false,
       short_description: 'SELECT',
       display_name: 'SELECT',
       product_group: 'uberx',
       description: 'A STEP ABOVE THE EVERY DAY' },
       ...
     { capacity: 4,
       product_id: '3ab64887-4842-4c8e-9780-ccecd3a0391d',
       price_details: null,
       image: 'http://d1a3f4spazzrp4.cloudfront.net/car-types/mono/mono-taxi.png',
       cash_enabled: false,
       shared: false,
       short_description: 'TAXI',
       display_name: 'TAXI',
       product_group: 'taxi',
       description: 'TAXI WITHOUT THE HASSLE' } ] }

Response from: GET https://localhost:8000/osdb/api/v1/logout
status code: 200
content type: application/json; charset=utf-8
data:
{"status":"success","method":"logout","api":"/osdb/api/v1/logout","response":{"user":"http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity"}}

#osdb_tasks: Completed tasks: [ 'login', 'execute action', 'logout' ]

Output: Client authenticated but unauthorized

Sample output when the client has authenticated but is denied access to a REST API endpoint by an ACL.

Response from: GET https://localhost:8000/osdb/api/v1/login
status code: 200
content type: application/json; charset=utf-8
data:
{"status":"success","method":"login","api":"/osdb/api/v1/login","response":{"user":"http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity"}}

Response from: POST https://localhost:8000/osdb/api/v1/actions/uber/products/exec
status code: 403
content type: application/json; charset=utf-8
data:
{ status: 'error',
  api: '/osdb/api/v1/actions/uber/products/exec',
  response: 'Access denied for http://ods-qa.openlinksw.com:8896/DAV/home/nobody/cmsb_ex_webid_170223.ttl#identity' }

Example: Node.js Client v2

This example shows an equivalent client application, using Node.js core module ‘http’ in place of ‘request’.

"use strict"

var async = require('async');
var cookie = require('cookie');
var fs = require('fs'); 
var http = require('http'); 
var https = require('https'); 
var url = require('url');

// --------------------------------------------------------------------------

function Request (key, cert, ca)
{
  this.maxRedirects = 10;
  this.redirects = 0;
  this.key = key ? fs.readFileSync(key) : null;
  this.cert = cert ? fs.readFileSync(cert) : null;
  this.ca = ca ? fs.readFileSync(ca) : null; 
  // ca is required if using a self-signed certificate
  this.response_data = null;
}

Request.prototype.get = function (href, headers, callback) {
  var uri = url.parse(href);

  var options = { 
    hostname: uri.hostname,
    path: uri.path,
    headers: headers || { },
  };

  if (uri.port) options.port = uri.port;
  if (uri.protocol === 'https:')
  {
    options.key = this.key;
    options.cert = this.cert;
    options.ca = this.ca ? [ this.ca ] : null;
    options.checkServerIdentity = function (host, cert) { return undefined; };
  }

  var httpGet = uri.protocol === 'http:' ? http.get : https.get;
  var host = uri.protocol + '//' + options.hostname + (options.port ? ':' + options.port : '');

  function processResponse(response) {
    this.response_data = null;
    if (response.statusCode >= 300 && response.statusCode < 400) {
      if (this.redirects >= this.maxRedirects) {
        this.error = new Error('Too many redirects for: ' + href);
      } else {
        this.redirects++;
        href = url.resolve(host, response.headers.location);
        return this.get(href, callback);
      }
    }

    response.url = href;
    response.redirects = this.redirects;
    response.content_type = response.headers['content-type'];

    function end_hndlr () {
      response.data = this.response_data; 
      callback(this.error, response);
    }

    function data_hndlr (chunk) {
      // chunk is instanceof Buffer
      if (this.response_data)
        this.response_data += chunk.toString();
      else
        this.response_data = chunk.toString();
    }

    response.on('data', data_hndlr.bind(this));
    response.on('end', end_hndlr.bind(this));
  } 

  httpGet(options, processResponse.bind(this))
    .on('error', function(err) {
      callback(err);
    });
};

// --------------------------------------------------------------------------

function osdb_api_response_hndlr (err, res)
{
  if (err) {
    console.log(err);
  } else {
    console.log('Response from GET', res.url, 'after', res.redirects, 'redirects');
    console.log('status code:', res.statusCode);
    console.log('content type:', res.content_type);
    console.log('data:');
    console.log(res.data);
    console.log();
  }
};

function osdb_tasks(key, cert, ca_cert, tasks_cb) {
  let request = new Request(key, cert, ca_cert);
  let osdb_session_cookie = null;
  let cookies = [];

  async.series(
    [
      function (callback) {
        let url = 'https://localhost:8000/osdb/api/v1/login';
        let headers = {};
        request.get(url, headers, function(err, response) {
          if (err) {
            return callback(err);
          }

          // Extract OSDB session key
          let setcookie = response.headers["set-cookie"];
          if (setcookie)
          {
            setcookie.forEach(
              function (cookiestr) {
                cookies.push(cookie.parse(cookiestr));
                let parsed_cookie = cookie.parse(cookiestr);
                if (parsed_cookie['osdb.sid'])
                {
                  osdb_session_cookie = `osdb.sid=${parsed_cookie['osdb.sid']}`;
                }
              }
            );
          }

          tasks_cb(err, response);
          if (response.statusCode >= 400)
            callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${response.data}`));
          else
            callback(null, 'login');
        });
      },
      function (callback) {
        let url = 'https://localhost:8000/osdb/api/v1/services';
        let headers = {};
        if (osdb_session_cookie)
          headers['Cookie'] = osdb_session_cookie;

        request.get(url, headers, function(err, response) {
          if (err) {
            return callback(err);
          }
          tasks_cb(err, response);
          if (response.statusCode >= 400)
            callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${response.data}`));
          else
            callback(null, 'List services');
        });
      },
      function (callback) {
        let url = 'https://localhost:8000/osdb/api/v1/logout';
        let headers = {};
        
        if (osdb_session_cookie)
          headers['Cookie'] = osdb_session_cookie;

        request.get(url, headers, function(err, response) {
          if (err) {
            return callback(err);
          }
          tasks_cb(err, response);
          if (response.statusCode >= 400)
            callback(new Error(`From ${url}: (HTTP ${response.statusCode}) ${response.data}`));
          else
          {
            osdb_session_cookie = null;
            callback(null, 'logout');
          }
        });
      }
    ], 
    function (err, results) {
      if(err)
        console.log('#osdb_tasks:', err);
      else
        console.log('#osdb_tasks: Completed tasks:' , results);
    }
  );

}

// ==========================================================================

let myKey = 'cmsb-170223-key.pem';
let myCrt = 'cmsb-170223-crt.pem'; 
let serverSelfSignedCert = 'server.crt';

osdb_tasks(myKey, myCrt, serverSelfSignedCert, osdb_api_response_hndlr);