From 3a486a2f6561d8b62e93955c7d452ecfd3972de6 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Thu, 23 Jan 2020 19:22:01 -0500 Subject: [PATCH 1/4] WIP on consolidation of apiml-auth, zss-auth Signed-off-by: 1000TurquoisePogs --- plugins/sso-auth/lib/apimlHandler.js | 201 ++++++++++++++++++++ plugins/sso-auth/lib/safprofile.js | 151 +++++++++++++++ plugins/sso-auth/lib/ssoAuth.js | 237 +++++++++++++++++++++++ plugins/sso-auth/lib/tokenInjector.js | 23 +++ plugins/sso-auth/lib/zssHandler.js | 249 +++++++++++++++++++++++++ plugins/sso-auth/pluginDefinition.json | 18 ++ 6 files changed, 879 insertions(+) create mode 100644 plugins/sso-auth/lib/apimlHandler.js create mode 100644 plugins/sso-auth/lib/safprofile.js create mode 100644 plugins/sso-auth/lib/ssoAuth.js create mode 100644 plugins/sso-auth/lib/tokenInjector.js create mode 100644 plugins/sso-auth/lib/zssHandler.js create mode 100644 plugins/sso-auth/pluginDefinition.json diff --git a/plugins/sso-auth/lib/apimlHandler.js b/plugins/sso-auth/lib/apimlHandler.js new file mode 100644 index 00000000..3b6bcf23 --- /dev/null +++ b/plugins/sso-auth/lib/apimlHandler.js @@ -0,0 +1,201 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +const Promise = require('bluebird'); +const https = require('https'); +const fs = require('fs'); + +/*495 minutes default session length for zosmf + * TODO: This is the session length of a zosmf session according to their documentation. + * However, it is not clear if that is configurable or if APIML may use a different value under other circumstances + */ +const DEFAULT_EXPIRATION_MS = 29700000; + +function readUtf8FilesToArray(fileArray) { + var contentArray = []; + for (var i = 0; i < fileArray.length; i++) { + const filePath = fileArray[i]; + try { + var content = fs.readFileSync(filePath); + if (content.indexOf('-BEGIN CERTIFICATE-') > -1) { + contentArray.push(content); + } + else { + content = fs.readFileSync(filePath, 'utf8'); + if (content.indexOf('-BEGIN CERTIFICATE-') > -1) { + contentArray.push(content); + } + else { + this.logger.warn('Error: file ' + filePath + ' is not a certificate') + } + } + } catch (e) { + this.logger.warn('Error when reading file=' + filePath + '. Error=' + e.message); + } + } + + if (contentArray.length > 0) { + return contentArray; + } else { + return null; + } +} + + +class ApimlHandler { + constructor(pluginDef, pluginConf, serverConf, context) { + this.logger = context.logger; + this.apimlConf = serverConf.node.mediationLayer.server; + this.gatewayUrl = `https://${this.apimlConf.hostname}:${this.apimlConf.gatewayPort}`; + + if (serverConf.node.https.certificateAuthorities === undefined) { + this.logger.warn("This server is not configured with certificate authorities, so it will not validate certificates with APIML"); + this.httpsAgent = new https.Agent({ + rejectUnauthorized: false + }); + } else { + this.httpsAgent = new https.Agent({ + rejectUnauthorized: true, + ca: readUtf8FilesToArray(serverConf.node.https.certificateAuthorities) + }); + } + } + + /** + * Should be called e.g. when the users enters credentials + * + * Supposed to change the state of the client-server session. NOP for + * stateless authentication (e.g. HTTP basic). + * + * `request` must be treated as read-only by the code. `sessionState` is this + * plugin's private storage within the session (if stateful) + * + * If auth doesn't fail, should return an object containing at least + * { success: true }. Should not reject the promise. + */ + authenticate(request, sessionState) { + return new Promise((resolve, reject) => { + const gatewayUrl = this.gatewayUrl; + const data = JSON.stringify({ + username: request.body.username, + password: request.body.password + }); + const options = { + hostname: this.apimlConf.hostname, + port: this.apimlConf.gatewayPort, + path: '/api/v1/apicatalog/auth/login', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + }, + agent: this.httpsAgent + } + + const req = https.request(options, (res) => { + res.on('data', (d) => {}); + res.on('end', () => { + let apimlCookie; + if (res.statusCode == 204) { + if (typeof res.headers['set-cookie'] === 'object') { + for (const cookie of res.headers['set-cookie']) { + const content = cookie.split(';')[0]; + if (content.indexOf('apimlAuthenticationToken') >= 0) { + apimlCookie = content; + } + } + } + } + + if (apimlCookie) { + sessionState.username = request.body.username; + sessionState.apimlCookie = apimlCookie; + sessionState.apimlToken = apimlCookie.split("=")[1]; + resolve({ success: true, username: sessionState.username, expms: DEFAULT_EXPIRATION_MS }); + } else { + if (res.statusCode == 405) { + reason: 'ConnectionError'; + } + let response = { + success: false, + reason: 'Unknown', + error: { + message: `APIML ${res.statusCode} ${res.statusMessage}` + } + }; + //Seems that when auth is first called, it may not be loaded yet, so you get a 405. + if (res.statusCode == 405) { + response.reason = 'TryAgain'; + } + resolve(response); + } + }); + }); + + req.on('error', (error) => { + this.logger.warn("APIML login has failed:"); + this.logger.warn(error); + var details = error.message; + if ((error.response !== undefined) && (error.response.data !== undefined)) { + details = error.response.data; + } + resolve({ + success: false, + reason: 'Unknown', + error: { message: `APIML ${details}`} + }); + }); + + req.write(data); + req.end(); + }); + } + + cleanupSession(sessionState) { + delete sessionState.apimlToken; + delete sessionState.apimlCookie; + } + + /** + * Invoked for every service call by the middleware. + * + * Checks if the session is valid in a stateful scheme, or authenticates the + * request in a stateless scheme. Then checks if the user can access the + * resource. Modifies the request if necessary. + * + * `sessionState` is this plugin's private storage within the session (if + * stateful) + * + * The promise should resolve to an object containing, at least, + * { authorized: true } if everything is fine. Should not reject the promise. + */ + authorized(request, sessionState) { + if (sessionState.authenticated) { + request.username = sessionState.username; + request.ssoToken = sessionState.apimlToken; + return Promise.resolve({ authenticated: true, authorized: true }); + } else { + return Promise.resolve({ authenticated: false, authorized: false }); + } + } + + addProxyAuthorizations(req1, req2Options, sessionState) { + if (!sessionState.apimlCookie) { + return; + } + //apimlToken vs apimlAuthenticationToken ??? + req2Options.headers['apimlToken'] = sessionState.apimlToken; + req2Options.headers['Authorization'] = 'Bearer '+sessionState.apimlToken + } +} + +module.exports = function(pluginDef, pluginConf, serverConf, context) { + return new ApimlHandler(pluginDef, pluginConf, serverConf, context); +} diff --git a/plugins/sso-auth/lib/safprofile.js b/plugins/sso-auth/lib/safprofile.js new file mode 100644 index 00000000..4b7192e6 --- /dev/null +++ b/plugins/sso-auth/lib/safprofile.js @@ -0,0 +1,151 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +const ZOWE_PROFILE_NAME_LEN = 246; +const DEFAULT_INSTANCE_ID = "0"; + +function partsUpToTotalLength(parts, maxLen) { + let curLen = 0; + const outParts = []; + + for (let p of parts) { + curLen += p.length; + if (curLen > maxLen) { + break; + } + curLen++; //account for the separator + outParts.push(p); + } + return outParts; +} + +function rootServiceProfileName(parms){ + if (parms.productCode == null) { + throw new Error("productCode missing"); + } + if (parms.instanceID == null) { + throw new Error("instanceID missing"); + } + if (parms.rootServiceName == null) { + throw new Error("rootServiceName missing"); + } + if (parms.method == null) { + throw new Error("method missing"); + } + return `${parms.productCode}.${parms.instanceID}.COR` + + `.${parms.method}.${parms.rootServiceName}`; +} + +function serviceProfileName(parms) { + if (parms.productCode == null) { + throw new Error("productCode missing"); + } + if (parms.instanceID == null) { + throw new Error("instanceID missing"); + } + if (parms.pluginID == null) { + throw new Error("pluginID missing"); + } + if (parms.serviceName == null) { + throw new Error("serviceName missing"); + } + if (parms.method == null) { + throw new Error("method missing"); + } + return `${parms.productCode}.${parms.instanceID}.SVC.${parms.pluginID}` + + `.${parms.serviceName}.${parms.method}`; +} + +function configProfileName(parms) { + if (parms.productCode == null) { + throw new Error("productCode missing"); + } + if (parms.instanceID == null) { + throw new Error("instanceID missing"); + } + if (parms.pluginID == null) { + throw new Error("pluginID missing"); + } + if (parms.method == null) { + throw new Error("method missing"); + } + if (parms.scope == null) { + throw new Error("scope missing"); + } + return `${parms.productCode}.${parms.instanceID}.CFG.${parms.pluginID}.` + + `${parms.method}.${parms.scope}`; +} + +function makeProfileName(type, parms) { + let makeProfileName; + switch(type){ + case "service": + makeProfileName = serviceProfileName; + break; + case "config": + makeProfileName = configProfileName; + break; + case "core": + makeProfileName = rootServiceProfileName; + break; + } + let profileName = makeProfileName(parms); + if (profileName.length > ZOWE_PROFILE_NAME_LEN) { + throw new Error("SAF resource name too long"); + } + if (parms.subUrl.length > 0) { + const usableParts = partsUpToTotalLength(parms.subUrl, + ZOWE_PROFILE_NAME_LEN - profileName.length - 1); + if (usableParts.length > 0) { + profileName += '.' + usableParts.join('.'); + } + } + return profileName; +} + +function makeProfileNameForRequest(url, method, instanceID) { + let urlData; + let type; + if (!url.match(/^\/[A-Za-z0-9]+\/plugins\//)) { + url = url.toUpperCase(); + type = "core"; + let splitUrl = url.split('/'); + splitUrl = splitUrl.filter(x => x); + let productCode = "ZLUX"; + let rootServiceName = splitUrl[0]; + let subUrl = splitUrl.slice(1); + if (!instanceID) { + instanceID = DEFAULT_INSTANCE_ID; + } + urlData = { productCode, instanceID, rootServiceName, method, subUrl }; + } else { + url = url.toUpperCase(); + let [_l, productCode, _p, pluginID, _s, serviceName, _v, ...subUrl] = url.split('/'); + if (!instanceID) { + instanceID = DEFAULT_INSTANCE_ID; + } + subUrl = subUrl.filter(x => x); + if ((pluginID === "ORG.ZOWE.CONFIGJS") && (serviceName === "DATA")) { + type = "config"; + pluginID = subUrl[0]; + let scope = subUrl[1]; + subUrl = subUrl.slice(2); + urlData = { productCode, instanceID, pluginID, method, scope, subUrl }; + } else { + type = "service"; + urlData = { productCode, instanceID, pluginID, serviceName, method, subUrl }; + } + urlData.pluginID = urlData.pluginID? urlData.pluginID.replace(/\./g, "_") : null; + } + return makeProfileName(type, urlData); +}; + +exports.makeProfileNameForRequest = makeProfileNameForRequest; +exports.ZOWE_PROFILE_NAME_LEN = ZOWE_PROFILE_NAME_LEN; diff --git a/plugins/sso-auth/lib/ssoAuth.js b/plugins/sso-auth/lib/ssoAuth.js new file mode 100644 index 00000000..3faed40a --- /dev/null +++ b/plugins/sso-auth/lib/ssoAuth.js @@ -0,0 +1,237 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +const https = require('https'); +const fs = require('fs'); +const Promise = require('bluebird'); +const ipaddr = require('ipaddr.js'); +const url = require('url'); +const zssHandlerFactory = require('./zssHandler'); +const apimlHandlerFactory = require('./apimlHandler'); + +function doesApimlExist(serverConf) { + return (serverConf.node.mediationLayer !== undefined) + && (serverConf.node.mediationLayer.server !== undefined) + && (serverConf.node.mediationLayer.server.hostname !== undefined) + && (serverConf.node.mediationLayer.gatewayPort !== undefined) +} + +/* + TODO technically not all agents are zss, but currently that is true, + and it's assumed that all agents follow some api standard, + so it is possible our auth logic will work for other agents, as long as they do SAF +*/ +function doesZssExist(serverConf) { + return (serverConf.agent !== undefined) + && (serverConf.agent.host !== undefined) + && (serverConf.agent.http !== undefined) + && (serverConf.agent.http.port !== undefined) +} + + +function cleanupSessionGeneric(sessionState) { + sessionState.authenticated = false; + delete sessionState.username; + delete sessionState.sessionExpTime; +} + +function SsoAuthenticator(pluginDef, pluginConf, serverConf, context) { + this.usingApiml = doesApimlExist(serverConf); + this.usingZss = doesZssExist(serverConf); + //TODO does this automatically mean JWT or does this mean JWT+other things, or is it unrelated? + this.apimlSsoEnabled = process.env['APIML_ENABLE_SSO'] == 'true'; + //Sso here meaning just authenticate to apiml, and handle jwt + this.usingSso = this.apimlSsoEnabled; + + this.pluginConf = pluginConf; + this.instanceID = serverConf.instanceID; + this.authPluginID = pluginDef.identifier; + this.logger = context.logger; + + if (this.usingApiml) { + this.apimlHandler = apimlHandlerFactory(pluginDef, pluginConf, serverConf, context); + } + + if (this.usingZss) { + this.zssHandler = zssHandlerFactory(pluginDef, pluginConf, serverConf, context); + } + + this.capabilities = { + "canGetStatus": true, + //when zosmf cookie becomes invalid, we can purge zss cookie even if it is valid to be consistent + "canRefresh": this.usingApiml ? false : true, + "canAuthenticate": true, + "canAuthorize": true, + "proxyAuthorizations": true, + //TODO do we need to process proxy headers for both? + "processesProxyHeaders": this.usingZss ? true: false + }; +} + +SsoAuthenticator.prototype = { + + getCapabilities(){ + return this.capabilities; + }, + + getStatus(sessionState) { + const expms = sessionState.sessionExpTime - Date.now(); + if (expms <= 0 || sessionState.sessionExpTime === undefined) { + if (this.usingApiml) { + this.apimlHandler.cleanupSession(sessionState); + } + if (this.usingZss) { + this.zssHandler.cleanupSession(sessionState); + } + cleanupSessionGeneric(sessionState); + return { authenticated: false }; + } + return { + authenticated: !!sessionState.authenticated, + username: sessionState.username, + expms: sessionState.sessionExpTime ? expms : undefined + }; + }, + + _insertHandlerStatus(response) { + response.apiml = this.usingApiml; + response.zss = this.usingZss; + response.sso = this.usingSso; + return response; + }, + + /* + When JWT SSO is present, auth only to apiml to reduce latency and point of failure + When not present, OK to auth to both, but must return messages about partial failure if present + */ + authenticate(request, sessionState) { + return new Promise((resolve, reject)=> { + if (this.usingSso || !this.usingZss) { + //case 1: apiml present and with sso that zss can understand, if present too + //case 2: zss not present, therefore apiml must be + this.apimlHandler.authenticate(request, sessionState).then((apimlResult)=> { + if (apimlResult.success) { + sessionState.sessionExpTime = Date.now() + apimlResult.expms; + } else { + this.apimlHandler.cleanupSession(sessionState); + cleanupSessionGeneric(sessionState); + } + resolve(this._insertHandlerStatus(apimlResult)); + }).catch((e)=> { + this.apimlHandler.cleanupSession(sessionState); + cleanupSessionGeneric(sessionState); + reject(e); + }); + } else if (this.usingZss) { + //case 3: zss present, and maybe apiml also + this.zssHandler.authenticate(request, sessionState).then((zssResult)=> { + if (this.usingApiml) { + this.apimlHandler.authenticate(request, sessionState).then((apimlResult)=> { + resolve(this._mergeAuthenticate(zssResult, apimlResult, sessionState)); + }).catch((e)=> { + this.apimlHandler.cleanupSession(sessionState); + this.zssHandler.cleanupSession(sessionState); + cleanupSessionGeneric(sessionState); + reject(e); + }); + } else { + console.log('cookies returned as=',sessionState.zssCookies); + resolve(this._insertHandlerStatus(zssResult)); + } + }).catch((e)=> { + this.zssHandler.cleanupSession(sessionState); + cleanupSessionGeneric(sessionState); + reject(e); + }); + } + }); + }, + + _mergeAuthenticate(zss, apiml, sessionState) { + const now = Date.now(); + //mixed success = failure, complete success = figure out expiration + if (!apiml.success || !zss.success) { + this.apimlHandler.cleanupSession(sessionState); + this.zssHandler.cleanupSession(sessionState); + cleanupSessionGeneric(sessionState); + return this._insertHandlerStatus(!apiml.success ? apiml : zss); + } else { + sessionState.authenticated = true; + sessionState.sessionExpTime = sessionState.sessionExpTime + ? Math.min(sessionState.sessionExpTime, now+zss.expms, now+apiml.expms) + : Math.min(now+zss.expms, now+apiml.expms); + return this._insertHandlerStatus({ + success: true, + username: sessionState.username, + expms: sessionState.sessionExpTime + }); + } + }, + + refreshStatus(request, sessionState) { + console.log('refresh enters with cookies=',sessionState.zssCookies); + return new Promise((resolve, reject) => { + if (this.usingZss) { + this.zssHandler.refreshStatus(request, sessionState).then((result)=> { + const now = Date.now(); + if (result.success) { + if (this.usingApiml) { + sessionState.sessionExpTime = sessionState.sessionExpTime + ? Math.min(sessionState.sessionExpTime, now+result.expms) + : now+result.expms; + } else { + sessionState.sessionExpTime = now+result.expms; + } + + } + /* if failure, dont un-auth or delete cookie... perhaps this was a network error. + Let session expire naturally if no success + */ + resolve(this._insertHandlerStatus(result)); + }).catch((e)=> { + this.logger.warn(e); + return this._insertHandlerStatus({success:false}); + }); + } else { + resolve(this._insertHandlerStatus({success: false})); + } + }); + }, + + authorized(request, sessionState) { + //prefer ZSS here because it can do RBAC the way the app fw expects + if (!this.usingZss) { + return this.apimlHandler.authorized(request, sessionState); + } else { + return this.zssHandler.authorized(request, sessionState); + } + }, + + addProxyAuthorizations(req1, req2Options, sessionState) { + if (this.usingApiml) { + this.apimlHandler.addProxyAuthorizations(req1, req2Options, sessionState); + } + if (this.usingZss) { + this.zssHandler.addProxyAuthorizations(req1, req2Options, sessionState); + } + }, + + processProxiedHeaders(req, headers, sessionState) { + if (this.usingZss) { + headers = this.zssHandler.processProxiedHeaders(req, headers, sessionState); + } + //TODO does apiml need this too? + return headers; + } +}; + +module.exports = function (pluginDef, pluginConf, serverConf, context) { + return Promise.resolve(new SsoAuthenticator(pluginDef, pluginConf, serverConf, context)); +} diff --git a/plugins/sso-auth/lib/tokenInjector.js b/plugins/sso-auth/lib/tokenInjector.js new file mode 100644 index 00000000..197b6209 --- /dev/null +++ b/plugins/sso-auth/lib/tokenInjector.js @@ -0,0 +1,23 @@ +const express = require('express'); + +module.exports = pluginContext => { + const r = express.Router(); + r.get('/**', (req, res) => { + const apimlSession = req.session.authPlugins['org.zowe.zlux.auth.apiml']; + if (apimlSession === undefined) { + res.status(401).send("Missing APIML authentication token in zLUX session"); + } + else { + const token = apimlSession.apimlToken; + const gatewayUrl = apimlSession.gatewayUrl; + const newUrl = gatewayUrl + req.url.replace("1.0.0/", "") + "?apimlAuthenticationToken=" + token; + res.redirect(newUrl); + } + }) + + return { + then(f) { + f(r); + } + } +}; diff --git a/plugins/sso-auth/lib/zssHandler.js b/plugins/sso-auth/lib/zssHandler.js new file mode 100644 index 00000000..e2dd55b1 --- /dev/null +++ b/plugins/sso-auth/lib/zssHandler.js @@ -0,0 +1,249 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +const Promise = require('bluebird'); +const ipaddr = require('ipaddr.js'); +const url = require('url'); +const makeProfileNameForRequest = require('./safprofile').makeProfileNameForRequest; +const DEFAULT_CLASS = "ZOWE"; +const DEFAULT_EXPIRATION_MS = 3600000 //hour; + +class ZssHandler { + constructor(pluginDef, pluginConf, serverConf, context) { + this.log = context.logger; + this.sessionExpirationMS = DEFAULT_EXPIRATION_MS; //ahead of time assumption of unconfigurable zss session length + this.authorized = Promise.coroutine(function *authorized(request, sessionState, + options) { + const result = { authenticated: false, authorized: false }; + options = options || {}; + try { + const { syncOnly } = options; + let bypassUrls = [ + '/login', + '/unixfile', + '/datasetContents', + '/VSAMdatasetContents', + '/datasetMetadata', + '/omvs', + '/security-mgmt' + ] + for(let i = 0; i < bypassUrls.length; i++){ + if(request.originalUrl.startsWith(bypassUrls[i])){ + result.authorized = true; + return result; + } + } + if (!sessionState.authenticated) { + return result; + } + result.authenticated = true; + request.username = sessionState.username; + if (options.bypassAuthorizatonCheck) { + result.authorized = true; + return result; + } + if (request.originalUrl.startsWith("/saf-auth")) { + //The '/saf-auth' service must not be available to external callers. + //Note that this potentially allows someone running the browser on + //the same host to still access the service. However: + // 1. That shouldn't be allowed + // 2. They can run the request agains the ZSS host itself. The firewall + // would allow that. So, simply go back to item 1 + this._allowIfLoopback(request, result); + return result; + } + const resourceName = this._makeProfileName(request.originalUrl, + request.method); + if (syncOnly) { + // can't do anything further: the user is authenticated but we can't + // make an actual RBAC check + this.log.info(`Can't make a call to the OS agent for access check. ` + + `Allowing ${sessionState.username} access to ${resourceName} ` + + 'unconditinally'); + result.authorized = true; + return result; + } + const httpResponse = yield this._callAgent(request.zluxData, + sessionState.username, resourceName); + this._processAgentResponse(httpResponse, result, sessionState.username); + //console.log("returning result", result) + return result; + } catch (e) { + this.log.warn(`User ${sessionState.username}, ` + + `authorization problem: ${e.message}`, e); + result.authorized = false; + result.message = "Problem checking auth permissions"; + return result; + } + }) + + } + + /** + * Should be called e.g. when the users enters credentials + * + * Supposed to change the state of the client-server session. NOP for + * stateless authentication (e.g. HTTP basic). + * + * `request` must be treated as read-only by the code. `sessionState` is this + * plugin's private storage within the session (if stateful) + * + * If auth doesn't fail, should return an object containing at least + * { success: true }. Should not reject the promise. + */ + authenticate(request, sessionState) { + return this._authenticateOrRefresh(request, sessionState, false).catch ((e)=> { + this.log.warn(e); + return { success: false }; + }); + } + + cleanupSession(sessionState) { + delete sessionState.zssCookies; + } + + refreshStatus(request, sessionState) { + return this._authenticateOrRefresh(request, sessionState, true).catch ((e)=> { + this.log.warn(e); + //dont un-auth or delete cookie... perhaps this was a network error. Let session expire naturally if no success + return { success: false }; + }); + } + + _authenticateOrRefresh(request, sessionState, isRefresh) { + return new Promise((resolve, reject) => { + if (isRefresh && !sessionState.zssCookies) { + reject(new Error('No cookie given for refresh or check, skipping zss request')); + return; + } + let options = isRefresh ? { + method: 'GET', + headers: {'cookie': sessionState.zssCookies} + } : { + method: 'POST', + body: request.body + }; + request.zluxData.webApp.callRootService("login", options).then((response) => { + let zssCookie; + if (typeof response.headers['set-cookie'] === 'object') { + for (const cookie of response.headers['set-cookie']) { + const content = cookie.split(';')[0]; + //TODO proper manage cookie expiration + if (content.indexOf('jedHTTPSession') >= 0) { + zssCookie = content; + } + } + } + if (zssCookie) { + if (!isRefresh) { + sessionState.username = request.body.username.toUpperCase(); + } + //intended to be known as result of network call + sessionState.zssCookies = zssCookie; + console.log('cookies set to=',sessionState.zssCookies); + resolve({ success: true, username: sessionState.username, expms: DEFAULT_EXPIRATION_MS }) + } else { + let res = { success: false, error: {message: `ZSS ${response.statusCode} ${response.statusMessage}`}}; + if (response.statusCode === 500) { + res.reason = 'ConnectionError'; + } else { + res.reason = 'Unknown'; + } + resolve(res); + } + }).catch((e) => { + reject(e); + }); + }); + } + + addProxyAuthorizations(req1, req2Options, sessionState) { + if (req1.cookies) { + delete req1.cookies['jedHTTPSession']; + } + if (!sessionState.zssCookies) { + return; + } + req2Options.headers['cookie'] = sessionState.zssCookies; + } + + processProxiedHeaders(req, headers, sessionState) { + let cookies = headers['set-cookie']; + if (cookies) { + let modifiedCookies = []; + for (let i = 0; i < cookies.length; i++) { + if (cookies[i].startsWith('jedHTTPSession')) { + let zssCookie = cookies[i]; + let semiIndex = zssCookie.indexOf(';'); + sessionState.zssCookies = semiIndex != -1 ? zssCookie.substring(0,semiIndex) : zssCookie; + + } else { + modifiedCookies.push(cookies[i]); + } + } + headers['set-cookie']=modifiedCookies; + } + return headers; + } + + _allowIfLoopback(request, result) { + const requestIP = ipaddr.process(request.ip); + if (requestIP.range() == "loopback") { + result.authorized = true; + } else { + this.log.warn(`Access to /saf-auth blocked, caller: ${request.ip}`) + result.authorized = false; + } + } + + _makeProfileName(reqUrl, method) { + //console.log("request.originalUrl", request.originalUrl) + const path = url.parse(reqUrl).pathname; + //console.log("originalPath", originalPath) + const resourceName = makeProfileNameForRequest(path, method, this.instanceID); + //console.log("resourceName", resourceName) + return resourceName; + } + + _callAgent(zluxData, userName, resourceName) { + //console.log("resourceName", resourceName) + userName = encodeURIComponent(userName); + resourceName = encodeURIComponent(resourceName); + const path = `${resourceName}/READ`; + //console.log('trying path ', path); + //console.log(new Error("stack trace before calling root serivce")) + return zluxData.webApp.callRootService("saf-auth", path); + } + + _processAgentResponse(httpResponse, result, username) { + if (!(200 <= httpResponse.statusCode && httpResponse.statusCode < 299)) { + result.authorized = false; + result.message = httpResponse.body; + } else { + //console.log("httpResponse.body", httpResponse.body) + const responseBody = JSON.parse(httpResponse.body); + if (responseBody.authorized === true) { + result.authorized = true; + } else if (responseBody.authorized === false) { + result.authorized = false; + result.message = responseBody.message; + } else { + result.authorized = false; + result.message = "Problem checking access permissions"; + this.log.warn(`User ${username}, ` + + `authorization problem: ${responseBody.message}`); + } + } + } +} + +module.exports = function(pluginDef, pluginConf, serverConf, context) { + return new ZssHandler(pluginDef, pluginConf, serverConf, context); +} diff --git a/plugins/sso-auth/pluginDefinition.json b/plugins/sso-auth/pluginDefinition.json new file mode 100644 index 00000000..4cf0d9f5 --- /dev/null +++ b/plugins/sso-auth/pluginDefinition.json @@ -0,0 +1,18 @@ +{ + "identifier": "org.zowe.zlux.auth.safsso", + "pluginType": "nodeAuthentication", + "authenticationCategory": "saf", + "apiVersion": "1.0.0", + "pluginVersion": "1.0.0", + "license": "EPL-2.0", + "filename": "ssoAuth.js", + "dataServices": [ + { + "type": "router", + "name": "tokenInjector", + "fileName": "tokenInjector.js", + "version": "1.0.0", + "dependenciesIncluded": true + } + ] +} From 005ab25e8bb317610aa8057498d61d282393d501 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Mon, 27 Jan 2020 18:05:57 -0500 Subject: [PATCH 2/4] Bugfixes for when to zss versus when to apiml and expiration fix Signed-off-by: 1000TurquoisePogs --- plugins/sso-auth/lib/apimlHandler.js | 6 ++++-- plugins/sso-auth/lib/ssoAuth.js | 28 ++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/plugins/sso-auth/lib/apimlHandler.js b/plugins/sso-auth/lib/apimlHandler.js index 3b6bcf23..15155b62 100644 --- a/plugins/sso-auth/lib/apimlHandler.js +++ b/plugins/sso-auth/lib/apimlHandler.js @@ -186,13 +186,15 @@ class ApimlHandler { } } - addProxyAuthorizations(req1, req2Options, sessionState) { + addProxyAuthorizations(req1, req2Options, sessionState, usingSso) { if (!sessionState.apimlCookie) { return; } //apimlToken vs apimlAuthenticationToken ??? req2Options.headers['apimlToken'] = sessionState.apimlToken; - req2Options.headers['Authorization'] = 'Bearer '+sessionState.apimlToken + if (this.usingSso) { + req2Options.headers['Authorization'] = 'Bearer '+sessionState.apimlToken; + } } } diff --git a/plugins/sso-auth/lib/ssoAuth.js b/plugins/sso-auth/lib/ssoAuth.js index 3faed40a..c3a00653 100644 --- a/plugins/sso-auth/lib/ssoAuth.js +++ b/plugins/sso-auth/lib/ssoAuth.js @@ -20,7 +20,7 @@ function doesApimlExist(serverConf) { return (serverConf.node.mediationLayer !== undefined) && (serverConf.node.mediationLayer.server !== undefined) && (serverConf.node.mediationLayer.server.hostname !== undefined) - && (serverConf.node.mediationLayer.gatewayPort !== undefined) + && (serverConf.node.mediationLayer.server.gatewayPort !== undefined) } /* @@ -48,7 +48,7 @@ function SsoAuthenticator(pluginDef, pluginConf, serverConf, context) { //TODO does this automatically mean JWT or does this mean JWT+other things, or is it unrelated? this.apimlSsoEnabled = process.env['APIML_ENABLE_SSO'] == 'true'; //Sso here meaning just authenticate to apiml, and handle jwt - this.usingSso = this.apimlSsoEnabled; + this.usingSso = this.apimlSsoEnabled && this.usingApiml; this.pluginConf = pluginConf; this.instanceID = serverConf.instanceID; @@ -123,6 +123,7 @@ SsoAuthenticator.prototype = { this.apimlHandler.cleanupSession(sessionState); cleanupSessionGeneric(sessionState); } + sessionState.authenticated = apimlResult.success; resolve(this._insertHandlerStatus(apimlResult)); }).catch((e)=> { this.apimlHandler.cleanupSession(sessionState); @@ -142,7 +143,10 @@ SsoAuthenticator.prototype = { reject(e); }); } else { - console.log('cookies returned as=',sessionState.zssCookies); + if (zssResult.success) { + sessionState.sessionExpTime = Date.now() + zssResult.expms; + sessionState.authenticated = true; + } resolve(this._insertHandlerStatus(zssResult)); } }).catch((e)=> { @@ -164,19 +168,19 @@ SsoAuthenticator.prototype = { return this._insertHandlerStatus(!apiml.success ? apiml : zss); } else { sessionState.authenticated = true; + let shortestExpms = Math.min(zss.expms, apiml.expms); sessionState.sessionExpTime = sessionState.sessionExpTime - ? Math.min(sessionState.sessionExpTime, now+zss.expms, now+apiml.expms) - : Math.min(now+zss.expms, now+apiml.expms); + ? Math.min(sessionState.sessionExpTime, now+shortestExpms) + : now+shortestExpms; return this._insertHandlerStatus({ success: true, username: sessionState.username, - expms: sessionState.sessionExpTime + expms: shortestExpms }); } }, refreshStatus(request, sessionState) { - console.log('refresh enters with cookies=',sessionState.zssCookies); return new Promise((resolve, reject) => { if (this.usingZss) { this.zssHandler.refreshStatus(request, sessionState).then((result)=> { @@ -205,20 +209,20 @@ SsoAuthenticator.prototype = { }); }, - authorized(request, sessionState) { + authorized(request, sessionState, options) { //prefer ZSS here because it can do RBAC the way the app fw expects if (!this.usingZss) { - return this.apimlHandler.authorized(request, sessionState); + return this.apimlHandler.authorized(request, sessionState, options); } else { - return this.zssHandler.authorized(request, sessionState); + return this.zssHandler.authorized(request, sessionState, options); } }, addProxyAuthorizations(req1, req2Options, sessionState) { if (this.usingApiml) { - this.apimlHandler.addProxyAuthorizations(req1, req2Options, sessionState); + this.apimlHandler.addProxyAuthorizations(req1, req2Options, sessionState, this.usingSso); } - if (this.usingZss) { + if (this.usingZss && !this.usingSso) { this.zssHandler.addProxyAuthorizations(req1, req2Options, sessionState); } }, From b55e5880a9fbd19008fc206cb5fd0e645d943fa2 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Fri, 31 Jan 2020 10:43:09 -0500 Subject: [PATCH 3/4] Update conditionals for apiml, remove duplicate reason Signed-off-by: 1000TurquoisePogs --- plugins/sso-auth/lib/apimlHandler.js | 3 --- plugins/sso-auth/lib/ssoAuth.js | 6 ++++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/sso-auth/lib/apimlHandler.js b/plugins/sso-auth/lib/apimlHandler.js index 15155b62..7d0322a2 100644 --- a/plugins/sso-auth/lib/apimlHandler.js +++ b/plugins/sso-auth/lib/apimlHandler.js @@ -120,9 +120,6 @@ class ApimlHandler { sessionState.apimlToken = apimlCookie.split("=")[1]; resolve({ success: true, username: sessionState.username, expms: DEFAULT_EXPIRATION_MS }); } else { - if (res.statusCode == 405) { - reason: 'ConnectionError'; - } let response = { success: false, reason: 'Unknown', diff --git a/plugins/sso-auth/lib/ssoAuth.js b/plugins/sso-auth/lib/ssoAuth.js index c3a00653..a556fe58 100644 --- a/plugins/sso-auth/lib/ssoAuth.js +++ b/plugins/sso-auth/lib/ssoAuth.js @@ -17,7 +17,9 @@ const zssHandlerFactory = require('./zssHandler'); const apimlHandlerFactory = require('./apimlHandler'); function doesApimlExist(serverConf) { - return (serverConf.node.mediationLayer !== undefined) + return (process.env['LAUNCH_COMPONENT_GROUPS'] !== undefined) + && (process.env['LAUNCH_COMPONENT_GROUPS'].indexOf('GATEWAY') != -1) + && (serverConf.node.mediationLayer !== undefined) && (serverConf.node.mediationLayer.server !== undefined) && (serverConf.node.mediationLayer.server.hostname !== undefined) && (serverConf.node.mediationLayer.server.gatewayPort !== undefined) @@ -45,7 +47,7 @@ function cleanupSessionGeneric(sessionState) { function SsoAuthenticator(pluginDef, pluginConf, serverConf, context) { this.usingApiml = doesApimlExist(serverConf); this.usingZss = doesZssExist(serverConf); - //TODO does this automatically mean JWT or does this mean JWT+other things, or is it unrelated? + //TODO this seems temporary, will need to unconditionally say usingApiml=usingSso when sso support is complete this.apimlSsoEnabled = process.env['APIML_ENABLE_SSO'] == 'true'; //Sso here meaning just authenticate to apiml, and handle jwt this.usingSso = this.apimlSsoEnabled && this.usingApiml; From 85b4f70f3be6b430da4fba57c8ce05521930a89c Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Mon, 3 Feb 2020 14:06:57 -0500 Subject: [PATCH 4/4] Array check before using array map function Signed-off-by: 1000TurquoisePogs --- lib/webauth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webauth.js b/lib/webauth.js index a82fb9b1..f20d564b 100644 --- a/lib/webauth.js +++ b/lib/webauth.js @@ -63,7 +63,7 @@ function setAuthPluginSession(req, pluginID, authPluginSession) { function getRelevantHandlers(authManager, body) { let handlers = authManager.getAllHandlers(); - if (body && body.categories) { + if (body && Array.isArray(body.categories)) { const authCategories = {}; body.categories.map(t => authCategories[t] = true); handlers = handlers.filter(h =>