Change session expiration to override on app leave behavior (#129384)

* Change session expiration to override on app leave behavior

* fix types
This commit is contained in:
Thom Heymann 2022-04-05 14:12:59 +01:00 committed by GitHub
parent 1ee5666033
commit 735b4c936d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 59 additions and 38 deletions

View file

@ -6,7 +6,13 @@
*/
import { i18n } from '@kbn/i18n';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import type {
CoreSetup,
CoreStart,
HttpSetup,
Plugin,
PluginInitializerContext,
} from 'src/core/public';
import type { DataViewsPublicPluginStart } from 'src/plugins/data_views/public';
import type { HomePublicPluginSetup } from 'src/plugins/home/public';
import type { ManagementSetup, ManagementStart } from 'src/plugins/management/public';
@ -56,7 +62,7 @@ export class SecurityPlugin
>
{
private readonly config: ConfigType;
private sessionTimeout!: SessionTimeout;
private sessionTimeout?: SessionTimeout;
private readonly authenticationService = new AuthenticationService();
private readonly navControlService = new SecurityNavControlService();
private readonly securityLicenseService = new SecurityLicenseService();
@ -74,16 +80,6 @@ export class SecurityPlugin
core: CoreSetup<PluginStartDependencies>,
{ home, licensing, management, share }: PluginSetupDependencies
): SecurityPluginSetup {
const { http, notifications } = core;
const { anonymousPaths } = http;
const logoutUrl = `${core.http.basePath.serverBasePath}/logout`;
const tenant = core.http.basePath.serverBasePath;
const sessionExpired = new SessionExpired(logoutUrl, tenant);
http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths));
this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant);
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
this.securityCheckupService.setup({ http: core.http });
@ -99,7 +95,7 @@ export class SecurityPlugin
this.navControlService.setup({
securityLicense: license,
authc: this.authc,
logoutUrl,
logoutUrl: getLogoutUrl(core.http),
});
accountManagementApp.create({
@ -149,19 +145,25 @@ export class SecurityPlugin
core: CoreStart,
{ management, share }: PluginStartDependencies
): SecurityPluginStart {
const { application, http, notifications, docLinks } = core;
const { anonymousPaths } = http;
const logoutUrl = getLogoutUrl(http);
const tenant = http.basePath.serverBasePath;
const sessionExpired = new SessionExpired(application, logoutUrl, tenant);
http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths));
this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant);
this.sessionTimeout.start();
this.securityCheckupService.start({
http: core.http,
notifications: core.notifications,
docLinks: core.docLinks,
});
this.securityCheckupService.start({ http, notifications, docLinks });
if (management) {
this.managementService.start({ capabilities: core.application.capabilities });
this.managementService.start({ capabilities: application.capabilities });
}
if (share) {
this.anonymousAccessService.start({ http: core.http });
this.anonymousAccessService.start({ http });
}
return {
@ -172,13 +174,17 @@ export class SecurityPlugin
}
public stop() {
this.sessionTimeout.stop();
this.sessionTimeout?.stop();
this.navControlService.stop();
this.securityLicenseService.stop();
this.managementService.stop();
}
}
function getLogoutUrl(http: HttpSetup) {
return `${http.basePath.serverBasePath}/logout`;
}
export interface SecurityPluginSetup {
/**
* Exposes authentication information about the currently logged in user.

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import { applicationServiceMock } from 'src/core/public/mocks';
import { LogoutReason } from '../../common/types';
import { SessionExpired } from './session_expired';
describe('#logout', () => {
const application = applicationServiceMock.createStartContract();
const mockGetItem = jest.fn().mockReturnValue(null);
const CURRENT_URL = '/foo/bar?baz=quz#quuz';
const LOGOUT_URL = '/logout';
@ -26,13 +29,13 @@ describe('#logout', () => {
beforeEach(() => {
Object.defineProperty(window, 'location', {
value: {
assign: jest.fn(),
pathname: CURRENT_URL,
search: '',
hash: '',
},
configurable: true,
});
application.navigateToUrl.mockClear();
mockGetItem.mockReset();
});
@ -41,22 +44,24 @@ describe('#logout', () => {
});
it(`redirects user to the logout URL with 'msg' and 'next' parameters`, async () => {
const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT);
const sessionExpired = new SessionExpired(application, LOGOUT_URL, TENANT);
sessionExpired.logout(LogoutReason.SESSION_EXPIRED);
const next = `&next=${encodeURIComponent(CURRENT_URL)}`;
await expect(window.location.assign).toHaveBeenCalledWith(
`${LOGOUT_URL}?msg=SESSION_EXPIRED${next}`
await expect(application.navigateToUrl).toHaveBeenCalledWith(
`${LOGOUT_URL}?msg=SESSION_EXPIRED${next}`,
{ forceRedirect: true, skipAppLeave: true }
);
});
it(`redirects user to the logout URL with custom reason 'msg'`, async () => {
const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT);
const sessionExpired = new SessionExpired(application, LOGOUT_URL, TENANT);
sessionExpired.logout(LogoutReason.AUTHENTICATION_ERROR);
const next = `&next=${encodeURIComponent(CURRENT_URL)}`;
await expect(window.location.assign).toHaveBeenCalledWith(
`${LOGOUT_URL}?msg=AUTHENTICATION_ERROR${next}`
await expect(application.navigateToUrl).toHaveBeenCalledWith(
`${LOGOUT_URL}?msg=AUTHENTICATION_ERROR${next}`,
{ forceRedirect: true, skipAppLeave: true }
);
});
@ -64,7 +69,7 @@ describe('#logout', () => {
const providerName = 'basic';
mockGetItem.mockReturnValueOnce(providerName);
const sessionExpired = new SessionExpired(LOGOUT_URL, TENANT);
const sessionExpired = new SessionExpired(application, LOGOUT_URL, TENANT);
sessionExpired.logout(LogoutReason.SESSION_EXPIRED);
expect(mockGetItem).toHaveBeenCalledTimes(1);
@ -72,8 +77,9 @@ describe('#logout', () => {
const next = `&next=${encodeURIComponent(CURRENT_URL)}`;
const provider = `&provider=${providerName}`;
await expect(window.location.assign).toBeCalledWith(
`${LOGOUT_URL}?msg=SESSION_EXPIRED${next}${provider}`
await expect(application.navigateToUrl).toBeCalledWith(
`${LOGOUT_URL}?msg=SESSION_EXPIRED${next}${provider}`,
{ forceRedirect: true, skipAppLeave: true }
);
});
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { ApplicationStart } from 'src/core/public';
import {
LOGOUT_PROVIDER_QUERY_STRING_PARAMETER,
LOGOUT_REASON_QUERY_STRING_PARAMETER,
@ -27,13 +29,18 @@ const getProviderParameter = (tenant: string) => {
};
export class SessionExpired {
constructor(private logoutUrl: string, private tenant: string) {}
constructor(
private application: ApplicationStart,
private logoutUrl: string,
private tenant: string
) {}
logout(reason: LogoutReason) {
const next = getNextParameter();
const provider = getProviderParameter(this.tenant);
window.location.assign(
`${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=${reason}${next}${provider}`
this.application.navigateToUrl(
`${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=${reason}${next}${provider}`,
{ forceRedirect: true, skipAppLeave: true }
);
}
}

View file

@ -8,6 +8,7 @@
// @ts-ignore
import fetchMock from 'fetch-mock/es5/client';
import { applicationServiceMock } from 'src/core/public/mocks';
import { setup } from 'src/core/test_helpers/http_test_setup';
import { SessionExpired } from './session_expired';
@ -30,6 +31,7 @@ const setupHttp = (basePath: string) => {
return http;
};
const tenant = '';
const application = applicationServiceMock.createStartContract();
afterEach(() => {
fetchMock.restore();
@ -37,7 +39,7 @@ afterEach(() => {
it(`logs out 401 responses`, async () => {
const http = setupHttp('/foo');
const sessionExpired = new SessionExpired(`${http.basePath}/logout`, tenant);
const sessionExpired = new SessionExpired(application, `${http.basePath}/logout`, tenant);
const logoutPromise = new Promise<void>((resolve) => {
jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve());
});
@ -64,7 +66,7 @@ it(`ignores anonymous paths`, async () => {
const http = setupHttp('/foo');
const { anonymousPaths } = http;
anonymousPaths.register('/bar');
const sessionExpired = new SessionExpired(`${http.basePath}/logout`, tenant);
const sessionExpired = new SessionExpired(application, `${http.basePath}/logout`, tenant);
const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths);
http.intercept(interceptor);
fetchMock.mock('*', 401);
@ -75,7 +77,7 @@ it(`ignores anonymous paths`, async () => {
it(`ignores errors which don't have a response, for example network connectivity issues`, async () => {
const http = setupHttp('/foo');
const sessionExpired = new SessionExpired(`${http.basePath}/logout`, tenant);
const sessionExpired = new SessionExpired(application, `${http.basePath}/logout`, tenant);
const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths);
http.intercept(interceptor);
fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down'))));
@ -86,7 +88,7 @@ it(`ignores errors which don't have a response, for example network connectivity
it(`ignores requests which omit credentials`, async () => {
const http = setupHttp('/foo');
const sessionExpired = new SessionExpired(`${http.basePath}/logout`, tenant);
const sessionExpired = new SessionExpired(application, `${http.basePath}/logout`, tenant);
const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths);
http.intercept(interceptor);
fetchMock.mock('*', 401);