Skip to content

Commit

Permalink
feat(authentication): Support OpenID and generic command-based authen…
Browse files Browse the repository at this point in the history
…tication (#247)
  • Loading branch information
jaredallard authored and silasbw committed Apr 4, 2018
1 parent 1b56da6 commit 98fe599
Show file tree
Hide file tree
Showing 8 changed files with 590 additions and 38 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
node_modules/
coverage/
npm-debug.log

# Assuming you don't want to check this in
yarn.lock
yarn-error.log
50 changes: 50 additions & 0 deletions lib/auth-providers/cmd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Fetches a bearer token via comamnd
*/

'use strict';

// for API compatability
/* eslint no-sync: 0 */
const spawnSync = require('child_process').spawnSync;

function getProperty(propertyName, object) {
// remove leading .
if (propertyName.match(/^\./)) {
propertyName = propertyName.replace(/^\./, '');
}

const parts = propertyName.split('.');
const length = parts.length;

let property = object || this;
for (let i = 0; i < length; i++) {
property = property[parts[i]];
}

return property;
}

module.exports = {
refresh: function (config) {
return new Promise((resolv, reject) => {
const cmd = config['cmd-path'];
const args = config['cmd-args'].split(' ');

const output = spawnSync(cmd, args, {
windowsHide: true
});

let result;
try {
result = JSON.parse(output.stdout.toString('utf8'));
} catch (err) {
return reject(new Error('Failed to run cmd.'));
}

const token = getProperty(config['token-key'].replace(/[{}]+/g, ''), result);

return resolv(token);
});
}
};
27 changes: 27 additions & 0 deletions lib/auth-providers/openid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Refreshes a OpenID token.
*/

'use strict';

const Issuer = require('openid-client').Issuer;

module.exports = {
refresh: function (config) {
return new Promise((resolv, reject) => {
Issuer.discover(config['idp-issuer-url'])
.then(function (ourIssuer) {
const client = new ourIssuer.Client({
client_id: config['client-id'],
client_secret: config['client-secret']
});

return client.refresh(config['refresh-token']);
})
.then(tokenSet => {
return resolv(tokenSet.id_token);
})
.catch(reject);
});
}
};
33 changes: 30 additions & 3 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ function fromKubeconfig(kubeconfig, current) {

let cert;
let key;
const auth = {};

let auth = {};
if (user) {
if (user['client-certificate']) {
cert = fs.readFileSync(path.normalize(user['client-certificate']));
Expand All @@ -97,8 +98,34 @@ function fromKubeconfig(kubeconfig, current) {
auth.bearer = user.token;
}

if (user['auth-provider'] && user['auth-provider'].config && user['auth-provider'].config['access-token']) {
auth.bearer = user['auth-provider'].config['access-token'];
if (user['auth-provider']) {
const config = user['auth-provider'].config;

// if we can't determine the type, just fail later (or don't refresh).
let type = null;
let token = null;
if (config['cmd-path']) {
type = 'cmd';
token = config['access-token'];
} else if (config['idp-issuer-url']) {
type = 'openid';
token = config['id-token'];
}

// If we have just an access-token, allow that... will expire later though.
if (config['access-token'] && !type) {
token = config['access-token'];
}

auth = {
request: {
bearer: token
},
provider: {
config,
type
}
};
}

if (user.username) auth.user = user.username;
Expand Down
46 changes: 44 additions & 2 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

const request = require('request');

/**
* Refresh whatever authentication {type} is.
* @param {String} type - type of authentication
* @param {Object} config - auth provider config
* @returns {Promise} with request friendly auth object
*/
function refreshAuth(type, config) {
return new Promise((resolv, reject) => {
const provider = require(`./auth-providers/${type}.js`);
provider.refresh(config)
.then(result => {
const auth = {
bearer: result
};

return resolv(auth);
})
.catch(err => reject(err));
});
}


class Request {
/**
* Internal representation of HTTP request object.
Expand All @@ -21,13 +43,19 @@ class Request {
this.requestOptions.ca = options.ca;
this.requestOptions.cert = options.cert;
this.requestOptions.key = options.key;
this.authProvider = {
type: null
};

if ('insecureSkipTlsVerify' in options) {
this.requestOptions.strictSSL = !options.insecureSkipTlsVerify;
}

if (options.auth) {
this.requestOptions.auth = options.auth;
if (options.auth.provider) {
this.requestOptions.auth = options.auth.request;
this.authProvider = options.auth.provider;
}
}
}

Expand Down Expand Up @@ -60,9 +88,23 @@ class Request {

if (typeof cb !== 'function') return request(requestOptions);

const auth = this.authProvider;
return request(requestOptions, (err, res, body) => {
if (err) return cb(err);
cb(null, { statusCode: res.statusCode, body: body });

// Refresh auth if 401
if (res.statusCode === 401 && auth.type) {
return refreshAuth(auth.type, auth.config)
.then(newAuth => {
requestOptions.auth = newAuth;
return request(requestOptions, (err, res, body) => {
if (err) return cb(err);
return cb(null, { statusCode: res.statusCode, body });
});
});
}

return cb(null, { statusCode: res.statusCode, body: body });
});
}
}
Expand Down
Loading

0 comments on commit 98fe599

Please sign in to comment.