Change session timeout values to use duration instead of number (#52520)

This commit is contained in:
Joe Portner 2019-12-13 13:58:40 -05:00 committed by GitHub
parent ec2134d221
commit 3e6270737a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 74 additions and 92 deletions

View file

@ -50,15 +50,17 @@ this to `true` if SSL is configured outside of {kib} (for example, you are
routing requests through a load balancer or proxy).
`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 idle timeout, closing
the browser still requires the user to log back in to {kib}.
Sets the session duration. The format is a string of `<count>[ms|s|m|h|d|w|M|Y]`
(e.g. '70ms', '5s', '3d', '1Y'). By default, sessions stay active 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
`idleTimeout` is not set, this setting will still cause sessions to expire.
Sets the maximum duration, also known as "absolute timeout". The format is a
string of `<count>[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). 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 `idleTimeout`
is not set, this setting will still cause sessions to expire.
`xpack.security.loginAssistanceMessage`::
Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc.

View file

@ -59,13 +59,14 @@ For more information, see <<security-settings-kb,Security Settings in {kib}>>.
. 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:
configuration file. The idle timeout is formatted as a duration of
`<count>[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). For example, set
the idle timeout to expire idle sessions after 10 minutes:
+
--
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.session.idleTimeout: 600000
xpack.security.session.idleTimeout: "10m"
--------------------------------------------------------------------------------
--
@ -74,13 +75,14 @@ the "absolute timeout". By default, a session stays active until the browser is
closed. If an idle timeout is defined, a session can still be extended
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:
file. The lifespan is formatted as a duration of `<count>[ms|s|m|h|d|w|M|Y]`
(e.g. '70ms', '5s', '3d', '1Y'). For example, set the lifespan to expire
sessions after 8 hours:
+
--
[source,yaml]
--------------------------------------------------------------------------------
xpack.security.session.lifespan: 28800000
xpack.security.session.lifespan: "8h"
--------------------------------------------------------------------------------
--

View file

@ -21,16 +21,17 @@ export const security = (kibana) => new kibana.Plugin({
require: ['kibana', 'elasticsearch', 'xpack_main'],
config(Joi) {
const HANDLED_IN_NEW_PLATFORM = Joi.any().description('This key is handled in the new platform security plugin ONLY');
return Joi.object({
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'),
cookieName: HANDLED_IN_NEW_PLATFORM,
encryptionKey: HANDLED_IN_NEW_PLATFORM,
session: Joi.object({
idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
idleTimeout: HANDLED_IN_NEW_PLATFORM,
lifespan: HANDLED_IN_NEW_PLATFORM,
}).default(),
secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
loginAssistanceMessage: Joi.any().description('This key is handled in the new platform security plugin ONLY'),
secureCookies: HANDLED_IN_NEW_PLATFORM,
loginAssistanceMessage: HANDLED_IN_NEW_PLATFORM,
authorization: Joi.object({
legacyFallback: Joi.object({
enabled: Joi.boolean().default(true) // deprecated
@ -39,7 +40,7 @@ export const security = (kibana) => new kibana.Plugin({
audit: Joi.object({
enabled: Joi.boolean().default(false)
}).default(),
authc: Joi.any().description('This key is handled in the new platform security plugin ONLY')
authc: HANDLED_IN_NEW_PLATFORM
}).default();
},
@ -91,8 +92,6 @@ export const security = (kibana) => new kibana.Plugin({
secureCookies: securityPlugin.__legacyCompat.config.secureCookies,
session: {
tenant: server.newPlatform.setup.core.http.basePath.serverBasePath,
idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout,
lifespan: securityPlugin.__legacyCompat.config.session.lifespan,
},
enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'),
};

View file

@ -7,6 +7,7 @@
jest.mock('./providers/basic', () => ({ BasicAuthenticationProvider: jest.fn() }));
import Boom from 'boom';
import { duration, Duration } from 'moment';
import { SessionStorage } from '../../../../../src/core/server';
import {
@ -439,7 +440,7 @@ describe('Authenticator', () => {
// Create new authenticator with non-null session `idleTimeout`.
mockOptions = getMockOptions({
session: {
idleTimeout: 3600 * 24,
idleTimeout: duration(3600 * 24),
lifespan: null,
},
authc: { providers: ['basic'], oidc: {}, saml: {} },
@ -478,8 +479,8 @@ describe('Authenticator', () => {
// Create new authenticator with non-null session `idleTimeout` and `lifespan`.
mockOptions = getMockOptions({
session: {
idleTimeout: hr * 2,
lifespan: hr * 8,
idleTimeout: duration(hr * 2),
lifespan: duration(hr * 8),
},
authc: { providers: ['basic'], oidc: {}, saml: {} },
});
@ -520,7 +521,7 @@ describe('Authenticator', () => {
const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf();
async function createAndUpdateSession(
lifespan: number | null,
lifespan: Duration | null,
oldExpiration: number | null,
newExpiration: number | null
) {
@ -564,7 +565,7 @@ describe('Authenticator', () => {
}
it('does not change a non-null lifespan expiration when configured to non-null value.', async () => {
await createAndUpdateSession(hr * 8, 1234, 1234);
await createAndUpdateSession(duration(hr * 8), 1234, 1234);
});
it('does not change a null lifespan expiration when configured to null value.', async () => {
await createAndUpdateSession(null, null, null);
@ -573,7 +574,7 @@ describe('Authenticator', () => {
await createAndUpdateSession(null, 1234, null);
});
it('does change a null lifespan expiration when configured to non-null value', async () => {
await createAndUpdateSession(hr * 8, null, currentDate + hr * 8);
await createAndUpdateSession(duration(hr * 8), null, currentDate + hr * 8);
});
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Duration } from 'moment';
import {
SessionStorageFactory,
SessionStorage,
@ -172,12 +173,12 @@ export class Authenticator {
/**
* Session timeout in ms. If `null` session will stay active until the browser is closed.
*/
private readonly idleTimeout: number | null = null;
private readonly idleTimeout: Duration | null = null;
/**
* Session max lifespan in ms. If `null` session may live indefinitely.
*/
private readonly lifespan: number | null = null;
private readonly lifespan: Duration | null = null;
/**
* Internal authenticator logger.
@ -225,7 +226,6 @@ export class Authenticator {
);
this.serverBasePath = this.options.basePath.serverBasePath || '/';
// only set these vars if they are defined in options (otherwise coalesce to existing/default)
this.idleTimeout = this.options.config.session.idleTimeout;
this.lifespan = this.options.config.session.lifespan;
}
@ -492,11 +492,16 @@ export class Authenticator {
private calculateExpiry(
existingSession: ProviderSession | null
): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } {
let lifespanExpiration = this.lifespan && Date.now() + this.lifespan;
if (existingSession && existingSession.lifespanExpiration && this.lifespan) {
lifespanExpiration = existingSession.lifespanExpiration;
}
const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout;
const now = Date.now();
// if we are renewing an existing session, use its `lifespanExpiration` -- otherwise, set this value
// based on the configured server `lifespan`.
// note, if the server had a `lifespan` set and then removes it, remove `lifespanExpiration` on renewed sessions
// also, if the server did not have a `lifespan` set and then adds it, add `lifespanExpiration` on renewed sessions
const lifespanExpiration =
existingSession?.lifespanExpiration && this.lifespan
? existingSession.lifespanExpiration
: this.lifespan && now + this.lifespan.asMilliseconds();
const idleTimeoutExpiration = this.idleTimeout && now + this.idleTimeout.asMilliseconds();
return { idleTimeoutExpiration, lifespanExpiration };
}

View file

@ -60,6 +60,10 @@ describe('setupAuthentication()', () => {
coreMock.createPluginInitializerContext({
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
session: {
idleTimeout: null,
lifespan: null,
},
cookieName: 'my-sid-cookie',
authc: { providers: ['basic'] },
}),
@ -87,7 +91,6 @@ describe('setupAuthentication()', () => {
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
cookieName: 'my-sid-cookie',
authc: { providers: ['basic'] },
};
await setupAuthentication(mockSetupAuthenticationParams);

View file

@ -254,19 +254,22 @@ describe('config schema', () => {
});
describe('createConfig$()', () => {
const mockAndCreateConfig = async (isTLSEnabled: boolean, value = {}, context?: any) => {
const contextMock = coreMock.createPluginInitializerContext(
// we must use validate to avoid errors in `createConfig$`
ConfigSchema.validate(value, context)
);
return await createConfig$(contextMock, isTLSEnabled)
.pipe(first())
.toPromise()
.then(config => ({ contextMock, config }));
};
it('should log a warning and set xpack.security.encryptionKey if not set', async () => {
const mockRandomBytes = jest.requireMock('crypto').randomBytes;
mockRandomBytes.mockReturnValue('ab'.repeat(16));
const contextMock = coreMock.createPluginInitializerContext({});
const config = await createConfig$(contextMock, true)
.pipe(first())
.toPromise();
expect(config).toEqual({
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
session: { idleTimeout: null, lifespan: null },
});
const { contextMock, config } = await mockAndCreateConfig(true, {}, { dist: true });
expect(config.encryptionKey).toEqual('ab'.repeat(16));
expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
Array [
@ -278,14 +281,7 @@ describe('createConfig$()', () => {
});
it('should log a warning if SSL is not configured', async () => {
const contextMock = coreMock.createPluginInitializerContext({
encryptionKey: 'a'.repeat(32),
secureCookies: false,
});
const config = await createConfig$(contextMock, false)
.pipe(first())
.toPromise();
const { contextMock, config } = await mockAndCreateConfig(false, {});
expect(config.secureCookies).toEqual(false);
expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
@ -298,14 +294,7 @@ describe('createConfig$()', () => {
});
it('should log a warning if SSL is not configured yet secure cookies are being used', async () => {
const contextMock = coreMock.createPluginInitializerContext({
encryptionKey: 'a'.repeat(32),
secureCookies: true,
});
const config = await createConfig$(contextMock, false)
.pipe(first())
.toPromise();
const { contextMock, config } = await mockAndCreateConfig(false, { secureCookies: true });
expect(config.secureCookies).toEqual(true);
expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
@ -318,14 +307,7 @@ describe('createConfig$()', () => {
});
it('should set xpack.security.secureCookies if SSL is configured', async () => {
const contextMock = coreMock.createPluginInitializerContext({
encryptionKey: 'a'.repeat(32),
secureCookies: false,
});
const config = await createConfig$(contextMock, true)
.pipe(first())
.toPromise();
const { contextMock, config } = await mockAndCreateConfig(true, {});
expect(config.secureCookies).toEqual(true);
expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]);

View file

@ -8,6 +8,7 @@ import crypto from 'crypto';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { schema, Type, TypeOf } from '@kbn/config-schema';
import { duration } from 'moment';
import { PluginInitializerContext } from '../../../../src/core/server';
export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer P>
@ -34,10 +35,10 @@ export const ConfigSchema = schema.object(
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED
sessionTimeout: schema.maybe(schema.nullable(schema.number())), // DEPRECATED
session: schema.object({
idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }),
lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }),
idleTimeout: schema.nullable(schema.duration()),
lifespan: schema.nullable(schema.duration()),
}),
secureCookies: schema.boolean({ defaultValue: false }),
authc: schema.object({
@ -90,17 +91,16 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b
// "sessionTimeout" is deprecated and replaced with "session.idleTimeout"
// however, NP does not yet have a mechanism to automatically rename deprecated keys
// for the time being, we'll do it manually:
const sess = config.session;
const session = {
idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null,
lifespan: (sess && sess.lifespan) || null,
};
const deprecatedSessionTimeout =
typeof config.sessionTimeout === 'number' ? duration(config.sessionTimeout) : null;
const val = {
...config,
encryptionKey,
secureCookies,
session,
session: {
...config.session,
idleTimeout: config.session.idleTimeout || deprecatedSessionTimeout,
},
};
delete val.sessionTimeout; // DEPRECATED
return val;

View file

@ -52,10 +52,6 @@ describe('Security Plugin', () => {
"cookieName": "sid",
"loginAssistanceMessage": undefined,
"secureCookies": true,
"session": Object {
"idleTimeout": 1500,
"lifespan": null,
},
},
"license": Object {
"getFeatures": [Function],

View file

@ -72,10 +72,6 @@ export interface PluginSetupContract {
registerPrivilegesWithCluster: () => void;
license: SecurityLicense;
config: RecursiveReadonly<{
session: {
idleTimeout: number | null;
lifespan: number | null;
};
secureCookies: boolean;
cookieName: string;
loginAssistanceMessage: string;
@ -209,10 +205,6 @@ export class Plugin {
// exception may be `sessionTimeout` as other parts of the app may want to know it.
config: {
loginAssistanceMessage: config.loginAssistanceMessage,
session: {
idleTimeout: config.session.idleTimeout,
lifespan: config.session.lifespan,
},
secureCookies: config.secureCookies,
cookieName: config.cookieName,
},