mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
309 lines
12 KiB
JavaScript
309 lines
12 KiB
JavaScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
import { getClient } from '../../../../../server/lib/get_client_shield';
|
|
import { AuthScopeService } from '../auth_scope_service';
|
|
import { getErrorStatusCode } from '../errors';
|
|
import { BasicAuthenticationProvider } from './providers/basic';
|
|
import { SAMLAuthenticationProvider } from './providers/saml';
|
|
import { TokenAuthenticationProvider } from './providers/token';
|
|
import { AuthenticationResult } from './authentication_result';
|
|
import { DeauthenticationResult } from './deauthentication_result';
|
|
import { Session } from './session';
|
|
import { LoginAttempt } from './login_attempt';
|
|
|
|
// Mapping between provider key defined in the config and authentication
|
|
// provider class that can handle specific authentication mechanism.
|
|
const providerMap = new Map([
|
|
['basic', BasicAuthenticationProvider],
|
|
['saml', SAMLAuthenticationProvider],
|
|
['token', TokenAuthenticationProvider],
|
|
]);
|
|
|
|
function assertRequest(request) {
|
|
if (!request || typeof request !== 'object') {
|
|
throw new Error(`Request should be a valid object, was [${typeof request}].`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares options object that is shared among all authentication providers.
|
|
* @param {Hapi.Server} server HapiJS Server instance.
|
|
* @returns {Object}
|
|
*/
|
|
function getProviderOptions(server) {
|
|
const config = server.config();
|
|
|
|
return {
|
|
client: getClient(server),
|
|
log: server.log.bind(server),
|
|
|
|
protocol: server.info.protocol,
|
|
hostname: config.get('server.host'),
|
|
port: config.get('server.port'),
|
|
basePath: config.get('server.basePath'),
|
|
|
|
...config.get('xpack.security.public')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Authenticator is responsible for authentication of the request using chain of
|
|
* authentication providers. The chain is essentially a prioritized list of configured
|
|
* providers (typically of various types). The order of the list determines the order in
|
|
* which the providers will be consulted. During the authentication process, Authenticator
|
|
* will try to authenticate the request via one provider at a time. Once one of the
|
|
* providers successfully authenticates the request, the authentication is considered
|
|
* to be successful and the authenticated user will be associated with the request.
|
|
* If provider cannot authenticate the request, the next in line provider in the chain
|
|
* will be used. If all providers in the chain could not authenticate the request,
|
|
* the authentication is then considered to be unsuccessful and an authentication error
|
|
* will be returned.
|
|
*/
|
|
class Authenticator {
|
|
/**
|
|
* HapiJS server instance.
|
|
* @type {Hapi.Server}
|
|
* @private
|
|
*/
|
|
_server = null;
|
|
|
|
/**
|
|
* Service that gathers all `scopes` for particular user.
|
|
* @type {AuthScopeService}
|
|
* @private
|
|
*/
|
|
_authScope = null;
|
|
|
|
/**
|
|
* List of configured and instantiated authentication providers.
|
|
* @type {Map.<string, Object>}
|
|
* @private
|
|
*/
|
|
_providers = null;
|
|
|
|
/**
|
|
* Session class instance.
|
|
* @type {Session}
|
|
* @private
|
|
*/
|
|
_session = null;
|
|
|
|
/**
|
|
* Instantiates Authenticator and bootstrap configured providers.
|
|
* @param {Hapi.Server} server HapiJS Server instance.
|
|
* @param {AuthScopeService} authScope AuthScopeService instance.
|
|
* @param {Session} session Session instance.
|
|
* @param {AuthorizationMode} authorizationMode AuthorizationMode instance
|
|
*/
|
|
constructor(server, authScope, session, authorizationMode) {
|
|
this._server = server;
|
|
this._authScope = authScope;
|
|
this._session = session;
|
|
this._authorizationMode = authorizationMode;
|
|
|
|
const config = this._server.config();
|
|
const authProviders = config.get('xpack.security.authProviders');
|
|
if (authProviders.length === 0) {
|
|
throw new Error(
|
|
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
|
|
);
|
|
}
|
|
|
|
const providerOptions = Object.freeze(getProviderOptions(server));
|
|
|
|
this._providers = new Map(
|
|
authProviders.map(
|
|
(providerType) => [providerType, this._instantiateProvider(providerType, providerOptions)]
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Performs request authentication using configured chain of authentication providers.
|
|
* @param {Hapi.Request} request HapiJS request instance.
|
|
* @returns {Promise.<AuthenticationResult>}.
|
|
*/
|
|
async authenticate(request) {
|
|
assertRequest(request);
|
|
|
|
const isSystemApiRequest = this._server.plugins.kibana.systemApi.isSystemApiRequest(request);
|
|
const existingSession = await this._session.get(request);
|
|
|
|
let authenticationResult;
|
|
for (const [providerType, provider] of this._providerIterator(existingSession)) {
|
|
// Check if current session has been set by this provider.
|
|
const ownsSession = existingSession && existingSession.provider === providerType;
|
|
|
|
authenticationResult = await provider.authenticate(
|
|
request,
|
|
ownsSession ? existingSession.state : null
|
|
);
|
|
|
|
if (ownsSession || authenticationResult.shouldUpdateState()) {
|
|
// If authentication succeeds or requires redirect we should automatically extend existing user session,
|
|
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
|
|
// state we should store it in the session regardless of whether it's a system API request or not.
|
|
const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected())
|
|
&& (authenticationResult.shouldUpdateState() || !isSystemApiRequest);
|
|
|
|
// If provider owned the session, but failed to authenticate anyway, that likely means that
|
|
// session is not valid and we should clear it. Also provider can specifically ask to clear
|
|
// session by setting it to `null` even if authentication attempt didn't fail.
|
|
if (authenticationResult.shouldClearState() || (
|
|
authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
|
|
) {
|
|
await this._session.clear(request);
|
|
} else if (sessionCanBeUpdated) {
|
|
await this._session.set(
|
|
request,
|
|
authenticationResult.shouldUpdateState()
|
|
? { state: authenticationResult.state, provider: providerType }
|
|
: existingSession
|
|
);
|
|
}
|
|
}
|
|
|
|
if (authenticationResult.failed()) {
|
|
return authenticationResult;
|
|
}
|
|
|
|
if (authenticationResult.succeeded()) {
|
|
// we have to do this here, as the auth scope's could be dependent on this
|
|
await this._authorizationMode.initialize(request);
|
|
return AuthenticationResult.succeeded({
|
|
...authenticationResult.user,
|
|
// Complement user returned from the provider with scopes.
|
|
scope: await this._authScope.getForRequestAndUser(request, authenticationResult.user)
|
|
});
|
|
} else if (authenticationResult.redirected()) {
|
|
return authenticationResult;
|
|
}
|
|
}
|
|
|
|
return authenticationResult;
|
|
}
|
|
|
|
/**
|
|
* Deauthenticates current request.
|
|
* @param {Hapi.Request} request HapiJS request instance.
|
|
* @returns {Promise.<DeauthenticationResult>}
|
|
*/
|
|
async deauthenticate(request) {
|
|
assertRequest(request);
|
|
|
|
const sessionValue = await this._getSessionValue(request);
|
|
if (sessionValue) {
|
|
await this._session.clear(request);
|
|
|
|
return this._providers.get(sessionValue.provider).deauthenticate(request, sessionValue.state);
|
|
}
|
|
|
|
// Normally when there is no active session in Kibana, `deauthenticate` method shouldn't do anything
|
|
// and user will eventually be redirected to the home page to log in. But if SAML is supported there
|
|
// is a special case when logout is initiated by the IdP or another SP, then IdP will request _every_
|
|
// SP associated with the current user session to do the logout. So if Kibana (without active session)
|
|
// receives such a request it shouldn't redirect user to the home page, but rather redirect back to IdP
|
|
// with correct logout response and only Elasticsearch knows how to do that.
|
|
if (request.query.SAMLRequest && this._providers.has('saml')) {
|
|
return this._providers.get('saml').deauthenticate(request);
|
|
}
|
|
|
|
return DeauthenticationResult.notHandled();
|
|
}
|
|
|
|
/**
|
|
* Instantiates authentication provider based on the provider key from config.
|
|
* @param {string} providerType Provider type key.
|
|
* @param {Object} options Options to pass to provider's constructor.
|
|
* @returns {Object} Authentication provider instance.
|
|
* @private
|
|
*/
|
|
_instantiateProvider(providerType, options) {
|
|
const ProviderClassName = providerMap.get(providerType);
|
|
if (!ProviderClassName) {
|
|
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
|
|
}
|
|
|
|
return new ProviderClassName(options);
|
|
}
|
|
|
|
/**
|
|
* Returns provider iterator where providers are sorted in the order of priority (based on the session ownership).
|
|
* @param {Object} sessionValue Current session value.
|
|
* @returns {Iterator.<Object>}
|
|
*/
|
|
*_providerIterator(sessionValue) {
|
|
// If there is no session to predict which provider to use first, let's use the order
|
|
// providers are configured in. Otherwise return provider that owns session first, and only then the rest
|
|
// of providers.
|
|
if (!sessionValue) {
|
|
yield* this._providers;
|
|
} else {
|
|
yield [sessionValue.provider, this._providers.get(sessionValue.provider)];
|
|
|
|
for (const [providerType, provider] of this._providers) {
|
|
if (providerType !== sessionValue.provider) {
|
|
yield [providerType, provider];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts session value for the specified request. Under the hood it can
|
|
* clear session if it belongs to the provider that is not available.
|
|
* @param {Hapi.Request} request Request to extract session value for.
|
|
* @returns {Promise.<Object|null>}
|
|
* @private
|
|
*/
|
|
async _getSessionValue(request) {
|
|
let sessionValue = await this._session.get(request);
|
|
|
|
// If for some reason we have a session stored for the provider that is not available
|
|
// (e.g. when user was logged in with one provider, but then configuration has changed
|
|
// and that provider is no longer available), then we should clear session entirely.
|
|
if (sessionValue && !this._providers.has(sessionValue.provider)) {
|
|
await this._session.clear(request);
|
|
sessionValue = null;
|
|
}
|
|
|
|
return sessionValue;
|
|
}
|
|
}
|
|
|
|
export async function initAuthenticator(server, authorizationMode) {
|
|
const session = await Session.create(server);
|
|
const authScope = new AuthScopeService();
|
|
const authenticator = new Authenticator(server, authScope, session, authorizationMode);
|
|
|
|
const loginAttempts = new WeakMap();
|
|
server.decorate('request', 'loginAttempt', function () {
|
|
const request = this;
|
|
if (!loginAttempts.has(request)) {
|
|
loginAttempts.set(request, new LoginAttempt());
|
|
}
|
|
return loginAttempts.get(request);
|
|
});
|
|
|
|
server.expose('authenticate', (request) => authenticator.authenticate(request));
|
|
server.expose('deauthenticate', (request) => authenticator.deauthenticate(request));
|
|
server.expose('registerAuthScopeGetter', (scopeExtender) => authScope.registerGetter(scopeExtender));
|
|
|
|
server.expose('isAuthenticated', async (request) => {
|
|
try {
|
|
await server.plugins.security.getUser(request);
|
|
return true;
|
|
} catch (err) {
|
|
// Don't swallow server errors.
|
|
if (!err.isBoom || err.output.statusCode !== 401) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|