kibana/x-pack/plugins/security/server/authentication/authenticator.ts
Xavier Mouligneau 55fa55ddc9
[SECURITY] Stop kibana crashes when no authentication providers are enabled (#118784)
* check if a provider is there

* the last puzzle

* fix lint

* review oleg

* Update x-pack/plugins/security/public/nav_control/nav_control_component.tsx

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* Update x-pack/test/security_api_integration/http_no_auth_providers.config.ts

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* Update x-pack/plugins/security/server/authentication/authenticator.test.ts

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* Update x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* Update x-pack/test/security_api_integration/http_no_auth_providers.config.ts

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* review II

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
2021-11-19 17:06:34 +01:00

834 lines
32 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { IBasePath, IClusterClient, LoggerFactory } from 'src/core/server';
import { KibanaRequest } from '../../../../../src/core/server';
import {
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
AUTH_URL_HASH_QUERY_STRING_PARAMETER,
LOGOUT_PROVIDER_QUERY_STRING_PARAMETER,
LOGOUT_REASON_QUERY_STRING_PARAMETER,
NEXT_URL_QUERY_STRING_PARAMETER,
} from '../../common/constants';
import type { SecurityLicense } from '../../common/licensing';
import type { AuthenticatedUser, AuthenticationProvider } from '../../common/model';
import { shouldProviderUseLoginForm } from '../../common/model';
import type { AuditServiceSetup } from '../audit';
import { accessAgreementAcknowledgedEvent, userLoginEvent } from '../audit';
import type { ConfigType } from '../config';
import { getErrorStatusCode } from '../errors';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import type { Session, SessionValue } from '../session_management';
import { AuthenticationResult } from './authentication_result';
import { canRedirectRequest } from './can_redirect_request';
import { DeauthenticationResult } from './deauthentication_result';
import { HTTPAuthorizationHeader } from './http_authentication';
import type {
AuthenticationProviderOptions,
AuthenticationProviderSpecificOptions,
BaseAuthenticationProvider,
} from './providers';
import {
AnonymousAuthenticationProvider,
BasicAuthenticationProvider,
HTTPAuthenticationProvider,
KerberosAuthenticationProvider,
OIDCAuthenticationProvider,
PKIAuthenticationProvider,
SAMLAuthenticationProvider,
TokenAuthenticationProvider,
} from './providers';
import { Tokens } from './tokens';
/**
* List of query string parameters used to pass various authentication related metadata that should
* be stripped away from URL as soon as they are no longer needed.
*/
const AUTH_METADATA_QUERY_STRING_PARAMETERS = [
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
AUTH_URL_HASH_QUERY_STRING_PARAMETER,
];
/**
* The shape of the login attempt.
*/
export interface ProviderLoginAttempt {
/**
* Name or type of the provider this login attempt is targeted for.
*/
provider: Pick<AuthenticationProvider, 'name'> | Pick<AuthenticationProvider, 'type'>;
/**
* Optional URL to redirect user to after successful login. This URL is ignored if provider
* decides to redirect user to another URL after login.
*/
redirectURL?: string;
/**
* Login attempt can have any form and defined by the specific provider.
*/
value: unknown;
}
export interface AuthenticatorOptions {
audit: AuditServiceSetup;
featureUsageService: SecurityFeatureUsageServiceStart;
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
config: Pick<ConfigType, 'authc'>;
basePath: IBasePath;
license: SecurityLicense;
loggers: LoggerFactory;
clusterClient: IClusterClient;
session: PublicMethodsOf<Session>;
getServerBaseURL: () => string;
}
// Mapping between provider key defined in the config and authentication
// provider class that can handle specific authentication mechanism.
const providerMap = new Map<
string,
new (
options: AuthenticationProviderOptions,
providerSpecificOptions?: AuthenticationProviderSpecificOptions
) => BaseAuthenticationProvider
>([
[BasicAuthenticationProvider.type, BasicAuthenticationProvider],
[KerberosAuthenticationProvider.type, KerberosAuthenticationProvider],
[SAMLAuthenticationProvider.type, SAMLAuthenticationProvider],
[TokenAuthenticationProvider.type, TokenAuthenticationProvider],
[OIDCAuthenticationProvider.type, OIDCAuthenticationProvider],
[PKIAuthenticationProvider.type, PKIAuthenticationProvider],
[AnonymousAuthenticationProvider.type, AnonymousAuthenticationProvider],
]);
/**
* The route to the access agreement UI.
*/
const ACCESS_AGREEMENT_ROUTE = '/security/access_agreement';
/**
* The route to the overwritten session UI.
*/
const OVERWRITTEN_SESSION_ROUTE = '/security/overwritten_session';
function assertRequest(request: KibanaRequest) {
if (!(request instanceof KibanaRequest)) {
throw new Error(`Request should be a valid "KibanaRequest" instance, was [${typeof request}].`);
}
}
function assertLoginAttempt(attempt: ProviderLoginAttempt) {
if (!isLoginAttemptWithProviderType(attempt) && !isLoginAttemptWithProviderName(attempt)) {
throw new Error(
'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.'
);
}
}
function isLoginAttemptWithProviderName(
attempt: unknown
): attempt is { value: unknown; provider: { name: string } } {
return (
typeof attempt === 'object' &&
(attempt as any)?.provider?.name &&
typeof (attempt as any)?.provider?.name === 'string'
);
}
function isLoginAttemptWithProviderType(
attempt: unknown
): attempt is { value: unknown; provider: Pick<AuthenticationProvider, 'type'> } {
return (
typeof attempt === 'object' &&
(attempt as any)?.provider?.type &&
typeof (attempt as any)?.provider?.type === 'string'
);
}
function isSessionAuthenticated(sessionValue?: Readonly<SessionValue> | null) {
return !!sessionValue?.username;
}
/**
* Instantiates authentication provider based on the provider key from config.
* @param providerType Provider type key.
* @param options Options to pass to provider's constructor.
* @param providerSpecificOptions Options that are specific to {@param providerType}.
*/
function instantiateProvider(
providerType: string,
options: AuthenticationProviderOptions,
providerSpecificOptions?: AuthenticationProviderSpecificOptions
) {
const ProviderClassName = providerMap.get(providerType);
if (!ProviderClassName) {
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
}
return new ProviderClassName(options, providerSpecificOptions);
}
/**
* 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.
*/
export class Authenticator {
/**
* List of configured and instantiated authentication providers.
*/
private readonly providers: Map<string, BaseAuthenticationProvider>;
/**
* Session instance.
*/
private readonly session = this.options.session;
/**
* Internal authenticator logger.
*/
private readonly logger = this.options.loggers.get('authenticator');
/**
* Instantiates Authenticator and bootstrap configured providers.
* @param options Authenticator options.
*/
constructor(private readonly options: Readonly<AuthenticatorOptions>) {
const providerCommonOptions = {
client: this.options.clusterClient,
basePath: this.options.basePath,
getRequestOriginalURL: this.getRequestOriginalURL.bind(this),
tokens: new Tokens({
client: this.options.clusterClient.asInternalUser,
logger: this.options.loggers.get('tokens'),
}),
getServerBaseURL: this.options.getServerBaseURL,
};
this.providers = new Map(
this.options.config.authc.sortedProviders.map(({ type, name }) => {
this.logger.debug(`Enabling "${name}" (${type}) authentication provider.`);
return [
name,
instantiateProvider(
type,
Object.freeze({
...providerCommonOptions,
name,
logger: options.loggers.get(type, name),
urls: { loggedOut: (request) => this.getLoggedOutURL(request, type) },
}),
this.options.config.authc.providers[type]?.[name]
),
];
})
);
// For the BWC reasons we always include HTTP authentication provider unless it's explicitly disabled.
if (this.options.config.authc.http.enabled) {
this.setupHTTPAuthenticationProvider(
Object.freeze({
...providerCommonOptions,
name: '__http__',
logger: options.loggers.get(HTTPAuthenticationProvider.type),
urls: {
loggedOut: (request) => this.getLoggedOutURL(request, HTTPAuthenticationProvider.type),
},
})
);
}
if (this.providers.size === 0) {
throw new Error(
'No authentication provider is configured. Verify `xpack.security.authc.*` config value.'
);
}
}
/**
* Performs the initial login request using the provider login attempt description.
* @param request Request instance.
* @param attempt Login attempt description.
*/
async login(request: KibanaRequest, attempt: ProviderLoginAttempt) {
assertRequest(request);
assertLoginAttempt(attempt);
const existingSessionValue = await this.getSessionValue(request);
// Login attempt can target specific provider by its name (e.g. chosen at the Login Selector UI)
// or a group of providers with the specified type (e.g. in case of 3rd-party initiated login
// attempts we may not know what provider exactly can handle that attempt and we have to try
// every enabled provider of the specified type).
const providers: Array<[string, BaseAuthenticationProvider]> =
isLoginAttemptWithProviderName(attempt) && this.providers.has(attempt.provider.name)
? [[attempt.provider.name, this.providers.get(attempt.provider.name)!]]
: isLoginAttemptWithProviderType(attempt)
? [...this.providerIterator(existingSessionValue?.provider.name)].filter(
([, { type }]) => type === attempt.provider.type
)
: [];
if (providers.length === 0) {
this.logger.debug(
`Login attempt for provider with ${
isLoginAttemptWithProviderName(attempt)
? `name ${attempt.provider.name}`
: `type "${(attempt.provider as Record<string, string>).type}"`
} is detected, but it isn't enabled.`
);
return AuthenticationResult.notHandled();
}
for (const [providerName, provider] of providers) {
// Check if current session has been set by this provider.
const ownsSession =
existingSessionValue?.provider.name === providerName &&
existingSessionValue?.provider.type === provider.type;
const authenticationResult = await provider.login(
request,
attempt.value,
ownsSession ? existingSessionValue!.state : null
);
if (!authenticationResult.notHandled()) {
const sessionUpdateResult = await this.updateSessionValue(request, {
provider: { type: provider.type, name: providerName },
authenticationResult,
existingSessionValue,
});
// Checking for presence of `user` object to determine success state rather than
// `success()` method since that indicates a successful authentication and `redirect()`
// could also (but does not always) authenticate a user successfully (e.g. SAML flow)
if (authenticationResult.user || authenticationResult.failed()) {
const auditLogger = this.options.audit.asScoped(request);
auditLogger.log(
userLoginEvent({
authenticationResult,
authenticationProvider: providerName,
authenticationType: provider.type,
})
);
}
return this.handlePreAccessRedirects(
request,
authenticationResult,
sessionUpdateResult,
attempt.redirectURL
);
}
}
return AuthenticationResult.notHandled();
}
/**
* Performs request authentication using configured chain of authentication providers.
* @param request Request instance.
*/
async authenticate(request: KibanaRequest) {
assertRequest(request);
const existingSessionValue = await this.getSessionValue(request);
if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) {
const providerNameSuggestedByHint = request.url.searchParams.get(
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER
);
this.logger.debug(
`Redirecting request to Login Selector (provider hint: ${
providerNameSuggestedByHint ?? 'n/a'
}).`
);
return AuthenticationResult.redirectTo(
`${
this.options.basePath.serverBasePath
}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}${
providerNameSuggestedByHint
? `&${AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER}=${encodeURIComponent(
providerNameSuggestedByHint
)}`
: ''
}`
);
}
const suggestedProviderName =
existingSessionValue?.provider.name ??
request.url.searchParams.get(AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER);
for (const [providerName, provider] of this.providerIterator(suggestedProviderName)) {
// Check if current session has been set by this provider.
const ownsSession =
existingSessionValue?.provider.name === providerName &&
existingSessionValue?.provider.type === provider.type;
const authenticationResult = await provider.authenticate(
request,
ownsSession ? existingSessionValue!.state : null
);
if (!authenticationResult.notHandled()) {
const sessionUpdateResult = await this.updateSessionValue(request, {
provider: { type: provider.type, name: providerName },
authenticationResult,
existingSessionValue,
});
return canRedirectRequest(request)
? this.handlePreAccessRedirects(request, authenticationResult, sessionUpdateResult)
: authenticationResult;
}
}
return AuthenticationResult.notHandled();
}
/**
* Deauthenticates current request.
* @param request Request instance.
*/
async logout(request: KibanaRequest) {
assertRequest(request);
const sessionValue = await this.getSessionValue(request);
const suggestedProviderName =
sessionValue?.provider.name ??
request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER);
if (suggestedProviderName) {
await this.invalidateSessionValue(request);
// Provider name may be passed in a query param and sourced from the browser's local storage;
// hence, we can't assume that this provider exists, so we have to check it.
const provider = this.providers.get(suggestedProviderName);
if (provider) {
return provider.logout(request, sessionValue?.state ?? null);
}
} else {
// In case logout is called and we cannot figure out what provider is supposed to handle it,
// we should iterate through all providers and let them decide if they can perform a logout.
// This can be necessary if some 3rd-party initiates logout. And even if user doesn't have an
// active session already some providers can still properly respond to the 3rd-party logout
// request. For example SAML provider can process logout request encoded in `SAMLRequest`
// query string parameter.
for (const [, provider] of this.providerIterator()) {
const deauthenticationResult = await provider.logout(request);
if (!deauthenticationResult.notHandled()) {
return deauthenticationResult;
}
}
}
// If none of the configured providers could perform a logout, we should redirect user to the
// default logout location.
return DeauthenticationResult.redirectTo(this.getLoggedOutURL(request));
}
/**
* Acknowledges access agreement on behalf of the currently authenticated user.
* @param request Request instance.
*/
async acknowledgeAccessAgreement(request: KibanaRequest) {
assertRequest(request);
const existingSessionValue = await this.getSessionValue(request);
const currentUser = this.options.getCurrentUser(request);
if (!existingSessionValue || !currentUser) {
throw new Error('Cannot acknowledge access agreement for unauthenticated user.');
}
if (!this.options.license.getFeatures().allowAccessAgreement) {
throw new Error('Current license does not allow access agreement acknowledgement.');
}
await this.session.update(request, {
...existingSessionValue,
accessAgreementAcknowledged: true,
});
const auditLogger = this.options.audit.asScoped(request);
auditLogger.log(
accessAgreementAcknowledgedEvent({
username: currentUser.username,
provider: existingSessionValue.provider,
})
);
this.options.featureUsageService.recordPreAccessAgreementUsage();
}
getRequestOriginalURL(
request: KibanaRequest,
additionalQueryStringParameters?: Array<[string, string]>
) {
const originalURLSearchParams = [
...[...request.url.searchParams.entries()].filter(
([key]) => !AUTH_METADATA_QUERY_STRING_PARAMETERS.includes(key)
),
...(additionalQueryStringParameters ?? []),
];
return `${this.options.basePath.get(request)}${request.url.pathname}${
originalURLSearchParams.length > 0
? `?${new URLSearchParams(originalURLSearchParams).toString()}`
: ''
}`;
}
/**
* Initializes HTTP Authentication provider and appends it to the end of the list of enabled
* authentication providers.
* @param options Common provider options.
*/
private setupHTTPAuthenticationProvider(options: AuthenticationProviderOptions) {
const supportedSchemes = new Set(
this.options.config.authc.http.schemes.map((scheme) => scheme.toLowerCase())
);
// If `autoSchemesEnabled` is set we should allow schemes that other providers use to
// authenticate requests with Elasticsearch.
if (this.options.config.authc.http.autoSchemesEnabled) {
for (const provider of this.providers.values()) {
const supportedScheme = provider.getHTTPAuthenticationScheme();
if (supportedScheme) {
supportedSchemes.add(supportedScheme.toLowerCase());
}
}
}
if (this.providers.has(options.name)) {
throw new Error(`Provider name "${options.name}" is reserved.`);
}
this.providers.set(options.name, new HTTPAuthenticationProvider(options, { supportedSchemes }));
}
/**
* Returns provider iterator starting from the suggested provider if any.
* @param suggestedProviderName Optional name of the provider to return first.
*/
private *providerIterator(
suggestedProviderName?: string | null
): IterableIterator<[string, BaseAuthenticationProvider]> {
// If there is no provider suggested or suggested provider isn't configured, let's use the order
// providers are configured in. Otherwise return suggested provider first, and only then the rest
// of providers.
if (!suggestedProviderName || !this.providers.has(suggestedProviderName)) {
yield* this.providers;
} else {
yield [suggestedProviderName, this.providers.get(suggestedProviderName)!];
for (const [providerName, provider] of this.providers) {
if (providerName !== suggestedProviderName) {
yield [providerName, 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 request Request instance.
*/
private async getSessionValue(request: KibanaRequest) {
const existingSessionValue = await this.session.get(request);
// If we detect that for some reason we have a session stored for the provider that is not
// available anymore (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 (
existingSessionValue &&
this.providers.get(existingSessionValue.provider.name)?.type !==
existingSessionValue.provider.type
) {
this.logger.warn(
`Attempted to retrieve session for the "${existingSessionValue.provider.type}/${existingSessionValue.provider.name}" provider, but it is not configured.`
);
await this.invalidateSessionValue(request);
return null;
}
return existingSessionValue;
}
/**
* Updates, creates, extends or clears session value based on the received authentication result.
* @param request Request instance.
* @param provider Provider that produced provided authentication result.
* @param authenticationResult Result of the authentication or login attempt.
* @param existingSessionValue Value of the existing session if any.
*/
private async updateSessionValue(
request: KibanaRequest,
{
provider,
authenticationResult,
existingSessionValue,
}: {
provider: AuthenticationProvider;
authenticationResult: AuthenticationResult;
existingSessionValue: Readonly<SessionValue> | null;
}
) {
if (!existingSessionValue && !authenticationResult.shouldUpdateState()) {
return null;
}
// Provider can specifically ask to clear session by setting it to `null` even if authentication
// attempt didn't fail.
if (authenticationResult.shouldClearState()) {
this.logger.debug('Authentication provider requested to invalidate existing session.');
await this.invalidateSessionValue(request);
return null;
}
const ownsSession =
existingSessionValue?.provider.name === provider.name &&
existingSessionValue?.provider.type === provider.type;
// If provider owned the session, but failed to authenticate anyway, that likely means that
// session is not valid and we should clear it. Unexpected errors should not cause session
// invalidation (e.g. when Elasticsearch is temporarily unavailable).
if (authenticationResult.failed()) {
if (ownsSession && getErrorStatusCode(authenticationResult.error) === 401) {
this.logger.debug('Authentication attempt failed, existing session will be invalidated.');
await this.invalidateSessionValue(request);
}
return null;
}
// 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 sessionShouldBeUpdatedOrExtended =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || (!request.isSystemRequest && ownsSession));
if (!sessionShouldBeUpdatedOrExtended) {
return ownsSession ? { value: existingSessionValue, overwritten: false } : null;
}
const isExistingSessionAuthenticated = isSessionAuthenticated(existingSessionValue);
const isNewSessionAuthenticated = !!authenticationResult.user;
const providerHasChanged = !!existingSessionValue && !ownsSession;
const sessionHasBeenAuthenticated =
!!existingSessionValue && !isExistingSessionAuthenticated && isNewSessionAuthenticated;
const usernameHasChanged =
isExistingSessionAuthenticated &&
isNewSessionAuthenticated &&
authenticationResult.user!.username !== existingSessionValue!.username;
// There are 3 cases when we SHOULD invalidate existing session and create a new one with
// regenerated SID/AAD:
// 1. If a new session must be created while existing is still valid (e.g. IdP initiated login
// for the user with active session created by another provider).
// 2. If the existing session was unauthenticated (e.g. intermediate session used during SSO
// handshake) and can now be turned into an authenticated one.
// 3. If we re-authenticated user with another username (e.g. during IdP initiated SSO login or
// when client certificate changes and PKI provider needs to re-authenticate user).
if (providerHasChanged) {
this.logger.debug(
'Authentication provider has changed, existing session will be invalidated.'
);
await this.invalidateSessionValue(request);
existingSessionValue = null;
} else if (sessionHasBeenAuthenticated) {
this.logger.debug(
'Session is authenticated, existing unauthenticated session will be invalidated.'
);
await this.invalidateSessionValue(request);
existingSessionValue = null;
} else if (usernameHasChanged) {
this.logger.debug('Username has changed, existing session will be invalidated.');
await this.invalidateSessionValue(request);
existingSessionValue = null;
}
let newSessionValue;
if (!existingSessionValue) {
newSessionValue = await this.session.create(request, {
username: authenticationResult.user?.username,
provider,
state: authenticationResult.shouldUpdateState() ? authenticationResult.state : null,
});
} else if (authenticationResult.shouldUpdateState()) {
newSessionValue = await this.session.update(request, {
...existingSessionValue,
state: authenticationResult.shouldUpdateState()
? authenticationResult.state
: existingSessionValue.state,
});
} else {
newSessionValue = await this.session.extend(request, existingSessionValue);
}
return {
value: newSessionValue,
// We care only about cases when one authenticated session has been overwritten by another
// authenticated session that belongs to a different user (different name or provider/realm).
overwritten:
isExistingSessionAuthenticated &&
isNewSessionAuthenticated &&
(providerHasChanged || usernameHasChanged),
};
}
/**
* Invalidates session value associated with the specified request.
* @param request Request instance.
*/
private async invalidateSessionValue(request: KibanaRequest) {
await this.session.invalidate(request, { match: 'current' });
}
/**
* Checks whether request should be redirected to the Login Selector UI.
* @param request Request instance.
* @param sessionValue Current session value if any.
*/
private shouldRedirectToLoginSelector(request: KibanaRequest, sessionValue: SessionValue | null) {
// Request should be redirected to Login Selector UI only if all following conditions are met:
// 1. Request can be redirected (not API call)
// 2. Request is not authenticated yet
// 3. Login Selector UI is enabled
// 4. Request isn't attributed with HTTP Authorization header
return (
canRedirectRequest(request) &&
!isSessionAuthenticated(sessionValue) &&
this.options.config.authc.selector.enabled &&
HTTPAuthorizationHeader.parseFromRequest(request) == null
);
}
/**
* Checks whether request should be redirected to the Access Agreement UI.
* @param sessionValue Current session value if any.
*/
private shouldRedirectToAccessAgreement(sessionValue: SessionValue | null) {
// Request should be redirected to Access Agreement UI only if all following conditions are met:
// 1. Request can be redirected (not API call)
// 2. Request is authenticated, but user hasn't acknowledged access agreement in the current
// session yet (based on the flag we store in the session)
// 3. Request is authenticated by the provider that has `accessAgreement` configured
// 4. Current license allows access agreement
// 5. And it's not a request to the Access Agreement UI itself
return (
sessionValue != null &&
!sessionValue.accessAgreementAcknowledged &&
(this.options.config.authc.providers as Record<string, any>)[sessionValue.provider.type]?.[
sessionValue.provider.name
]?.accessAgreement &&
this.options.license.getFeatures().allowAccessAgreement
);
}
/**
* In some cases we'd like to redirect user to another page right after successful authentication
* before they can access anything else in Kibana. This method makes sure we do a proper redirect
* that would eventually lead user to a initially requested Kibana URL.
* @param request Request instance.
* @param authenticationResult Result of the authentication.
* @param sessionUpdateResult Result of the session update.
* @param redirectURL
*/
private handlePreAccessRedirects(
request: KibanaRequest,
authenticationResult: AuthenticationResult,
sessionUpdateResult: { value: Readonly<SessionValue> | null; overwritten: boolean } | null,
redirectURL?: string
) {
if (
authenticationResult.failed() ||
request.url.pathname === ACCESS_AGREEMENT_ROUTE ||
request.url.pathname === OVERWRITTEN_SESSION_ROUTE
) {
return authenticationResult;
}
const isUpdatedSessionAuthenticated = isSessionAuthenticated(sessionUpdateResult?.value);
let preAccessRedirectURL;
if (isUpdatedSessionAuthenticated && sessionUpdateResult?.overwritten) {
this.logger.debug('Redirecting user to the overwritten session UI.');
preAccessRedirectURL = `${this.options.basePath.serverBasePath}${OVERWRITTEN_SESSION_ROUTE}`;
} else if (
isUpdatedSessionAuthenticated &&
this.shouldRedirectToAccessAgreement(sessionUpdateResult?.value ?? null)
) {
this.logger.debug('Redirecting user to the access agreement UI.');
preAccessRedirectURL = `${this.options.basePath.serverBasePath}${ACCESS_AGREEMENT_ROUTE}`;
}
// If we need to redirect user to anywhere else before they can access Kibana we should remember
// redirect URL in the `next` parameter. Redirect URL provided in authentication result, if any,
// always takes precedence over what is specified in `redirectURL` parameter.
if (preAccessRedirectURL) {
preAccessRedirectURL = `${preAccessRedirectURL}?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
authenticationResult.redirectURL ||
redirectURL ||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}`;
} else if (redirectURL && !authenticationResult.redirectURL) {
preAccessRedirectURL = redirectURL;
}
return preAccessRedirectURL
? AuthenticationResult.redirectTo(preAccessRedirectURL, {
state: authenticationResult.state,
user: authenticationResult.user,
authResponseHeaders: authenticationResult.authResponseHeaders,
})
: authenticationResult;
}
/**
* Creates a logged out URL for the specified request and provider.
* @param request Request that initiated logout.
* @param providerType Type of the provider that handles logout. If not specified, then the first
* provider in the chain (default) is assumed.
*/
private getLoggedOutURL(request: KibanaRequest, providerType?: string) {
// The app that handles logout needs to know the reason of the logout and the URL we may need to
// redirect user to once they log in again (e.g. when session expires).
const searchParams = new URLSearchParams();
for (const [key, defaultValue] of [
[NEXT_URL_QUERY_STRING_PARAMETER, null],
[LOGOUT_REASON_QUERY_STRING_PARAMETER, 'LOGGED_OUT'],
] as Array<[string, string | null]>) {
const value = request.url.searchParams.get(key) || defaultValue;
if (value) {
searchParams.append(key, value);
}
}
// Query string may contain the path where logout has been called or
// logout reason that login page may need to know.
return this.options.config.authc.selector.enabled ||
(providerType
? shouldProviderUseLoginForm(providerType)
: this.options.config.authc.sortedProviders.length > 0
? shouldProviderUseLoginForm(this.options.config.authc.sortedProviders[0].type)
: false)
? `${this.options.basePath.serverBasePath}/login?${searchParams.toString()}`
: `${this.options.basePath.serverBasePath}/security/logged_out?${searchParams.toString()}`;
}
}