/**
* OpenIDConnect client for UnityBase
*
* var oID = require('openIDConnect'),
* oIdEndPoint = oID.registerEndpoint('openIDConnect');
* oIdEndPoint.registerProvider('Google',{
* authUrl: 'https://accounts.google.com/o/oauth2/auth',
* tokenUrl: 'https://accounts.google.com/o/oauth2/token',
* userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
* userInfoHTTPMethod: 'GET',
* scope: 'openid',
* nonce: '123',
* response_type: 'code',
* client_id: '350085411136-lpj0qvr87ce0r0ae0a3imcm25joj2t2o.apps.googleusercontent.com',
* client_secret: 'dF4qmUxhHoBAj-E1R8YZUCqA',
* getOnFinishAction: function (response) {
* return 'opener.UB.view.LoginWindow.onFinishOpenIDAuth(' + JSON.stringify(response) + '); close();'
* },
* getUserID: function(userInfo) {
* var inst = UB.Repository('uba_user').attrs(['ID'])
* .where('[name]', '=', userInfo.id).select();
* return inst.eof ? null : inst.get('ID');
* }
* });
*
* @module openIDConnect
*/
module.exports = openIDConnect;
var queryString, endpoints = {};
queryString = require('querystring');
/**
* Register openID connect endpoint
* @param {String} endpointName
* @returns {Object} endpoint
*/
openIDConnect.registerEndpoint = function (endpointName) {
var providers = {};
if (endpoints[endpointName]) {
throw new Error('Endpoints already registered');
}
App.registerEndpoint(endpointName, openIDConnect, false);
return endpoints[endpointName] = {
/**
* Add provider to endpoint
* @param {String} name
* @param {Object} providerConfig
* @param {String} providerConfig.authUrl Provider's authorisation url
* @param {String} providerConfig.tokenUrl Provider's token url
* @param {String} providerConfig.userInfoUrl provider's userinfo url
* @param {String} [providerConfig.userInfoHTTPMethod='GET'] Http method for userinfo request. Default GET
* @param {String} providerConfig.scope requested scopes delimited by '+' symbol
* @param {String} [providerConfig.nonce] nonce TODO - generate random and cache in GlobalCache with expire
* @param {String} providerConfig.response_type response type. Must contain code. This module use code responce type.
* @param {String} providerConfig.client_id client_id. Get it from provider
* @param {String} providerConfig.client_secret client_secret. Get it from provider
* @param {Function} providerConfig.getOnFinishAction Function, that returns client-side code to be run after success/fail response from OpenID provider.
* For example: `opener.$App.onFinishOpenIDAuth`. In case of success will be called with `{success: true, data: userData, secretWord: secretWord}`
* In case of fail `{success: false}`
* @param {Function} providerConfig.getUserID Called with one 1 parameter - provider's response for userInfo request. Must return user ID from uba_user entity if user is authorised or null else
*/
registerProvider: function (name, providerConfig) {
// todo: check all parameters and add documentation for class
if (providers[name]) {
throw new Error('Provider already registered');
}
if (!providerConfig.getOnFinishAction) {
throw new Error('getOnFinishAction must me be a function');
}
providers[name] = providerConfig;
},
getProviderList: function (){
return Object.keys(providers);
},
getProvider: function(providerName){
return providers[providerName];
}
};
};
/**
* If called as host:port[/app]/endpoint - return a list of registered openIDConnect providers,
* If called as host:port[/app]/endpoint/provider without parameters - redirect to provider `authUrl`
* If called as host:port[/app]/endpoint/provider with parameters `code` and `state` - call doProviderAuthHandshake method
*
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
*/
function openIDConnect(req, resp) {
var paramStr, params,
providerName = req.uri,
provider,
url = req.url.split('?')[0],
endpointUrl = providerName ? url.substr(0, url.length - providerName.length - 1) : url,
endpointName = endpointUrl.substr(endpointUrl.lastIndexOf('/') - endpointUrl.length + 1),
endpoint = endpoints[endpointName],
redirect_url;
if (!endpoint) {
returnInvalidRequest(resp, 'Endpoint is not registered');
} else {
if (!providerName) {
returnProviderList(resp, endpoint);
} else {
provider = endpoint.getProvider(providerName);
if (!provider) {
returnInvalidRequest(resp, 'Provider not registered');
} else {
//TODO - check Refer (if any) is starts from App.serverURL
//redirect_url = getValueFromHeaders(req, 'Referer');
//if (redirect_url) {
// redirect_url = redirect_url.substr(0, redirect_url.lastIndexOf('/')) + '/' + endpointName + '/' + providerName;
//}
redirect_url = App.serverURL + (App.serverURL[App.serverURL.length - 1] === '/' ? '' : '/') + endpointName + '/' + providerName;
paramStr = (req.method === 'GET') ? req.parameters : req.read();
params = paramStr ? queryString.parse(paramStr) : null;
if (!paramStr || params.mode === "auth") {
redirectToProviderAuth(req, resp, provider, redirect_url, params);
} else {
params = queryString.parse(paramStr);
if (params.code && params.state) {
//if (!redirect_url)
// redirect_url = atob(params.state);
var origin = '',
headers = (req.headers || '').split('\r\n');
if (headers) {
headers.forEach(function (v) {
if (v && v.substring(0, 7) === 'Origin:') {
origin = v.substring(7);
return false;
}
});
}
doProviderAuthHandshake(resp, params.code, params.state, provider, redirect_url, origin);
} else {
notifyProviderError(resp, provider);
}
}
}
}
}
}
function returnProviderList(resp, endpoint) {
resp.statusCode = 200;
resp.writeEnd(JSON.stringify(endpoint.getProviderList()));
}
/**
*
* @param {Object} customHeaders
* @returns {string}
*/
function getAuthCustomHeadersString(customHeaders) {
if (customHeaders == null) {
return '';
}
if (typeof customHeaders !== 'object') {
throw new Error('custom headers must be an object');
}
var headerStringArray = [];
Object.keys(customHeaders).forEach(function (key) {
headerStringArray.push(key + '=' + customHeaders[key]);
});
return '&' + headerStringArray.join('&');
}
function redirectToProviderAuth(req, resp, providerConfig, redirect_url, requestParams) {
resp.statusCode = 302;
var customHeaders = "";
if (typeof providerConfig.getAuthCustomHeaders === "function") {
customHeaders = getAuthCustomHeadersString(providerConfig.getAuthCustomHeaders(requestParams));
}
resp.statusCode = 302;
resp.writeEnd('');
resp.writeHead('Location: ' + providerConfig.authUrl
+ '?scope=' + providerConfig.scope
+ '&state=' + btoa('fakestate') //TODO - get it from global cache
+ (providerConfig.nonce ? '&nonce=' + providerConfig.nonce : '')
+ '&redirect_uri=' + redirect_url
+ '&response_type=' + providerConfig.response_type
+ '&client_id=' + providerConfig.client_id
+ '&response_mode=form_post'
+ '&referer=' + redirect_url
// + '&referer=BACK_REDIRECT_URL'
+ customHeaders
);
}
/**
*
* @param {THTTPResponse} resp
* @param provider
*/
function notifyProviderError(resp, provider){
resp.statusCode = 200;
resp.write('<!DOCTYPE html><html>');
resp.write('<head><meta http-equiv="X-UA-Compatible" content="IE=edge" /></head>');
resp.write('<body><script>');
resp.write(provider.getOnFinishAction({success: false}));
resp.write('</script></body>');
resp.writeEnd('</html>');
resp.writeHead('Content-Type: text/html');
}
function doProviderAuthHandshake(resp, code, state, provider, redirect_url, orign) {
var http = require('http'),
request, response,
responseData,
userID;
request = http.request(provider.tokenUrl);
request.options.method = 'POST';
request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
request.write('grant_type=authorization_code');
request.write('&code=' + code);
request.write('&client_id=' + provider.client_id);
request.write('&client_secret=' + provider.client_secret);
request.write('&redirect_uri=' + redirect_url);
response = request.end();
if (response.statusCode === 200) {
responseData = JSON.parse(response.read()); //response._http.responseText
if (provider.userInfoHTTPMethod == 'POST') {
request = http.request(provider.userInfoUrl);
request.options.method = 'POST';
request.write('access_token=' + responseData.access_token);
request.write('&client_id=' + provider.client_id);
request.write('&client_secret=' + provider.client_secret);
request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
} else {
request = http.request(provider.userInfoUrl + '?access_token=' + responseData.access_token);
}
response = request.end();
if (response.statusCode == 200) {
responseData = JSON.parse(response._http.responseText);
userID = provider.getUserID(responseData);
if (!userID) {
notifyProviderError(resp, provider);
} else {
var loginResp = Session.setUser(userID, code);
var objConnectParam = {success: true, data: JSON.parse(loginResp) ,secretWord: code};
resp.statusCode = 200;
resp.write('<!DOCTYPE html><html>');
resp.write('<head><meta http-equiv="X-UA-Compatible" content="IE=edge" /></head>');
resp.write('<body><div id="connectParam" >');
resp.write(JSON.stringify(objConnectParam));
resp.write('</div><script>');
resp.write(provider.getOnFinishAction(objConnectParam));
resp.write('</script></body>');
resp.writeEnd('</html>');
resp.writeHead('Content-Type: text/html');
resp.writeHead('Access-Control-Allow-Credentials:true');
resp.writeHead('Access-Control-Allow-Methods:GET, OPTIONS');
resp.writeHead('Access-Control-Allow-Origin:' + orign);
}
}
} else {
console.error('OpenIDConnect provider return invalid response', response.statusCode, '. Headers:', response.headers());
notifyProviderError(resp, provider);
}
}
function returnInvalidRequest(resp, message) {
resp.statusCode = 400;
resp.writeEnd(message);
}
function getValueFromHeaders(req, headerName) {
var result = null;
req.headers.split('\r\n').forEach(function (value) {
var pair = value.split(': ');
if (pair[0].toLowerCase() === headerName.toLowerCase())
result = pair[1];
});
return result;
}