diff --git a/lib/auth/types/AuthType.js b/lib/auth/types/AuthType.js index 912a4cdf4b80898bd4b9c2929cd321b5717aa4fb..6fdc8bc52f403e46b41f9c001ed01f5def39061f 100644 --- a/lib/auth/types/AuthType.js +++ b/lib/auth/types/AuthType.js @@ -18,6 +18,7 @@ import { assign } from 'lodash'; import Boom from 'boom'; import InvalidSessionError from "../errors/invalid_session_error"; import SessionExpiredError from "../errors/session_expired_error"; +import filterAuthHeaders from '../filter_auth_headers'; export default class AuthType { @@ -56,6 +57,13 @@ export default class AuthType { */ this.authHeaderName = 'authorization'; + /** + * Additional headers that should be passed as part as the authentication. + * Do not use headers here that have an effect on which user is logged in. + * @type {string[]} + */ + this.allowedAdditionalAuthHeaders = ['sg_impersonate_as']; + /** * This is a workaround for keeping track of what caused hapi-auth-cookie's validateFunc to fail. * There seems to be an issue with how the plugin checks the thrown error and instead of passing @@ -81,6 +89,7 @@ export default class AuthType { options: { authType: this.type, authHeaderName: this.authHeaderName, + allowedAdditionalAuthHeaders: this.allowedAdditionalAuthHeaders, authenticateFunction: this.authenticate.bind(this), validateAvailableTenants: this.validateAvailableTenants } @@ -149,7 +158,7 @@ export default class AuthType { return null; } - authenticate(credentials) { + authenticate(credentials, options = {}, additionalAuthHeaders = {}) { throw new Error('The authenticate method must be implemented by the sub class'); } @@ -269,6 +278,12 @@ export default class AuthType { } } + // Make sure we don't have any conflicting auth headers + if (! this.validateAdditionalAuthHeaders(request, session)) { + request.auth.sgSessionStorage.clearStorage(); + return {valid: false}; + } + // If we are still here, we need to compare the expiration time // JWT's .exp is denoted in seconds, not milliseconds. if (session.exp && session.exp < Math.floor(Date.now() / 1000)) { @@ -300,6 +315,36 @@ export default class AuthType { return validate; } + /** + * Validates + * @param request + * @param session + * @returns {boolean} + */ + validateAdditionalAuthHeaders(request, session) { + + // Check if the request has any of the headers that can be used on authentication + const authHeadersInRequest = filterAuthHeaders(request.headers, this.allowedAdditionalAuthHeaders); + + if (Object.keys(authHeadersInRequest).length === 0) { + return true; + } + + // If we have applicable headers in the request, but not in the session, the validation fails + if (! session.additionalAuthHeaders) { + return false; + } + + // If the request has a conflicting auth header we log out the user + for (const header in session.additionalAuthHeaders) { + if (session.additionalAuthHeaders[header] !== authHeadersInRequest[header]) { + return false; + } + } + + return true; + } + /** * Add credential headers to the passed request. * @param request @@ -329,7 +374,7 @@ export default class AuthType { try { let authHeader = this.getAuthHeader(session); if (authHeader !== false) { - this.addAdditionalAuthHeaders(request, authHeader); + this.addAdditionalAuthHeaders(request, authHeader, session); assign(request.headers, authHeader); } } catch (error) { @@ -361,10 +406,18 @@ export default class AuthType { /** * Method for adding additional auth type specific authentication headers. * Override this in the auth type for type specific headers. + * + * NB: Remember to call the super method if you do. + * * @param request * @param authHeader + * @param session */ - addAdditionalAuthHeaders(request, authHeader) { - + addAdditionalAuthHeaders(request, authHeader, session) { + if (session.additionalAuthHeaders) { + for (let header in session.additionalAuthHeaders) { + authHeader[header] = session.additionalAuthHeaders[header]; + } + } } } \ No newline at end of file diff --git a/lib/auth/types/basicauth/BasicAuth.js b/lib/auth/types/basicauth/BasicAuth.js index 3ddb9160871ec6e5ec56b27a1b7fbbf1839b77ce..98693e586b1c632c4b82ca062d0838f661afd731 100644 --- a/lib/auth/types/basicauth/BasicAuth.js +++ b/lib/auth/types/basicauth/BasicAuth.js @@ -39,7 +39,7 @@ export default class BasicAuth extends AuthType { * @type {boolean} */ this.loadBalancerURL = this.config.get('searchguard.basicauth.loadbalancer_url'); - + /** * Allow anonymous access? * @type {boolean} @@ -84,12 +84,12 @@ export default class BasicAuth extends AuthType { return null; } - async authenticate(credentials, options = {}) { + async authenticate(credentials, options = {}, additionalAuthHeaders = {}) { // A login can happen via a POST request (login form) or when we have request headers with user credentials. // We also need to re-authenticate if the credentials (headers) don't match what's in the session. try { - let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue); + let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue, additionalAuthHeaders); let session = { username: user.username, credentials: credentials, diff --git a/lib/auth/types/jwt/Jwt.js b/lib/auth/types/jwt/Jwt.js index 69b2987cb33dc71e03bec13fa1021a26b0938702..6b4ed44014a6a73b252cdc110394dd99eb46094c 100644 --- a/lib/auth/types/jwt/Jwt.js +++ b/lib/auth/types/jwt/Jwt.js @@ -88,11 +88,11 @@ export default class Jwt extends AuthType { return authHeaderValue; } - async authenticate(credentials) { + async authenticate(credentials, options = {}, additionalAuthHeaders = {}) { // A "login" can happen when we have a token (as header or as URL parameter but no session, // or when we have an existing session, but the passed token does not match what's in the session. try { - let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue); + let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue, additionalAuthHeaders); let tokenPayload = {}; try { tokenPayload = JSON.parse(Buffer.from(credentials.authHeaderValue.split('.')[1], 'base64').toString()); diff --git a/lib/auth/types/openid/OpenId.js b/lib/auth/types/openid/OpenId.js index bd5aab947da16e32e8238fc8c28f1e459c1811ed..9dad981885cbf1a47bc49e599860a61af4d4fe76 100644 --- a/lib/auth/types/openid/OpenId.js +++ b/lib/auth/types/openid/OpenId.js @@ -58,12 +58,12 @@ export default class OpenId extends AuthType { } } - async authenticate(credentials) { + async authenticate(credentials, options = {}, additionalAuthHeaders = {}) { // A "login" can happen when we have a token (as header or as URL parameter but no session, // or when we have an existing session, but the passed token does not match what's in the session. try { - let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue); + let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue, additionalAuthHeaders); let tokenPayload = {}; try { tokenPayload = JSON.parse(Buffer.from(credentials.authHeaderValue.split('.')[1], 'base64').toString()); diff --git a/lib/auth/types/proxycache/ProxyCache.js b/lib/auth/types/proxycache/ProxyCache.js index de90e777b62d51cc9e67bfc9e57bffa54b6117dc..24c1a00b87a76bea6dc75182f12235715a974d0c 100644 --- a/lib/auth/types/proxycache/ProxyCache.js +++ b/lib/auth/types/proxycache/ProxyCache.js @@ -105,9 +105,9 @@ export default class ProxyCache extends AuthType { return false; } - async authenticate(credentialHeaders) { + async authenticate(credentialHeaders, options = {}, additionalAuthHeaders = {}) { try { - let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeaders(credentialHeaders, credentialHeaders); + let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeaders(credentialHeaders, credentialHeaders, additionalAuthHeaders); let session = { username: user.username, @@ -158,7 +158,9 @@ export default class ProxyCache extends AuthType { require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT); } - addAdditionalAuthHeaders(request, authHeader) { + addAdditionalAuthHeaders(request, authHeader, session) { + super.addAdditionalAuthHeaders(request, authHeader, session); + // for proxy cache mode, make it possible to assign the proxy ip, // usually as x-forwarded-for header. Only if no headers are already present let existingProxyHeaders = request.headers[this.config.get('searchguard.proxycache.proxy_header')]; diff --git a/lib/auth/types/saml/Saml.js b/lib/auth/types/saml/Saml.js index 62db8a589714caeb4bb3484a4dd2af5b53003a7b..5eb08c6aa8e0fa482c8abcd91e7b807f1f25a224 100644 --- a/lib/auth/types/saml/Saml.js +++ b/lib/auth/types/saml/Saml.js @@ -32,12 +32,12 @@ export default class Saml extends AuthType { this.type = 'saml'; } - async authenticate(credentials) { + async authenticate(credentials, options = {}, additionalAuthHeaders = {}) { // A "login" can happen when we have a token (as header or as URL parameter but no session, // or when we have an existing session, but the passed token does not match what's in the session. try { - let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue); + let user = await this.server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue, additionalAuthHeaders); let tokenPayload = {}; try { diff --git a/lib/backend/searchguard.js b/lib/backend/searchguard.js index 13cf38aaccd82486fb0227b29a824c50cfd3012f..269f5543d673eb88c77fc1e25ff16a08e87393dd 100644 --- a/lib/backend/searchguard.js +++ b/lib/backend/searchguard.js @@ -57,7 +57,7 @@ export default class SearchGuardBackend { } } - async authenticateWithHeader(headerName, headerValue) { + async authenticateWithHeader(headerName, headerValue, additionalAuthHeaders = {}) { try { const credentials = { @@ -65,7 +65,7 @@ export default class SearchGuardBackend { headerValue: headerValue }; - let headers = {}; + let headers = filterAuthHeaders(additionalAuthHeaders, this._esconfig.requestHeadersWhitelist); // For anonymous auth, we wouldn't have any value here if (headerValue) { @@ -93,7 +93,11 @@ export default class SearchGuardBackend { * @param credentials * @returns {Promise<User>} */ - async authenticateWithHeaders(headers, credentials = {}) { + async authenticateWithHeaders(headers, credentials = {}, additionalAuthHeaders = {}) { + headers = { + ...filterAuthHeaders(additionalAuthHeaders, this._esconfig.requestHeadersWhitelist), + ...headers, + }; try { const response = await this._client.searchguard.authinfo({ diff --git a/lib/session/sessionPlugin.js b/lib/session/sessionPlugin.js index 46b5a9c57ba686bd119e6da2d395ebcbb0231415..f38f994909313a6de0bf46c032babaa89e8f7eaa 100755 --- a/lib/session/sessionPlugin.js +++ b/lib/session/sessionPlugin.js @@ -1,8 +1,9 @@ import MissingTenantError from "../auth/errors/missing_tenant_error"; import MissingRoleError from "../auth/errors/missing_role_error"; +import filterAuthHeaders from '../auth/filter_auth_headers'; -var Hoek = require('hoek'); -var Joi = require('joi'); +const Hoek = require('hoek'); +const Joi = require('joi'); /** * Name of the cookie where we store additional session information, such as authInfo @@ -15,6 +16,7 @@ let internals = {}; internals.config = Joi.object({ authType: Joi.string().allow(null), authHeaderName: Joi.string(), + allowedAdditionalAuthHeaders: Joi.array().default([]), authenticateFunction: Joi.func(), validateAvailableTenants: Joi.boolean().default(true), validateAvailableRoles: Joi.boolean().default(true) @@ -38,10 +40,11 @@ const register = function (server, options) { */ authenticate: async function (credentials, options = {}) { try { + const additionalAuthHeaders = filterAuthHeaders(request.headers, settings.allowedAdditionalAuthHeaders); // authResponse is an object with .session and .user - const authResponse = await settings.authenticateFunction(credentials, options); + const authResponse = await settings.authenticateFunction(credentials, options, additionalAuthHeaders); - return this._handleAuthResponse(credentials, authResponse); + return this._handleAuthResponse(credentials, authResponse, additionalAuthHeaders); } catch (error) { // Make sure we clear any existing cookies if something went wrong this.clear(); @@ -50,10 +53,10 @@ const register = function (server, options) { }, - authenticateWithHeaders: async function (headers, credentials = {}, options = {}) { try { - let user = await server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeaders(headers); + const additionalAuthHeaders = filterAuthHeaders(request.headers, settings.allowedAdditionalAuthHeaders); + let user = await server.plugins.searchguard.getSearchGuardBackend().authenticateWithHeaders(headers, credentials, additionalAuthHeaders); let session = { username: user.username, credentials: credentials, @@ -75,7 +78,7 @@ const register = function (server, options) { user }; - return this._handleAuthResponse(credentials, authResponse) + return this._handleAuthResponse(credentials, authResponse, additionalAuthHeaders) } catch (error) { // Make sure we clear any existing cookies if something went wrong this.clear(); @@ -90,7 +93,7 @@ const register = function (server, options) { * @returns {*} * @private */ - _handleAuthResponse: function (credentials, authResponse) { + _handleAuthResponse: function (credentials, authResponse, additionalAuthHeaders = {}) { // Make sure the user has a tenant that they can use if (settings.validateAvailableTenants && server.config().get("searchguard.multitenancy.enabled") && !server.config().get("searchguard.multitenancy.tenants.enable_global")) { let privateTenantEnabled = server.config().get("searchguard.multitenancy.tenants.enable_private"); @@ -110,6 +113,12 @@ const register = function (server, options) { throw new MissingRoleError('No roles available for this user, please contact your system administrator.'); } + // If we used any additional auth headers when authenticating, we need to store them in the session + authResponse.session.additionalAuthHeaders = null; + if (Object.keys(additionalAuthHeaders).length) { + authResponse.session.additionalAuthHeaders = additionalAuthHeaders; + } + request.cookieAuth.set(authResponse.session); this.setAuthInfo(authResponse.user.username, authResponse.user.backendroles, authResponse.user.roles, authResponse.user.tenants, authResponse.user.selectedTenant); @@ -327,4 +336,4 @@ const register = function (server, options) { exports.plugin = { name: 'sg-session-storage', register -}; \ No newline at end of file +};