Make all providers to preserve original URL when session expires. (#84229)
This commit is contained in:
parent
30f8e41d45
commit
59a405dc80
|
@ -19,3 +19,6 @@ export const APPLICATION_PREFIX = 'kibana-';
|
|||
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';
|
||||
|
||||
export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint';
|
||||
export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider';
|
||||
export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg';
|
||||
export const NEXT_URL_QUERY_STRING_PARAMETER = 'next';
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import type { AuthenticationProvider } from '../types';
|
||||
import { User } from './user';
|
||||
import type { AuthenticationProvider, User } from '.';
|
||||
|
||||
const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { shouldProviderUseLoginForm } from './authentication_provider';
|
||||
|
||||
describe('#shouldProviderUseLoginForm', () => {
|
||||
['basic', 'token'].forEach((providerType) => {
|
||||
it(`returns "true" for "${providerType}" provider`, () => {
|
||||
expect(shouldProviderUseLoginForm(providerType)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
['anonymous', 'http', 'kerberos', 'oidc', 'pki', 'saml'].forEach((providerType) => {
|
||||
it(`returns "false" for "${providerType}" provider`, () => {
|
||||
expect(shouldProviderUseLoginForm(providerType)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type and name tuple to identify provider used to authenticate user.
|
||||
*/
|
||||
export interface AuthenticationProvider {
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether authentication provider with the specified type uses Kibana's native login form.
|
||||
* @param providerType Type of the authentication provider.
|
||||
*/
|
||||
export function shouldProviderUseLoginForm(providerType: string) {
|
||||
return providerType === 'basic' || providerType === 'token';
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
export { ApiKey, ApiKeyToInvalidate } from './api_key';
|
||||
export { User, EditUser, getUserDisplayName } from './user';
|
||||
export { AuthenticatedUser, canUserChangePassword } from './authenticated_user';
|
||||
export { AuthenticationProvider, shouldProviderUseLoginForm } from './authentication_provider';
|
||||
export { BuiltinESPrivileges } from './builtin_es_privileges';
|
||||
export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges';
|
||||
export { FeaturesPrivileges } from './features_privileges';
|
||||
|
|
|
@ -5,19 +5,21 @@
|
|||
*/
|
||||
|
||||
import { parse } from 'url';
|
||||
import { NEXT_URL_QUERY_STRING_PARAMETER } from './constants';
|
||||
import { isInternalURL } from './is_internal_url';
|
||||
|
||||
export function parseNext(href: string, basePath = '') {
|
||||
const { query, hash } = parse(href, true);
|
||||
if (!query.next) {
|
||||
|
||||
let next = query[NEXT_URL_QUERY_STRING_PARAMETER];
|
||||
if (!next) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
let next: string;
|
||||
if (Array.isArray(query.next) && query.next.length > 0) {
|
||||
next = query.next[0];
|
||||
if (Array.isArray(next) && next.length > 0) {
|
||||
next = next[0];
|
||||
} else {
|
||||
next = query.next as string;
|
||||
next = next as string;
|
||||
}
|
||||
|
||||
// validate that `next` is not attempting a redirect to somewhere
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type and name tuple to identify provider used to authenticate user.
|
||||
*/
|
||||
export interface AuthenticationProvider {
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
import type { AuthenticationProvider } from './model';
|
||||
|
||||
export interface SessionInfo {
|
||||
now: number;
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { LoggedOutPage } from './logged_out_page';
|
||||
|
||||
import { coreMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
describe('LoggedOutPage', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: 'https://some-host' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('points to a base path if `next` parameter is not provided', async () => {
|
||||
const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath;
|
||||
const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/');
|
||||
});
|
||||
|
||||
it('properly parses `next` parameter', async () => {
|
||||
window.location.href = `https://host.com/mock-base-path/security/logged_out?next=${encodeURIComponent(
|
||||
'/mock-base-path/app/home#/?_g=()'
|
||||
)}`;
|
||||
|
||||
const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath;
|
||||
const wrapper = mountWithIntl(<LoggedOutPage basePath={basePathMock} />);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/app/home#/?_g=()');
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@ import ReactDOM from 'react-dom';
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart, IBasePath } from 'src/core/public';
|
||||
import { parseNext } from '../../../common/parse_next';
|
||||
import { AuthenticationStatePage } from '../components';
|
||||
|
||||
interface Props {
|
||||
|
@ -25,7 +26,7 @@ export function LoggedOutPage({ basePath }: Props) {
|
|||
/>
|
||||
}
|
||||
>
|
||||
<EuiButton href={basePath.prepend('/')}>
|
||||
<EuiButton href={parseNext(window.location.href, basePath.serverBasePath)}>
|
||||
<FormattedMessage id="xpack.security.loggedOut.login" defaultMessage="Log in" />
|
||||
</EuiButton>
|
||||
</AuthenticationStatePage>
|
||||
|
|
|
@ -15,7 +15,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public';
|
||||
import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants';
|
||||
import {
|
||||
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
|
||||
LOGOUT_REASON_QUERY_STRING_PARAMETER,
|
||||
} from '../../../common/constants';
|
||||
import { LoginState } from '../../../common/login_state';
|
||||
import { LoginForm, DisabledLoginForm } from './components';
|
||||
|
||||
|
@ -219,7 +222,7 @@ export class LoginPage extends Component<Props, State> {
|
|||
http={this.props.http}
|
||||
notifications={this.props.notifications}
|
||||
selector={selector}
|
||||
infoMessage={infoMessageMap.get(query.msg?.toString())}
|
||||
infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())}
|
||||
loginAssistanceMessage={this.props.loginAssistanceMessage}
|
||||
loginHelp={loginHelp}
|
||||
authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()}
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LOGOUT_PROVIDER_QUERY_STRING_PARAMETER,
|
||||
LOGOUT_REASON_QUERY_STRING_PARAMETER,
|
||||
NEXT_URL_QUERY_STRING_PARAMETER,
|
||||
} from '../../common/constants';
|
||||
|
||||
export interface ISessionExpired {
|
||||
logout(): void;
|
||||
}
|
||||
|
@ -11,13 +17,15 @@ export interface ISessionExpired {
|
|||
const getNextParameter = () => {
|
||||
const { location } = window;
|
||||
const next = encodeURIComponent(`${location.pathname}${location.search}${location.hash}`);
|
||||
return `&next=${next}`;
|
||||
return `&${NEXT_URL_QUERY_STRING_PARAMETER}=${next}`;
|
||||
};
|
||||
|
||||
const getProviderParameter = (tenant: string) => {
|
||||
const key = `${tenant}/session_provider`;
|
||||
const providerName = sessionStorage.getItem(key);
|
||||
return providerName ? `&provider=${encodeURIComponent(providerName)}` : '';
|
||||
return providerName
|
||||
? `&${LOGOUT_PROVIDER_QUERY_STRING_PARAMETER}=${encodeURIComponent(providerName)}`
|
||||
: '';
|
||||
};
|
||||
|
||||
export class SessionExpired {
|
||||
|
@ -26,6 +34,8 @@ export class SessionExpired {
|
|||
logout() {
|
||||
const next = getNextParameter();
|
||||
const provider = getProviderParameter(this.tenant);
|
||||
window.location.assign(`${this.logoutUrl}?msg=SESSION_EXPIRED${next}${provider}`);
|
||||
window.location.assign(
|
||||
`${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=SESSION_EXPIRED${next}${provider}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import type { AuthenticationProvider } from '../../common/model';
|
||||
import { LegacyAuditLogger } from './audit_service';
|
||||
|
||||
/**
|
||||
|
|
|
@ -111,31 +111,78 @@ describe('Authenticator', () => {
|
|||
).toThrowError('Provider name "__http__" is reserved.');
|
||||
});
|
||||
|
||||
it('properly sets `loggedOut` URL.', () => {
|
||||
const basicAuthenticationProviderMock = jest.requireMock('./providers/basic')
|
||||
.BasicAuthenticationProvider;
|
||||
describe('#options.urls.loggedOut', () => {
|
||||
it('points to /login if provider requires login form', () => {
|
||||
const authenticationProviderMock = jest.requireMock(`./providers/basic`)
|
||||
.BasicAuthenticationProvider;
|
||||
authenticationProviderMock.mockClear();
|
||||
new Authenticator(getMockOptions());
|
||||
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
||||
|
||||
basicAuthenticationProviderMock.mockClear();
|
||||
new Authenticator(getMockOptions());
|
||||
expect(basicAuthenticationProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
urls: {
|
||||
loggedOut: '/mock-server-basepath/security/logged_out',
|
||||
},
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
||||
'/mock-server-basepath/login?msg=LOGGED_OUT'
|
||||
);
|
||||
|
||||
basicAuthenticationProviderMock.mockClear();
|
||||
new Authenticator(getMockOptions({ selector: { enabled: true } }));
|
||||
expect(basicAuthenticationProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
urls: {
|
||||
loggedOut: `/mock-server-basepath/login?msg=LOGGED_OUT`,
|
||||
},
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
expect(
|
||||
getLoggedOutURL(
|
||||
httpServerMock.createKibanaRequest({
|
||||
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
||||
})
|
||||
)
|
||||
).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED');
|
||||
});
|
||||
|
||||
it('points to /login if login selector is enabled', () => {
|
||||
const authenticationProviderMock = jest.requireMock(`./providers/saml`)
|
||||
.SAMLAuthenticationProvider;
|
||||
authenticationProviderMock.mockClear();
|
||||
new Authenticator(
|
||||
getMockOptions({
|
||||
selector: { enabled: true },
|
||||
providers: { saml: { saml1: { order: 0, realm: 'realm' } } },
|
||||
})
|
||||
);
|
||||
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
||||
|
||||
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
||||
'/mock-server-basepath/login?msg=LOGGED_OUT'
|
||||
);
|
||||
|
||||
expect(
|
||||
getLoggedOutURL(
|
||||
httpServerMock.createKibanaRequest({
|
||||
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
||||
})
|
||||
)
|
||||
).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED');
|
||||
});
|
||||
|
||||
it('points to /security/logged_out if login selector is NOT enabled', () => {
|
||||
const authenticationProviderMock = jest.requireMock(`./providers/saml`)
|
||||
.SAMLAuthenticationProvider;
|
||||
authenticationProviderMock.mockClear();
|
||||
new Authenticator(
|
||||
getMockOptions({
|
||||
selector: { enabled: false },
|
||||
providers: { saml: { saml1: { order: 0, realm: 'realm' } } },
|
||||
})
|
||||
);
|
||||
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
||||
|
||||
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
||||
'/mock-server-basepath/security/logged_out?msg=LOGGED_OUT'
|
||||
);
|
||||
|
||||
expect(
|
||||
getLoggedOutURL(
|
||||
httpServerMock.createKibanaRequest({
|
||||
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
||||
})
|
||||
)
|
||||
).toBe(
|
||||
'/mock-server-basepath/security/logged_out?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP authentication provider', () => {
|
||||
|
@ -1769,7 +1816,9 @@ describe('Authenticator', () => {
|
|||
});
|
||||
|
||||
it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic1' } });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
query: { provider: 'basic1' },
|
||||
});
|
||||
mockOptions.session.get.mockResolvedValue(null);
|
||||
|
||||
mockBasicAuthenticationProvider.logout.mockResolvedValue(
|
||||
|
@ -1782,7 +1831,7 @@ describe('Authenticator', () => {
|
|||
|
||||
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
|
||||
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null);
|
||||
expect(mockOptions.session.clear).not.toHaveBeenCalled();
|
||||
expect(mockOptions.session.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => {
|
||||
|
@ -1811,7 +1860,7 @@ describe('Authenticator', () => {
|
|||
);
|
||||
|
||||
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
|
||||
expect(mockOptions.session.clear).not.toHaveBeenCalled();
|
||||
expect(mockOptions.session.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,10 +10,15 @@ import {
|
|||
ILegacyClusterClient,
|
||||
IBasePath,
|
||||
} from '../../../../../src/core/server';
|
||||
import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../common/constants';
|
||||
import {
|
||||
AUTH_PROVIDER_HINT_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 } from '../../common/model';
|
||||
import type { AuthenticationProvider } from '../../common/types';
|
||||
import type { AuthenticatedUser, AuthenticationProvider } from '../../common/model';
|
||||
import { shouldProviderUseLoginForm } from '../../common/model';
|
||||
import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit';
|
||||
import type { ConfigType } from '../config';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
|
@ -199,11 +204,6 @@ export class Authenticator {
|
|||
client: this.options.clusterClient,
|
||||
logger: this.options.loggers.get('tokens'),
|
||||
}),
|
||||
urls: {
|
||||
loggedOut: options.config.authc.selector.enabled
|
||||
? `${options.basePath.serverBasePath}/login?msg=LOGGED_OUT`
|
||||
: `${options.basePath.serverBasePath}/security/logged_out`,
|
||||
},
|
||||
};
|
||||
|
||||
this.providers = new Map(
|
||||
|
@ -218,6 +218,7 @@ export class Authenticator {
|
|||
...providerCommonOptions,
|
||||
name,
|
||||
logger: options.loggers.get(type, name),
|
||||
urls: { loggedOut: (request) => this.getLoggedOutURL(request, type) },
|
||||
}),
|
||||
this.options.config.authc.providers[type]?.[name]
|
||||
),
|
||||
|
@ -232,6 +233,9 @@ export class Authenticator {
|
|||
...providerCommonOptions,
|
||||
name: '__http__',
|
||||
logger: options.loggers.get(HTTPAuthenticationProvider.type),
|
||||
urls: {
|
||||
loggedOut: (request) => this.getLoggedOutURL(request, HTTPAuthenticationProvider.type),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -338,7 +342,9 @@ export class Authenticator {
|
|||
if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) {
|
||||
this.logger.debug('Redirecting request to Login Selector.');
|
||||
return AuthenticationResult.redirectTo(
|
||||
`${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent(
|
||||
`${
|
||||
this.options.basePath.serverBasePath
|
||||
}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
|
||||
)}${
|
||||
suggestedProviderName && !existingSessionValue
|
||||
|
@ -385,20 +391,17 @@ export class Authenticator {
|
|||
assertRequest(request);
|
||||
|
||||
const sessionValue = await this.getSessionValue(request);
|
||||
if (sessionValue) {
|
||||
const suggestedProviderName =
|
||||
sessionValue?.provider.name ??
|
||||
request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER);
|
||||
if (suggestedProviderName) {
|
||||
await this.session.clear(request);
|
||||
return this.providers
|
||||
.get(sessionValue.provider.name)!
|
||||
.logout(request, sessionValue.state ?? null);
|
||||
}
|
||||
|
||||
const queryStringProviderName = (request.query as Record<string, string>)?.provider;
|
||||
if (queryStringProviderName) {
|
||||
// provider name is 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(queryStringProviderName);
|
||||
// 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, null);
|
||||
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,
|
||||
|
@ -737,7 +740,7 @@ export class Authenticator {
|
|||
// 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=${encodeURIComponent(
|
||||
preAccessRedirectURL = `${preAccessRedirectURL}?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
|
||||
authenticationResult.redirectURL ||
|
||||
redirectURL ||
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
|
||||
|
@ -754,4 +757,30 @@ export class Authenticator {
|
|||
})
|
||||
: 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.
|
||||
*/
|
||||
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 || shouldProviderUseLoginForm(providerType)
|
||||
? `${this.options.basePath.serverBasePath}/login?${searchParams.toString()}`
|
||||
: `${this.options.basePath.serverBasePath}/security/logged_out?${searchParams.toString()}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider
|
|||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@ export function mockAuthenticationProviderOptions(options?: { name: string }) {
|
|||
tokens: { refresh: jest.fn(), invalidate: jest.fn() },
|
||||
name: options?.name ?? 'basic1',
|
||||
urls: {
|
||||
loggedOut: '/mock-server-basepath/security/logged_out',
|
||||
loggedOut: jest.fn().mockReturnValue('/mock-server-basepath/security/logged_out'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export interface AuthenticationProviderOptions {
|
|||
logger: Logger;
|
||||
tokens: PublicMethodsOf<Tokens>;
|
||||
urls: {
|
||||
loggedOut: string;
|
||||
loggedOut: (request: KibanaRequest) => string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ describe('BasicAuthenticationProvider', () => {
|
|||
let mockOptions: ReturnType<typeof mockAuthenticationProviderOptions>;
|
||||
beforeEach(() => {
|
||||
mockOptions = mockAuthenticationProviderOptions();
|
||||
mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page');
|
||||
|
||||
provider = new BasicAuthenticationProvider(mockOptions);
|
||||
});
|
||||
|
||||
|
@ -184,30 +186,13 @@ describe('BasicAuthenticationProvider', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('redirects to login view if state is `null`.', async () => {
|
||||
await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
||||
);
|
||||
});
|
||||
|
||||
it('always redirects to the login page.', async () => {
|
||||
it('redirects to the logged out URL.', async () => {
|
||||
await expect(provider.logout(httpServerMock.createKibanaRequest(), {})).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
||||
DeauthenticationResult.redirectTo('/some-logged-out-page')
|
||||
);
|
||||
});
|
||||
|
||||
it('passes query string parameters to the login page.', async () => {
|
||||
await expect(
|
||||
provider.logout(
|
||||
httpServerMock.createKibanaRequest({
|
||||
query: { next: '/app/ml', msg: 'SESSION_EXPIRED' },
|
||||
}),
|
||||
{}
|
||||
)
|
||||
).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(
|
||||
'/mock-server-basepath/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED'
|
||||
)
|
||||
await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/some-logged-out-page')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
|
||||
import { canRedirectRequest } from '../can_redirect_request';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
@ -108,7 +109,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
this.logger.debug('Redirecting request to Login page.');
|
||||
const basePath = this.options.basePath.get(request);
|
||||
return AuthenticationResult.redirectTo(
|
||||
`${basePath}/login?next=${encodeURIComponent(
|
||||
`${basePath}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
|
||||
`${basePath}${request.url.pathname}${request.url.search}`
|
||||
)}`
|
||||
);
|
||||
|
@ -131,12 +132,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
// Query string may contain the path where logout has been called or
|
||||
// logout reason that login page may need to know.
|
||||
const queryString = request.url.search || `?msg=LOGGED_OUT`;
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.get(request)}/login${queryString}`
|
||||
);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -470,7 +470,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await expect(provider.logout(request, null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
|
@ -501,7 +501,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, tokenPair)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -124,7 +124,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -611,10 +611,10 @@ describe('OIDCAuthenticationProvider', () => {
|
|||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await expect(provider.logout(request, null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
await expect(provider.logout(request, { nonce: 'x', realm: 'oidc1' })).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
|
@ -647,7 +647,7 @@ describe('OIDCAuthenticationProvider', () => {
|
|||
|
||||
await expect(
|
||||
provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' })
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut));
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import type from 'type-detect';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
|
||||
import type { AuthenticationInfo } from '../../elasticsearch';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { canRedirectRequest } from '../can_redirect_request';
|
||||
|
@ -434,7 +435,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -450,14 +451,18 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
* @param request Request instance.
|
||||
*/
|
||||
private captureRedirectURL(request: KibanaRequest) {
|
||||
const searchParams = new URLSearchParams([
|
||||
[
|
||||
NEXT_URL_QUERY_STRING_PARAMETER,
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`,
|
||||
],
|
||||
['providerType', this.type],
|
||||
['providerName', this.options.name],
|
||||
]);
|
||||
return AuthenticationResult.redirectTo(
|
||||
`${
|
||||
this.options.basePath.serverBasePath
|
||||
}/internal/security/capture-url?next=${encodeURIComponent(
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
|
||||
)}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent(
|
||||
this.options.name
|
||||
)}`,
|
||||
}/internal/security/capture-url?${searchParams.toString()}`,
|
||||
// Here we indicate that current session, if any, should be invalidated. It is a no-op for the
|
||||
// initial handshake, but is essential when both access and refresh tokens are expired.
|
||||
{ state: null }
|
||||
|
|
|
@ -544,7 +544,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await expect(provider.logout(request, null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
|
@ -572,7 +572,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, state)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -128,7 +128,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1022,10 +1022,10 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await expect(provider.logout(request, null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
|
@ -1082,7 +1082,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
refreshToken,
|
||||
realm: 'test-realm',
|
||||
})
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut));
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1103,7 +1103,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
refreshToken,
|
||||
realm: 'test-realm',
|
||||
})
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut));
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1126,7 +1126,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
refreshToken,
|
||||
realm: 'test-realm',
|
||||
})
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut));
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', {
|
||||
|
@ -1145,7 +1145,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
refreshToken: 'x-saml-refresh-token',
|
||||
realm: 'test-realm',
|
||||
})
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut));
|
||||
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', {
|
||||
|
@ -1159,7 +1159,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
@ -1174,7 +1174,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
|
@ -1187,7 +1187,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
const request = httpServerMock.createKibanaRequest({ query: { SAMLResponse: 'xxx yyy' } });
|
||||
|
||||
await expect(provider.logout(request)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)
|
||||
DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))
|
||||
);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { isInternalURL } from '../../../common/is_internal_url';
|
||||
import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
|
||||
import type { AuthenticationInfo } from '../../elasticsearch';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
@ -282,7 +283,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -606,14 +607,18 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
* @param request Request instance.
|
||||
*/
|
||||
private captureRedirectURL(request: KibanaRequest) {
|
||||
const searchParams = new URLSearchParams([
|
||||
[
|
||||
NEXT_URL_QUERY_STRING_PARAMETER,
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`,
|
||||
],
|
||||
['providerType', this.type],
|
||||
['providerName', this.options.name],
|
||||
]);
|
||||
return AuthenticationResult.redirectTo(
|
||||
`${
|
||||
this.options.basePath.serverBasePath
|
||||
}/internal/security/capture-url?next=${encodeURIComponent(
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
|
||||
)}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent(
|
||||
this.options.name
|
||||
)}`,
|
||||
}/internal/security/capture-url?${searchParams.toString()}`,
|
||||
// Here we indicate that current session, if any, should be invalidated. It is a no-op for the
|
||||
// initial handshake, but is essential when both access and refresh tokens are expired.
|
||||
{ state: null }
|
||||
|
|
|
@ -37,6 +37,8 @@ describe('TokenAuthenticationProvider', () => {
|
|||
let mockOptions: MockAuthenticationProviderOptions;
|
||||
beforeEach(() => {
|
||||
mockOptions = mockAuthenticationProviderOptions({ name: 'token' });
|
||||
mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page');
|
||||
|
||||
provider = new TokenAuthenticationProvider(mockOptions);
|
||||
});
|
||||
|
||||
|
@ -347,11 +349,9 @@ describe('TokenAuthenticationProvider', () => {
|
|||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirects to login view if state is `null`.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
await expect(provider.logout(request, null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
||||
it('redirects to the logged out URL if state is `null`.', async () => {
|
||||
await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/some-logged-out-page')
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
|
@ -372,28 +372,14 @@ describe('TokenAuthenticationProvider', () => {
|
|||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair);
|
||||
});
|
||||
|
||||
it('redirects to /login if tokens are invalidated successfully', async () => {
|
||||
it('redirects to the logged out URL if tokens are invalidated successfully.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
|
||||
|
||||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, tokenPair)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair);
|
||||
});
|
||||
|
||||
it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ query: { yep: 'nope' } });
|
||||
const tokenPair = { accessToken: 'foo', refreshToken: 'bar' };
|
||||
|
||||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
await expect(provider.logout(request, tokenPair)).resolves.toEqual(
|
||||
DeauthenticationResult.redirectTo('/mock-server-basepath/login?yep=nope')
|
||||
DeauthenticationResult.redirectTo('/some-logged-out-page')
|
||||
);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import Boom from '@hapi/boom';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { canRedirectRequest } from '../can_redirect_request';
|
||||
|
@ -145,10 +146,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
}
|
||||
}
|
||||
|
||||
const queryString = request.url.search || `?msg=LOGGED_OUT`;
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this.options.basePath.get(request)}/login${queryString}`
|
||||
);
|
||||
return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,6 +233,8 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
const nextURL = encodeURIComponent(
|
||||
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
|
||||
);
|
||||
return `${this.options.basePath.get(request)}/login?next=${nextURL}`;
|
||||
return `${this.options.basePath.get(
|
||||
request
|
||||
)}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${nextURL}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { Duration } from 'moment';
|
|||
import { schema, Type, TypeOf } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Logger, config as coreConfig } from '../../../../src/core/server';
|
||||
import type { AuthenticationProvider } from '../common/types';
|
||||
import type { AuthenticationProvider } from '../common/model';
|
||||
|
||||
export type ConfigType = ReturnType<typeof createConfig>;
|
||||
type RawConfigType = TypeOf<typeof ConfigSchema>;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
RequestHandlerContext,
|
||||
} from '../../../../../../src/core/server';
|
||||
import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing';
|
||||
import { AuthenticationProvider } from '../../../common/types';
|
||||
import type { AuthenticationProvider } from '../../../common/model';
|
||||
import { ConfigType } from '../../config';
|
||||
import { Session } from '../../session_management';
|
||||
import { defineAccessAgreementRoutes } from './access_agreement';
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { parseNext } from '../../../common/parse_next';
|
||||
import { LoginState } from '../../../common/login_state';
|
||||
import { shouldProviderUseLoginForm } from '../../../common/model';
|
||||
import {
|
||||
LOGOUT_REASON_QUERY_STRING_PARAMETER,
|
||||
NEXT_URL_QUERY_STRING_PARAMETER,
|
||||
} from '../../../common/constants';
|
||||
import { RouteDefinitionParams } from '..';
|
||||
|
||||
/**
|
||||
|
@ -26,8 +31,8 @@ export function defineLoginRoutes({
|
|||
validate: {
|
||||
query: schema.object(
|
||||
{
|
||||
next: schema.maybe(schema.string()),
|
||||
msg: schema.maybe(schema.string()),
|
||||
[NEXT_URL_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()),
|
||||
[LOGOUT_REASON_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
|
@ -59,7 +64,7 @@ export function defineLoginRoutes({
|
|||
// Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can
|
||||
// be sure that config is present for every provider in `config.authc.sortedProviders`.
|
||||
const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!;
|
||||
const usesLoginForm = type === 'basic' || type === 'token';
|
||||
const usesLoginForm = shouldProviderUseLoginForm(type);
|
||||
return {
|
||||
type,
|
||||
name,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { randomBytes, createHash } from 'crypto';
|
|||
import nodeCrypto, { Crypto } from '@elastic/node-crypto';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { KibanaRequest, Logger } from '../../../../../src/core/server';
|
||||
import type { AuthenticationProvider } from '../../common/types';
|
||||
import type { AuthenticationProvider } from '../../common/model';
|
||||
import type { ConfigType } from '../config';
|
||||
import type { SessionIndex, SessionIndexValue } from './session_index';
|
||||
import type { SessionCookie } from './session_cookie';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { ILegacyClusterClient, Logger } from '../../../../../src/core/server';
|
||||
import type { AuthenticationProvider } from '../../common/types';
|
||||
import type { AuthenticationProvider } from '../../common/model';
|
||||
import type { ConfigType } from '../config';
|
||||
|
||||
export interface SessionIndexOptions {
|
||||
|
|
|
@ -182,7 +182,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(cookies).to.have.length(1);
|
||||
checkCookieIsCleared(request.cookie(cookies[0])!);
|
||||
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out');
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
|
||||
|
||||
// Old cookie should be invalidated and not allow API access.
|
||||
const apiResponse = await supertest
|
||||
|
|
|
@ -259,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(cookies).to.have.length(1);
|
||||
checkCookieIsCleared(request.cookie(cookies[0])!);
|
||||
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out');
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
|
||||
|
||||
// Token that was stored in the previous cookie should be invalidated as well and old
|
||||
// session cookie should not allow API access.
|
||||
|
|
|
@ -10,7 +10,7 @@ import { resolve } from 'path';
|
|||
import url from 'url';
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import expect from '@kbn/expect';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
|
||||
import { getStateAndNonce } from '../../fixtures/oidc/oidc_tools';
|
||||
import {
|
||||
getMutualAuthenticationResponseToken,
|
||||
|
|
|
@ -307,7 +307,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(cookies).to.have.length(1);
|
||||
checkCookieIsCleared(request.cookie(cookies[0])!);
|
||||
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out');
|
||||
expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
|
||||
});
|
||||
|
||||
it('should redirect to home page if session cookie is not provided', async () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import request, { Cookie } from 'request';
|
||||
import { delay } from 'bluebird';
|
||||
import expect from '@kbn/expect';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
|
||||
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import request, { Cookie } from 'request';
|
||||
import { delay } from 'bluebird';
|
||||
import expect from '@kbn/expect';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/model';
|
||||
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
|
|
Loading…
Reference in a new issue