add authHandler option to deal with authorization header (#132)
authorCarlos Santana <csantanapr@apache.org>
Fri, 17 Aug 2018 08:28:05 +0000 (04:28 -0400)
committerJames Thomas <jthomas.uk@gmail.com>
Fri, 17 Aug 2018 08:28:05 +0000 (09:28 +0100)
* add authHandler option
Closes #131

README.md
lib/client.js
test/unit/client.test.js

index 74df04e..dd4a346 100644 (file)
--- a/README.md
+++ b/README.md
@@ -64,6 +64,24 @@ var ow = openwhisk(options);
 ow.actions.invoke('sample').then(result => console.log(result))
 ```
 
+#### using 3rd party authentication handler
+You can specify an authentication handler in `options.auth_handler` this is an object that provides a function `getAuthHeader` that returns a Promise or String to be used in the `Authorization` http header for every http request.
+```javascript
+const authHandler = {
+  getAuthHeader: ()=>{
+    return Promise.resolve('Basic user:password')
+  }
+}
+var openwhisk = require('openwhisk');
+var options = {
+  apihost: 'openwhisk.ng.bluemix.net',
+  auth_handler: authHandler
+}
+var ow = openwhisk(options)
+ow.actions.invoke('sample').then(result => console.log(result))
+``` 
+
+
 ### constructor options
 
 _Client constructor supports the following mandatory parameters:_
index 48723e3..7dd0982 100644 (file)
@@ -66,6 +66,7 @@ class Client {
    * @param {boolean} [options.ignore_certs]
    * @param {string} [options.apigw_token]
    * @param {string} [options.apigw_space_guid]
+   * @param {Function} [options.auth_handler]
    */
   constructor (options) {
     this.options = this.parseOptions(options || {})
@@ -91,13 +92,13 @@ class Client {
       apigwSpaceGuid = apiKey.split(':')[0]
     }
 
-    if (!apiKey) {
-      throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing api_key parameter.`)
+    if (!apiKey && !options.auth_handler) {
+      throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing api_key parameter or token plugin.`)
     } else if (!api) {
       throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing either api or apihost parameters.`)
     }
 
-    return {apiKey: apiKey, api, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid}
+    return {apiKey: apiKey, api, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler}
   }
 
   urlFromApihost (apihost) {
@@ -113,21 +114,23 @@ class Client {
   }
 
   request (method, path, options) {
-    const req = this.params(method, path, options)
-    return rp(req).catch(err => this.handleErrors(err))
+    const params = this.params(method, path, options)
+    return params.then(req => rp(req)).catch(err => this.handleErrors(err))
   }
 
   params (method, path, options) {
-    return Object.assign({
-      json: true,
-      method: method,
-      url: this.pathUrl(path),
-      rejectUnauthorized: !this.options.ignoreCerts,
-      headers: {
-        'User-Agent': (options && options['User-Agent']) || 'openwhisk-client-js',
-        Authorization: this.authHeader()
-      }
-    }, options)
+    return this.authHeader().then(header => {
+      return Object.assign({
+        json: true,
+        method: method,
+        url: this.pathUrl(path),
+        rejectUnauthorized: !this.options.ignoreCerts,
+        headers: {
+          'User-Agent': (options && options['User-Agent']) || 'openwhisk-client-js',
+          Authorization: header
+        }
+      }, options)
+    })
   }
 
   pathUrl (urlPath) {
@@ -143,10 +146,13 @@ class Client {
   }
 
   authHeader () {
-    const apiKeyBase64 = Buffer.from(this.options.apiKey).toString('base64')
-    return `Basic ${apiKeyBase64}`
+    if (this.options.authHandler) {
+      return this.options.authHandler.getAuthHeader()
+    } else {
+      const apiKeyBase64 = Buffer.from(this.options.apiKey).toString('base64')
+      return Promise.resolve(`Basic ${apiKeyBase64}`)
+    }
   }
-
   handleErrors (reason) {
     let message = `Unknown Error From API: ${reason.message}`
     if (reason.hasOwnProperty('statusCode')) {
index dc97643..6d8fad3 100644 (file)
@@ -108,12 +108,12 @@ test('should handle multiple api parameter formats', t => {
   t.is(client.urlFromApihost('http://my_host:80'), 'http://my_host:80/api/v1/')
 })
 
-test('should return default request parameters without options', t => {
+test('should return default request parameters without options', async t => {
   const client = new Client({api_key: 'username:password', apihost: 'blah'})
   const METHOD = 'get'
   const PATH = 'some/path/to/resource'
 
-  const params = client.params(METHOD, PATH)
+  const params = await client.params(METHOD, PATH)
   t.is(params.url, 'https://blah/api/v1/some/path/to/resource')
   t.is(params.method, METHOD)
   t.true(params.json)
@@ -121,13 +121,13 @@ test('should return default request parameters without options', t => {
   t.true(params.headers.hasOwnProperty('Authorization'))
 })
 
-test('should return request parameters with merged options', t => {
+test('should return request parameters with merged options', async t => {
   const client = new Client({api_key: 'username:password', apihost: 'blah'})
   const METHOD = 'get'
   const PATH = 'some/path/to/resource'
   const OPTIONS = {b: {bar: 'foo'}, a: {foo: 'bar'}}
 
-  const params = client.params(METHOD, PATH, OPTIONS)
+  const params = await client.params(METHOD, PATH, OPTIONS)
   t.is(params.url, 'https://blah/api/v1/some/path/to/resource')
   t.is(params.method, METHOD)
   t.true(params.json)
@@ -137,20 +137,30 @@ test('should return request parameters with merged options', t => {
   t.deepEqual(params.b, {bar: 'foo'})
 })
 
-test('should return request parameters with explicit api option', t => {
+test('should return request parameters with explicit api option', async t => {
   const client = new Client({api_key: 'username:password', api: 'https://api.com/api/v1'})
   const METHOD = 'get'
   const PATH = 'some/path/to/resource'
 
-  t.is(client.params(METHOD, PATH).url, 'https://api.com/api/v1/some/path/to/resource')
+  t.is((await client.params(METHOD, PATH)).url, 'https://api.com/api/v1/some/path/to/resource')
   client.options.api += '/'
-  t.is(client.params(METHOD, PATH).url, 'https://api.com/api/v1/some/path/to/resource')
+  t.is((await client.params(METHOD, PATH)).url, 'https://api.com/api/v1/some/path/to/resource')
 })
 
-test('should generate auth header from API key', t => {
+test('should generate auth header from API key', async t => {
   const apiKey = 'some sample api key'
   const client = new Client({api: true, api_key: apiKey})
-  t.is(client.authHeader(), `Basic ${Buffer.from(apiKey).toString('base64')}`)
+  t.is(await client.authHeader(), `Basic ${Buffer.from(apiKey).toString('base64')}`)
+})
+
+test('should generate auth header from 3rd party authHandler plugin', async t => {
+  const authHandler = {
+    getAuthHeader: () => {
+      return Promise.resolve('Basic user:password')
+    }
+  }
+  const client = new Client({api: true, auth_handler: authHandler})
+  t.is(await client.authHeader(), `Basic user:password`)
 })
 
 test('should return path and status code in error message', t => {