Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve session idle timeout, add session lifespan #49855

Merged
merged 26 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d7cf8d4
Remove dead comment
jportner Oct 28, 2019
fa71ade
Add session lifespan, rename sessionTimeout to session.idleTimeout
jportner Oct 28, 2019
00cf1d0
Add route config option to avoid extending the user's session timeout
jportner Oct 30, 2019
86ea7c9
Change method of using Hapi RouteOptionsApp interface
jportner Oct 30, 2019
45ee92d
Add /api/security/session/info endpoint
jportner Oct 29, 2019
b96b8da
Add /api/security/session/extend endpoint
jportner Oct 30, 2019
59ead42
Revert "Change method of using Hapi RouteOptionsApp interface"
jportner Oct 31, 2019
0a12ce6
Revert "Add route config option to avoid extending the user's session…
jportner Oct 31, 2019
268111f
Remove "extendsSession" option from session info route
jportner Oct 31, 2019
e623b04
Revamp client-side session idle timeout
jportner Oct 31, 2019
50fa1a0
Rename session timeout warning
jportner Nov 1, 2019
a12d33c
Add session lifespan warning
jportner Nov 1, 2019
7e1ae0e
Create status_page plugin for New Platform
jportner Nov 5, 2019
5f7c553
Update docs
jportner Nov 5, 2019
3f989ca
Include provider name when calling logout endpoint
jportner Nov 7, 2019
9e52cbd
Update session maxExpiration when server config changes
jportner Nov 14, 2019
0a0bd39
Tweak BroadcastChannel behavior
jportner Nov 16, 2019
a8661f9
Merge branch 'master' of github.com:elastic/kibana into issue-18566-a…
jportner Nov 20, 2019
62ba43b
Address review comments
jportner Nov 18, 2019
ab2b39c
Fix i18n test
jportner Nov 20, 2019
3175a6f
Merge branch 'master' of github.com:elastic/kibana into issue-18566-a…
jportner Nov 21, 2019
c128fd5
Prevent flashing lifespan warning
jportner Nov 21, 2019
6862001
Merge branch 'master' of github.com:elastic/kibana into issue-18566-a…
jportner Nov 25, 2019
8b6d1fd
Address 2nd round of review comments
jportner Nov 25, 2019
2649e5a
Fix basePath (tenant) issue
jportner Nov 25, 2019
55d7f21
Merge branch 'master' of github.com:elastic/kibana into issue-18566-a…
jportner Nov 26, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/settings/security-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set
this to `true` if SSL is configured outside of {kib} (for example, you are
routing requests through a load balancer or proxy).

`xpack.security.sessionTimeout`::
`xpack.security.session.idleTimeout`::
Sets the session duration (in milliseconds). By default, sessions stay active
until the browser is closed. When this is set to an explicit timeout, closing the
browser still requires the user to log back in to {kib}.
until the browser is closed. When this is set to an explicit idle timeout, closing
the browser still requires the user to log back in to {kib}.

`xpack.security.session.lifespan`::
Sets the maximum duration (in milliseconds), also known as "absolute timeout". By
default, a session can be renewed indefinitely. When this value is set, a session
will end once its lifespan is exceeded, even if the user is not idle. Note, if
jportner marked this conversation as resolved.
Show resolved Hide resolved
`idleTimeout` is not set, this setting will still cause sessions to expire.
7 changes: 4 additions & 3 deletions docs/user/security/authentication/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ The following sections apply both to <<saml>> and <<oidc>>

Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens
that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider
for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout`
setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie
can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie.
for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.session.idleTimeout`
and `xpack.security.session.lifespan` settings, and the user is automatically logged out if the session expires. An access token
jportner marked this conversation as resolved.
Show resolved Hide resolved
that is stored in the session cookie can expire, in which case {kib} will automatically renew it with a one-time-use refresh
token and store it in the same cookie.

{kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access
and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and
Expand Down
27 changes: 21 additions & 6 deletions docs/user/security/securing-kibana.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters"
For more information, see <<security-settings-kb,Security Settings in {kib}>>.
--

. Optional: Change the default session duration. By default, sessions stay
active until the browser is closed. To change the duration, set the
`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file.
The timeout is specified in milliseconds. For example, set the timeout to 600000
to expire sessions after 10 minutes:
. Optional: Set a timeout to expire idle sessions. By default, a session stays
active until the browser is closed. To define a sliding session expiration, set
the `xpack.security.session.idleTimeout` property in the `kibana.yml`
configuration file. The idle timeout is specified in milliseconds. For example,
set the idle timeout to 600000 to expire idle sessions after 10 minutes:
+
--
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.sessionTimeout: 600000
xpack.security.session.idleTimeout: 600000
--------------------------------------------------------------------------------
--

. Optional: Change the maximum session duration or "lifespan" -- also known as
the "absolute timeout". By default, a session stays active until the browser is
closed; also, if an idle timeout is defined, a session can still be extended
jportner marked this conversation as resolved.
Show resolved Hide resolved
indefinitely. To define a maximum session lifespan, set the
`xpack.security.session.lifespan` property in the `kibana.yml` configuration
file. The lifespan is specified in milliseconds. For example, set the lifespan
to 28800000 to expire sessions after 8 hours:
+
--
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.session.lifespan: 28800000
--------------------------------------------------------------------------------
--

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"bluebird": "3.5.5",
"boom": "^7.2.0",
"brace": "0.11.1",
"broadcast-channel": "^2.3.2",
jportner marked this conversation as resolved.
Show resolved Hide resolved
"cache-loader": "^4.1.0",
"chalk": "^2.4.2",
"check-disk-space": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ kibana_vars=(
xpack.security.enabled
xpack.security.encryptionKey
xpack.security.secureCookies
xpack.security.sessionTimeout
jportner marked this conversation as resolved.
Show resolved Hide resolved
xpack.security.session.timeout
jportner marked this conversation as resolved.
Show resolved Hide resolved
xpack.security.session.lifespan
telemetry.enabled
telemetry.sendUsageFrom
)
Expand Down
5 changes: 5 additions & 0 deletions src/legacy/core_plugins/status_page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export default function (kibana) {
hidden: true,
url: '/status',
},
injectDefaultVars(server) {
return {
isStatusPageAnonymous: server.config().get('status.allowAnonymous'),
};
}
}
});
}
6 changes: 6 additions & 0 deletions src/plugins/status_page/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "status_page",
"version": "kibana",
"server": false,
"ui": true
}
24 changes: 24 additions & 0 deletions src/plugins/status_page/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { PluginInitializer } from 'kibana/public';
import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin';

export const plugin: PluginInitializer<StatusPagePluginSetup, StatusPagePluginStart> = () =>
new StatusPagePlugin();
39 changes: 39 additions & 0 deletions src/plugins/status_page/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Plugin, CoreStart } from 'kibana/public';

export class StatusPagePlugin implements Plugin<StatusPagePluginSetup, StatusPagePluginStart> {
public setup() {}

public start(core: CoreStart) {
const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar(
'isStatusPageAnonymous'
) as boolean;

if (isStatusPageAnonymous) {
core.http.anonymousPaths.register('/status');
}
}

public stop() {}
}

export type StatusPagePluginSetup = ReturnType<StatusPagePlugin['setup']>;
export type StatusPagePluginStart = ReturnType<StatusPagePlugin['start']>;
13 changes: 10 additions & 3 deletions x-pack/legacy/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export const security = (kibana) => new kibana.Plugin({
enabled: Joi.boolean().default(true),
cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
session: Joi.object({
idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
jportner marked this conversation as resolved.
Show resolved Hide resolved
lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
}).default(),
secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
authorization: Joi.object({
legacyFallback: Joi.object({
Expand All @@ -43,9 +46,10 @@ export const security = (kibana) => new kibana.Plugin({
}).default();
},

deprecations: function ({ unused }) {
deprecations: function ({ rename, unused }) {
return [
unused('authorization.legacyFallback.enabled'),
rename('sessionTimeout', 'session.idleTimeout'),
];
},

Expand Down Expand Up @@ -88,7 +92,10 @@ export const security = (kibana) => new kibana.Plugin({

return {
secureCookies: securityPlugin.__legacyCompat.config.secureCookies,
sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout,
session: {
idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout,
lifespan: securityPlugin.__legacyCompat.config.session.lifespan,
},
enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'),
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ import { isSystemApiRequest } from 'ui/system_api';
import { Path } from 'plugins/xpack_main/services/path';
import { npSetup } from 'ui/new_platform';

/**
* Client session timeout is decreased by this number so that Kibana server
* can still access session content during logout request to properly clean
* user session up (invalidate access tokens, redirect to logout portal etc.).
* @type {number}
*/

const module = uiModules.get('security', []);
module.config(($httpProvider) => {
$httpProvider.interceptors.push((
Expand All @@ -28,7 +21,7 @@ module.config(($httpProvider) => {
function interceptorFactory(responseHandler) {
return function interceptor(response) {
if (!isUnauthenticated && !isSystemApiRequest(response.config)) {
jportner marked this conversation as resolved.
Show resolved Hide resolved
npSetup.plugins.security.sessionTimeout.extend();
npSetup.plugins.security.sessionTimeout.extend(response.config.url);
}
return responseHandler(response);
};
Expand Down
3 changes: 3 additions & 0 deletions x-pack/legacy/plugins/security/public/views/login/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const messageMap = {
SESSION_EXPIRED: i18n.translate('xpack.security.login.sessionExpiredDescription', {
defaultMessage: 'Your session has timed out. Please log in again.',
}),
SESSION_ENDED: i18n.translate('xpack.security.login.sessionEndedDescription', {
defaultMessage: 'Your session has exceeded the maximum time limit. Please log in again.',
jportner marked this conversation as resolved.
Show resolved Hide resolved
}),
LOGGED_OUT: i18n.translate('xpack.security.login.loggedOutDescription', {
defaultMessage: 'You have logged out of Kibana.',
}),
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
"requiredPlugins": ["features", "licensing"],
"requiredPlugins": ["features", "licensing", "status_page"],
jportner marked this conversation as resolved.
Show resolved Hide resolved
"server": true,
"ui": true
}
19 changes: 9 additions & 10 deletions x-pack/plugins/security/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,29 @@ import {
} from './session';

export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPluginStart> {
private sessionTimeout!: SessionTimeout;

public setup(core: CoreSetup) {
const { http, notifications, injectedMetadata } = core;
const { http, notifications } = core;
const { basePath, anonymousPaths } = http;
anonymousPaths.register('/login');
anonymousPaths.register('/logout');
anonymousPaths.register('/logged_out');

const sessionExpired = new SessionExpired(basePath);
http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths));
const sessionTimeout = new SessionTimeout(
injectedMetadata.getInjectedVar('sessionTimeout', null) as number | null,
notifications,
sessionExpired,
http
);
http.intercept(new SessionTimeoutHttpInterceptor(sessionTimeout, anonymousPaths));
this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http);
http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths));

return {
anonymousPaths,
sessionTimeout,
sessionTimeout: this.sessionTimeout,
};
}

public start() {}
public start() {
this.sessionTimeout.init();
jportner marked this conversation as resolved.
Show resolved Hide resolved
}
}

export type SecurityPluginSetup = ReturnType<SecurityPlugin['setup']>;
Expand Down
27 changes: 25 additions & 2 deletions x-pack/plugins/security/public/session/session_expired.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
import { coreMock } from 'src/core/public/mocks';
import { SessionExpired } from './session_expired';

Object.defineProperty(window, 'sessionStorage', {
value: {
getItem: jest.fn(() => null),
},
writable: true,
});

const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url);

it('redirects user to "/logout" when there is no basePath', async () => {
Expand All @@ -19,14 +26,30 @@ it('redirects user to "/logout" when there is no basePath', async () => {
});
});

sessionExpired.logout();
sessionExpired.logout(false);

const url = await newUrlPromise;
expect(url).toBe(
`/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED`
);
});

it('redirects user with correct message when session lifespan is exceeded', async () => {
const { basePath } = coreMock.createSetup().http;
mockCurrentUrl('/foo/bar?baz=quz#quuz');
const sessionExpired = new SessionExpired(basePath);
const newUrlPromise = new Promise<string>(resolve => {
jest.spyOn(window.location, 'assign').mockImplementation(url => {
resolve(url);
});
});

sessionExpired.logout(true);

const url = await newUrlPromise;
expect(url).toBe(`/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_ENDED`);
});

it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => {
const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http;
mockCurrentUrl('/foo/bar?baz=quz#quuz');
Expand All @@ -37,7 +60,7 @@ it('redirects user to "/${basePath}/logout" and removes basePath from next param
});
});

sessionExpired.logout();
sessionExpired.logout(false);

const url = await newUrlPromise;
expect(url).toBe(
Expand Down
9 changes: 6 additions & 3 deletions x-pack/plugins/security/public/session/session_expired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
import { HttpSetup } from 'src/core/public';

export interface ISessionExpired {
logout(): void;
logout(isMaximum: boolean): void;
jportner marked this conversation as resolved.
Show resolved Hide resolved
}

export class SessionExpired {
constructor(private basePath: HttpSetup['basePath']) {}

logout() {
logout(isMaximum: boolean) {
const next = this.basePath.remove(
`${window.location.pathname}${window.location.search}${window.location.hash}`
);
const msg = isMaximum ? 'SESSION_ENDED' : 'SESSION_EXPIRED';
const providerName = sessionStorage.getItem('session.provider');
const provider = providerName ? `&provider=${providerName}` : '';
jportner marked this conversation as resolved.
Show resolved Hide resolved
window.location.assign(
this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`)
this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=${msg}${provider}`)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
*/
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { SessionTimeoutWarning } from './session_timeout_warning';
import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning';

describe('SessionTimeoutWarning', () => {
describe('SessionIdleTimeoutWarning', () => {
it('fires its callback when the OK button is clicked', () => {
const handler = jest.fn();
const wrapper = mountWithIntl(<SessionTimeoutWarning onRefreshSession={handler} />);
const wrapper = mountWithIntl(<SessionIdleTimeoutWarning onRefreshSession={handler} />);

expect(handler).toBeCalledTimes(0);
wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click');
Expand Down
Loading