mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[7.x] Add support for provider specific session timeout settings. (#82855)
This commit is contained in:
parent
7552c17763
commit
0deed8eb29
30 changed files with 1370 additions and 267 deletions
|
@ -92,8 +92,8 @@ The valid settings in the `xpack.security.authc.providers` namespace vary depend
|
|||
`<provider-type>.<provider-name>.icon` {ess-icon}
|
||||
| Custom icon for the provider entry displayed on the Login Selector UI.
|
||||
|
||||
| `xpack.security.authc.providers.`
|
||||
`<provider-type>.<provider-name>.showInSelector` {ess-icon}
|
||||
| `xpack.security.authc.providers.<provider-type>.`
|
||||
`<provider-name>.showInSelector` {ess-icon}
|
||||
| Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain.
|
||||
|
||||
2+a|
|
||||
|
@ -103,10 +103,31 @@ The valid settings in the `xpack.security.authc.providers` namespace vary depend
|
|||
You are unable to set this setting to `false` for `basic` and `token` authentication providers.
|
||||
============
|
||||
|
||||
| `xpack.security.authc.providers.`
|
||||
`<provider-type>.<provider-name>.accessAgreement.message` {ess-icon}
|
||||
| `xpack.security.authc.providers.<provider-type>.`
|
||||
`<provider-name>.accessAgreement.message` {ess-icon}
|
||||
| Access agreement text in Markdown format. For more information, refer to <<xpack-security-access-agreement>>.
|
||||
|
||||
| [[xpack-security-provider-session-idleTimeout]] `xpack.security.authc.providers.<provider-type>.`
|
||||
`<provider-name>.session.idleTimeout` {ess-icon}
|
||||
| Ensures that user sessions will expire after a period of inactivity. Setting this to `0` will prevent sessions from expiring because of inactivity. By default, this setting is equal to <<xpack-session-idleTimeout, `xpack.security.session.idleTimeout`>>.
|
||||
|
||||
2+a|
|
||||
[TIP]
|
||||
============
|
||||
Use a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
============
|
||||
|
||||
| [[xpack-security-provider-session-lifespan]] `xpack.security.authc.providers.<provider-type>.`
|
||||
`<provider-name>.session.lifespan` {ess-icon}
|
||||
| Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If
|
||||
this is set to `0`, user sessions could stay active indefinitely. By default, this setting is equal to <<xpack-session-lifespan, `xpack.security.session.lifespan`>>.
|
||||
|
||||
2+a|
|
||||
[TIP]
|
||||
============
|
||||
Use a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
============
|
||||
|
||||
|===
|
||||
|
||||
[float]
|
||||
|
@ -210,32 +231,32 @@ You can configure the following settings in the `kibana.yml` file.
|
|||
|
||||
|[[xpack-session-idleTimeout]] `xpack.security.session.idleTimeout` {ess-icon}
|
||||
| Ensures that user sessions will expire after a period of inactivity. This and <<xpack-session-lifespan,`xpack.security.session.lifespan`>> are both
|
||||
highly recommended. By default, this setting is not set.
|
||||
highly recommended. You can also specify this setting for <<xpack-security-provider-session-idleTimeout, every provider separately>>. If this is _not_ set or set to `0`, then sessions will never expire due to inactivity. By default, this setting is not set.
|
||||
|
||||
2+a|
|
||||
[TIP]
|
||||
============
|
||||
The format is a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
Use a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
============
|
||||
|
||||
|[[xpack-session-lifespan]] `xpack.security.session.lifespan` {ess-icon}
|
||||
| Ensures that user sessions will expire after the defined time period. This behavior also known as an "absolute timeout". If
|
||||
this is _not_ set, user sessions could stay active indefinitely. This and <<xpack-session-idleTimeout, `xpack.security.session.idleTimeout`>> are both highly
|
||||
recommended. By default, this setting is not set.
|
||||
| Ensures that user sessions will expire after the defined time period. This behavior is also known as an "absolute timeout". If
|
||||
this is _not_ set or set to `0`, user sessions could stay active indefinitely. This and <<xpack-session-idleTimeout, `xpack.security.session.idleTimeout`>> are both highly
|
||||
recommended. You can also specify this setting for <<xpack-security-provider-session-lifespan, every provider separately>>. By default, this setting is not set.
|
||||
|
||||
2+a|
|
||||
[TIP]
|
||||
============
|
||||
The format is a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
Use a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
============
|
||||
|
||||
| `xpack.security.session.cleanupInterval`
|
||||
| `xpack.security.session.cleanupInterval` {ess-icon}
|
||||
| Sets the interval at which {kib} tries to remove expired and invalid sessions from the session index. By default, this value is 1 hour. The minimum value is 10 seconds.
|
||||
|
||||
2+a|
|
||||
[TIP]
|
||||
============
|
||||
The format is a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
Use a string of `<count>[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w').
|
||||
============
|
||||
|
||||
|===
|
||||
|
|
|
@ -15,7 +15,7 @@ export function mockAuthenticatedUser(user: Partial<AuthenticatedUser> = {}) {
|
|||
enabled: true,
|
||||
authentication_realm: { name: 'native1', type: 'native' },
|
||||
lookup_realm: { name: 'native1', type: 'native' },
|
||||
authentication_provider: 'basic1',
|
||||
authentication_provider: { type: 'basic', name: 'basic1' },
|
||||
authentication_type: 'realm',
|
||||
...user,
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('#canUserChangePassword', () => {
|
|||
expect(
|
||||
canUserChangePassword({
|
||||
username: 'foo',
|
||||
authentication_provider: { type: 'basic', name: 'basic1' },
|
||||
authentication_realm: {
|
||||
name: 'the realm name',
|
||||
type: realm,
|
||||
|
@ -25,6 +26,7 @@ describe('#canUserChangePassword', () => {
|
|||
expect(
|
||||
canUserChangePassword({
|
||||
username: 'foo',
|
||||
authentication_provider: { type: 'the provider type', name: 'does not matter' },
|
||||
authentication_realm: {
|
||||
name: 'the realm name',
|
||||
type: 'does not matter',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import type { AuthenticationProvider } from '../types';
|
||||
import { User } from './user';
|
||||
|
||||
const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
|
||||
|
@ -28,9 +29,9 @@ export interface AuthenticatedUser extends User {
|
|||
lookup_realm: UserRealm;
|
||||
|
||||
/**
|
||||
* Name of the Kibana authentication provider that used to authenticate user.
|
||||
* The authentication provider that used to authenticate user.
|
||||
*/
|
||||
authentication_provider: string;
|
||||
authentication_provider: AuthenticationProvider;
|
||||
|
||||
/**
|
||||
* The AuthenticationType used by ES to authenticate the user.
|
||||
|
|
|
@ -10,14 +10,14 @@ import {
|
|||
ILegacyClusterClient,
|
||||
IBasePath,
|
||||
} from '../../../../../src/core/server';
|
||||
import { SecurityLicense } from '../../common/licensing';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import type { SecurityLicense } from '../../common/licensing';
|
||||
import type { AuthenticatedUser } from '../../common/model';
|
||||
import type { AuthenticationProvider } from '../../common/types';
|
||||
import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit';
|
||||
import { ConfigType } from '../config';
|
||||
import type { ConfigType } from '../config';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
|
||||
import { SessionValue, Session } from '../session_management';
|
||||
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
|
||||
import type { SessionValue, Session } from '../session_management';
|
||||
|
||||
import {
|
||||
AuthenticationProviderOptions,
|
||||
|
@ -261,7 +261,7 @@ export class Authenticator {
|
|||
isLoginAttemptWithProviderName(attempt) && this.providers.has(attempt.provider.name)
|
||||
? [[attempt.provider.name, this.providers.get(attempt.provider.name)!]]
|
||||
: isLoginAttemptWithProviderType(attempt)
|
||||
? [...this.providerIterator(existingSessionValue)].filter(
|
||||
? [...this.providerIterator(existingSessionValue?.provider.name)].filter(
|
||||
([, { type }]) => type === attempt.provider.type
|
||||
)
|
||||
: [];
|
||||
|
@ -340,7 +340,9 @@ export class Authenticator {
|
|||
);
|
||||
}
|
||||
|
||||
for (const [providerName, provider] of this.providerIterator(existingSessionValue)) {
|
||||
for (const [providerName, provider] of this.providerIterator(
|
||||
existingSessionValue?.provider.name
|
||||
)) {
|
||||
// Check if current session has been set by this provider.
|
||||
const ownsSession =
|
||||
existingSessionValue?.provider.name === providerName &&
|
||||
|
@ -397,7 +399,7 @@ export class Authenticator {
|
|||
// active session already some providers can still properly respond to the 3rd-party logout
|
||||
// request. For example SAML provider can process logout request encoded in `SAMLRequest`
|
||||
// query string parameter.
|
||||
for (const [, provider] of this.providerIterator(null)) {
|
||||
for (const [, provider] of this.providerIterator()) {
|
||||
const deauthenticationResult = await provider.logout(request);
|
||||
if (!deauthenticationResult.notHandled()) {
|
||||
return deauthenticationResult;
|
||||
|
@ -475,22 +477,22 @@ export class Authenticator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns provider iterator where providers are sorted in the order of priority (based on the session ownership).
|
||||
* @param sessionValue Current session value.
|
||||
* Returns provider iterator starting from the suggested provider if any.
|
||||
* @param suggestedProviderName Optional name of the provider to return first.
|
||||
*/
|
||||
private *providerIterator(
|
||||
sessionValue: SessionValue | null
|
||||
suggestedProviderName?: string | null
|
||||
): IterableIterator<[string, BaseAuthenticationProvider]> {
|
||||
// If there is no session to predict which provider to use first, let's use the order
|
||||
// providers are configured in. Otherwise return provider that owns session first, and only then the rest
|
||||
// If there is no provider suggested or suggested provider isn't configured, let's use the order
|
||||
// providers are configured in. Otherwise return suggested provider first, and only then the rest
|
||||
// of providers.
|
||||
if (!sessionValue) {
|
||||
if (!suggestedProviderName || !this.providers.has(suggestedProviderName)) {
|
||||
yield* this.providers;
|
||||
} else {
|
||||
yield [sessionValue.provider.name, this.providers.get(sessionValue.provider.name)!];
|
||||
yield [suggestedProviderName, this.providers.get(suggestedProviderName)!];
|
||||
|
||||
for (const [providerName, provider] of this.providers) {
|
||||
if (providerName !== sessionValue.provider.name) {
|
||||
if (providerName !== suggestedProviderName) {
|
||||
yield [providerName, provider];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ export abstract class BaseAuthenticationProvider {
|
|||
...(await this.options.client
|
||||
.asScoped({ headers: { ...request.headers, ...authHeaders } })
|
||||
.callAsCurrentUser('shield.authenticate')),
|
||||
authentication_provider: this.options.name,
|
||||
authentication_provider: { type: this.type, name: this.options.name },
|
||||
} as AuthenticatedUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,7 +136,10 @@ describe('HTTPAuthenticationProvider', () => {
|
|||
});
|
||||
|
||||
await expect(provider.authenticate(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded({ ...user, authentication_provider: 'http' })
|
||||
AuthenticationResult.succeeded({
|
||||
...user,
|
||||
authentication_provider: { type: 'http', name: 'http' },
|
||||
})
|
||||
);
|
||||
|
||||
expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } });
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
|
||||
await expect(operation(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'kerberos' },
|
||||
{ ...user, authentication_provider: { type: 'kerberos', name: 'kerberos' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer some-token' },
|
||||
state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' },
|
||||
|
@ -164,7 +164,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
|
||||
await expect(operation(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'kerberos' },
|
||||
{ ...user, authentication_provider: { type: 'kerberos', name: 'kerberos' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer some-token' },
|
||||
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' },
|
||||
|
@ -361,7 +361,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'kerberos' },
|
||||
{ ...user, authentication_provider: { type: 'kerberos', name: 'kerberos' } },
|
||||
{ authHeaders: { authorization } }
|
||||
)
|
||||
);
|
||||
|
@ -401,7 +401,7 @@ describe('KerberosAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'kerberos' },
|
||||
{ ...user, authentication_provider: { type: 'kerberos', name: 'kerberos' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer newfoo' },
|
||||
state: { accessToken: 'newfoo', refreshToken: 'newbar' },
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('OIDCAuthenticationProvider', () => {
|
|||
mockOptions = mockAuthenticationProviderOptions({ name: 'oidc' });
|
||||
|
||||
mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
|
||||
mockUser = mockAuthenticatedUser({ authentication_provider: 'oidc' });
|
||||
mockUser = mockAuthenticatedUser({ authentication_provider: { type: 'oidc', name: 'oidc' } });
|
||||
mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method) => {
|
||||
if (method === 'shield.authenticate') {
|
||||
return mockUser;
|
||||
|
|
|
@ -127,7 +127,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
|
||||
await expect(operation(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'pki' },
|
||||
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer access-token' },
|
||||
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
|
||||
|
@ -169,7 +169,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
|
||||
await expect(operation(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'pki' },
|
||||
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer access-token' },
|
||||
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
|
||||
|
@ -356,7 +356,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, state)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'pki' },
|
||||
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer access-token' },
|
||||
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
|
||||
|
@ -405,7 +405,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, state)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'pki' },
|
||||
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer access-token' },
|
||||
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
|
||||
|
@ -486,7 +486,7 @@ describe('PKIAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, state)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'pki' },
|
||||
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
|
||||
{ authHeaders: { authorization: `Bearer ${state.accessToken}` } }
|
||||
)
|
||||
);
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
mockOptions = mockAuthenticationProviderOptions({ name: 'saml' });
|
||||
|
||||
mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
|
||||
mockUser = mockAuthenticatedUser({ authentication_provider: 'saml' });
|
||||
mockUser = mockAuthenticatedUser({ authentication_provider: { type: 'saml', name: 'saml' } });
|
||||
mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method) => {
|
||||
if (method === 'shield.authenticate') {
|
||||
return mockUser;
|
||||
|
@ -542,7 +542,9 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
for (const [description, response] of [
|
||||
[
|
||||
'current session is valid',
|
||||
Promise.resolve(mockAuthenticatedUser({ authentication_provider: 'saml' })),
|
||||
Promise.resolve(
|
||||
mockAuthenticatedUser({ authentication_provider: { type: 'saml', name: 'saml' } })
|
||||
),
|
||||
],
|
||||
[
|
||||
'current session is is expired',
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.login(request, credentials)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'token' },
|
||||
{ ...user, authentication_provider: { type: 'token', name: 'token' } },
|
||||
{ authHeaders: { authorization }, state: tokenPair }
|
||||
)
|
||||
);
|
||||
|
@ -196,7 +196,7 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'token' },
|
||||
{ ...user, authentication_provider: { type: 'token', name: 'token' } },
|
||||
{ authHeaders: { authorization } }
|
||||
)
|
||||
);
|
||||
|
@ -236,7 +236,7 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded(
|
||||
{ ...user, authentication_provider: 'token' },
|
||||
{ ...user, authentication_provider: { type: 'token', name: 'token' } },
|
||||
{
|
||||
authHeaders: { authorization: 'Bearer newfoo' },
|
||||
state: { accessToken: 'newfoo', refreshToken: 'newbar' },
|
||||
|
|
|
@ -36,6 +36,10 @@ describe('config schema', () => {
|
|||
"hint": undefined,
|
||||
"icon": undefined,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": undefined,
|
||||
"lifespan": undefined,
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -55,8 +59,6 @@ describe('config schema', () => {
|
|||
"secureCookies": false,
|
||||
"session": Object {
|
||||
"cleanupInterval": "PT1H",
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
@ -83,6 +85,10 @@ describe('config schema', () => {
|
|||
"hint": undefined,
|
||||
"icon": undefined,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": undefined,
|
||||
"lifespan": undefined,
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -102,8 +108,6 @@ describe('config schema', () => {
|
|||
"secureCookies": false,
|
||||
"session": Object {
|
||||
"cleanupInterval": "PT1H",
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
@ -130,6 +134,10 @@ describe('config schema', () => {
|
|||
"hint": undefined,
|
||||
"icon": undefined,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": undefined,
|
||||
"lifespan": undefined,
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -148,8 +156,6 @@ describe('config schema', () => {
|
|||
"secureCookies": false,
|
||||
"session": Object {
|
||||
"cleanupInterval": "PT1H",
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
@ -521,6 +527,35 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 0,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: 123, lifespan: 546 } } },
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"basic": Object {
|
||||
"basic1": Object {
|
||||
"description": "Log in with Elasticsearch",
|
||||
"enabled": true,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -573,6 +608,35 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 0,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
token: { token1: { order: 0, session: { idleTimeout: 123, lifespan: 546 } } },
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"token": Object {
|
||||
"token1": Object {
|
||||
"description": "Log in with Elasticsearch",
|
||||
"enabled": true,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -611,6 +675,33 @@ describe('config schema', () => {
|
|||
"pki1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
pki: { pki1: { order: 0, session: { idleTimeout: 123, lifespan: 546 } } },
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"pki": Object {
|
||||
"pki1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -651,6 +742,33 @@ describe('config schema', () => {
|
|||
"kerberos1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
kerberos: { kerberos1: { order: 0, session: { idleTimeout: 123, lifespan: 546 } } },
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"kerberos": Object {
|
||||
"kerberos1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -696,12 +814,53 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"order": 0,
|
||||
"realm": "oidc1",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
"oidc2": Object {
|
||||
"enabled": true,
|
||||
"order": 1,
|
||||
"realm": "oidc2",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
oidc: {
|
||||
oidc1: { order: 0, realm: 'oidc1', session: { idleTimeout: 123 } },
|
||||
oidc2: { order: 1, realm: 'oidc2', session: { idleTimeout: 321, lifespan: 546 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"oidc": Object {
|
||||
"oidc1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"realm": "oidc1",
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
"oidc2": Object {
|
||||
"enabled": true,
|
||||
"order": 1,
|
||||
"realm": "oidc2",
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -751,6 +910,7 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"order": 0,
|
||||
"realm": "saml1",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
|
@ -761,6 +921,7 @@ describe('config schema', () => {
|
|||
},
|
||||
"order": 1,
|
||||
"realm": "saml2",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
|
@ -768,6 +929,65 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"order": 2,
|
||||
"realm": "saml3",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be successfully validated with session config overrides', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
authc: {
|
||||
providers: {
|
||||
saml: {
|
||||
saml1: { order: 0, realm: 'saml1', session: { idleTimeout: 123 } },
|
||||
saml2: {
|
||||
order: 1,
|
||||
realm: 'saml2',
|
||||
maxRedirectURLSize: '1kb',
|
||||
session: { idleTimeout: 321, lifespan: 546 },
|
||||
},
|
||||
saml3: { order: 2, realm: 'saml3', useRelayStateDeepLink: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}).authc.providers
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"saml": Object {
|
||||
"saml1": Object {
|
||||
"enabled": true,
|
||||
"order": 0,
|
||||
"realm": "saml1",
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
"saml2": Object {
|
||||
"enabled": true,
|
||||
"maxRedirectURLSize": ByteSizeValue {
|
||||
"valueInBytes": 1024,
|
||||
},
|
||||
"order": 1,
|
||||
"realm": "saml2",
|
||||
"session": Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": "PT0.546S",
|
||||
},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
"saml3": Object {
|
||||
"enabled": true,
|
||||
"order": 2,
|
||||
"realm": "saml3",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": true,
|
||||
},
|
||||
|
@ -835,6 +1055,7 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 0,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
"basic2": Object {
|
||||
|
@ -842,6 +1063,7 @@ describe('config schema', () => {
|
|||
"enabled": false,
|
||||
"icon": "logoElasticsearch",
|
||||
"order": 1,
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
|
@ -850,6 +1072,7 @@ describe('config schema', () => {
|
|||
"enabled": false,
|
||||
"order": 3,
|
||||
"realm": "saml3",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
|
@ -857,6 +1080,7 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"order": 1,
|
||||
"realm": "saml1",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
|
@ -864,6 +1088,7 @@ describe('config schema', () => {
|
|||
"enabled": true,
|
||||
"order": 2,
|
||||
"realm": "saml2",
|
||||
"session": Object {},
|
||||
"showInSelector": true,
|
||||
"useRelayStateDeepLink": false,
|
||||
},
|
||||
|
@ -1223,4 +1448,314 @@ describe('createConfig()', () => {
|
|||
'[audit]: xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('#getExpirationTimeouts', () => {
|
||||
function createMockConfig(config: Record<string, any> = {}) {
|
||||
return createConfig(ConfigSchema.validate(config), loggingSystemMock.createLogger(), {
|
||||
isTLSEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
it('returns default values if neither global nor provider specific settings are set', async () => {
|
||||
expect(createMockConfig().session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly handles explicitly disabled global settings', async () => {
|
||||
expect(
|
||||
createMockConfig({
|
||||
session: { idleTimeout: null, lifespan: null },
|
||||
}).session.getExpirationTimeouts({ type: 'basic', name: 'basic1' })
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
createMockConfig({
|
||||
session: { idleTimeout: 0, lifespan: 0 },
|
||||
}).session.getExpirationTimeouts({ type: 'basic', name: 'basic1' })
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('falls back to the global settings if provider does not override them', async () => {
|
||||
expect(
|
||||
createMockConfig({ session: { idleTimeout: 123 } }).session.getExpirationTimeouts({
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
createMockConfig({ session: { lifespan: 456 } }).session.getExpirationTimeouts({
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": "PT0.456S",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
createMockConfig({
|
||||
session: { idleTimeout: 123, lifespan: 456 },
|
||||
}).session.getExpirationTimeouts({ type: 'basic', name: 'basic1' })
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.456S",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('falls back to the global settings if provider is not known', async () => {
|
||||
expect(
|
||||
createMockConfig({ session: { idleTimeout: 123 } }).session.getExpirationTimeouts({
|
||||
type: 'some type',
|
||||
name: 'some name',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
createMockConfig({ session: { lifespan: 456 } }).session.getExpirationTimeouts({
|
||||
type: 'some type',
|
||||
name: 'some name',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": "PT0.456S",
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
createMockConfig({
|
||||
session: { idleTimeout: 123, lifespan: 456 },
|
||||
}).session.getExpirationTimeouts({ type: 'some type', name: 'some name' })
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.123S",
|
||||
"lifespan": "PT0.456S",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('uses provider overrides if specified (only idle timeout)', async () => {
|
||||
const configWithoutGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: 321 } } },
|
||||
saml: { saml1: { order: 1, realm: 'saml-realm', session: { idleTimeout: 332211 } } },
|
||||
},
|
||||
},
|
||||
session: { idleTimeout: null },
|
||||
});
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT5M32.211S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
|
||||
const configWithGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: 321 } } },
|
||||
saml: { saml1: { order: 1, realm: 'saml-realm', session: { idleTimeout: 332211 } } },
|
||||
},
|
||||
},
|
||||
session: { idleTimeout: 123 },
|
||||
});
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT5M32.211S",
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('uses provider overrides if specified (only lifespan)', async () => {
|
||||
const configWithoutGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { lifespan: 654 } } },
|
||||
saml: { saml1: { order: 1, realm: 'saml-realm', session: { lifespan: 665544 } } },
|
||||
},
|
||||
},
|
||||
session: { lifespan: null },
|
||||
});
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": "PT0.654S",
|
||||
}
|
||||
`);
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": "PT11M5.544S",
|
||||
}
|
||||
`);
|
||||
|
||||
const configWithGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { lifespan: 654 } } },
|
||||
saml: { saml1: { order: 1, realm: 'saml-realm', session: { idleTimeout: 665544 } } },
|
||||
},
|
||||
},
|
||||
session: { lifespan: 456 },
|
||||
});
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": "PT0.654S",
|
||||
}
|
||||
`);
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT11M5.544S",
|
||||
"lifespan": "PT0.456S",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('uses provider overrides if specified (both idle timeout and lifespan)', async () => {
|
||||
const configWithoutGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: 321, lifespan: 654 } } },
|
||||
saml: {
|
||||
saml1: {
|
||||
order: 1,
|
||||
realm: 'saml-realm',
|
||||
session: { idleTimeout: 332211, lifespan: 665544 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { idleTimeout: null, lifespan: null },
|
||||
});
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": "PT0.654S",
|
||||
}
|
||||
`);
|
||||
expect(configWithoutGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT5M32.211S",
|
||||
"lifespan": "PT11M5.544S",
|
||||
}
|
||||
`);
|
||||
|
||||
const configWithGlobal = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: 321, lifespan: 654 } } },
|
||||
saml: {
|
||||
saml1: {
|
||||
order: 1,
|
||||
realm: 'saml-realm',
|
||||
session: { idleTimeout: 332211, lifespan: 665544 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { idleTimeout: 123, lifespan: 456 },
|
||||
});
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT0.321S",
|
||||
"lifespan": "PT0.654S",
|
||||
}
|
||||
`);
|
||||
expect(configWithGlobal.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": "PT5M32.211S",
|
||||
"lifespan": "PT11M5.544S",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('uses provider overrides if disabled (both idle timeout and lifespan)', async () => {
|
||||
const config = createMockConfig({
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0, session: { idleTimeout: null, lifespan: null } } },
|
||||
saml: {
|
||||
saml1: {
|
||||
order: 1,
|
||||
realm: 'saml-realm',
|
||||
session: { idleTimeout: 0, lifespan: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { idleTimeout: 123, lifespan: 456 },
|
||||
});
|
||||
expect(config.session.getExpirationTimeouts({ type: 'basic', name: 'basic1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
expect(config.session.getExpirationTimeouts({ type: 'saml', name: 'saml1' }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"idleTimeout": null,
|
||||
"lifespan": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,24 @@
|
|||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
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';
|
||||
|
||||
export type ConfigType = ReturnType<typeof createConfig>;
|
||||
type RawConfigType = TypeOf<typeof ConfigSchema>;
|
||||
|
||||
interface ProvidersCommonConfigType {
|
||||
enabled: Type<boolean>;
|
||||
showInSelector: Type<boolean>;
|
||||
order: Type<number>;
|
||||
description?: Type<string>;
|
||||
hint?: Type<string>;
|
||||
icon?: Type<string>;
|
||||
session?: Type<{ idleTimeout?: Duration | null; lifespan?: Duration | null }>;
|
||||
}
|
||||
|
||||
const providerOptionsSchema = (providerType: string, optionsSchema: Type<any>) =>
|
||||
schema.conditional(
|
||||
|
@ -21,10 +34,6 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type<any>) =
|
|||
schema.never()
|
||||
);
|
||||
|
||||
type ProvidersCommonConfigType = Record<
|
||||
'enabled' | 'showInSelector' | 'order' | 'description' | 'hint' | 'icon',
|
||||
Type<any>
|
||||
>;
|
||||
function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonConfigType> = {}) {
|
||||
return {
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
|
@ -34,6 +43,10 @@ function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonCon
|
|||
hint: schema.maybe(schema.string()),
|
||||
icon: schema.maybe(schema.string()),
|
||||
accessAgreement: schema.maybe(schema.object({ message: schema.string() })),
|
||||
session: schema.object({
|
||||
idleTimeout: schema.maybe(schema.oneOf([schema.duration(), schema.literal(null)])),
|
||||
lifespan: schema.maybe(schema.oneOf([schema.duration(), schema.literal(null)])),
|
||||
}),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -147,8 +160,8 @@ export const ConfigSchema = schema.object({
|
|||
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
|
||||
),
|
||||
session: schema.object({
|
||||
idleTimeout: schema.nullable(schema.duration()),
|
||||
lifespan: schema.nullable(schema.duration()),
|
||||
idleTimeout: schema.maybe(schema.oneOf([schema.duration(), schema.literal(null)])),
|
||||
lifespan: schema.maybe(schema.oneOf([schema.duration(), schema.literal(null)])),
|
||||
cleanupInterval: schema.duration({
|
||||
defaultValue: '1h',
|
||||
validate(value) {
|
||||
|
@ -180,6 +193,7 @@ export const ConfigSchema = schema.object({
|
|||
hint: undefined,
|
||||
icon: undefined,
|
||||
accessAgreement: undefined,
|
||||
session: { idleTimeout: undefined, lifespan: undefined },
|
||||
},
|
||||
},
|
||||
token: undefined,
|
||||
|
@ -230,7 +244,7 @@ export const ConfigSchema = schema.object({
|
|||
});
|
||||
|
||||
export function createConfig(
|
||||
config: TypeOf<typeof ConfigSchema>,
|
||||
config: RawConfigType,
|
||||
logger: Logger,
|
||||
{ isTLSEnabled }: { isTLSEnabled: boolean }
|
||||
) {
|
||||
|
@ -319,7 +333,33 @@ export function createConfig(
|
|||
sortedProviders: Object.freeze(sortedProviders),
|
||||
http: config.authc.http,
|
||||
},
|
||||
session: getSessionConfig(config.session, providers),
|
||||
encryptionKey,
|
||||
secureCookies,
|
||||
};
|
||||
}
|
||||
|
||||
function getSessionConfig(session: RawConfigType['session'], providers: ProvidersConfigType) {
|
||||
return {
|
||||
cleanupInterval: session.cleanupInterval,
|
||||
getExpirationTimeouts({ type, name }: AuthenticationProvider) {
|
||||
// Both idle timeout and lifespan from the provider specific session config can have three
|
||||
// possible types of values: `Duration`, `null` and `undefined`. The `undefined` type means that
|
||||
// provider doesn't override session config and we should fall back to the global one instead.
|
||||
const providerSessionConfig = providers[type as keyof ProvidersConfigType]?.[name]?.session;
|
||||
|
||||
const [idleTimeout, lifespan] = [
|
||||
[session.idleTimeout, providerSessionConfig?.idleTimeout],
|
||||
[session.lifespan, providerSessionConfig?.lifespan],
|
||||
].map(([globalTimeout, providerTimeout]) => {
|
||||
const timeout = providerTimeout === undefined ? globalTimeout ?? null : providerTimeout;
|
||||
return timeout && timeout.asMilliseconds() > 0 ? timeout : null;
|
||||
});
|
||||
|
||||
return {
|
||||
idleTimeout,
|
||||
lifespan,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@ describe('Change password', () => {
|
|||
it('successfully changes own password if provided old password is correct for non-basic provider.', async () => {
|
||||
const mockUser = mockAuthenticatedUser({
|
||||
username: 'user',
|
||||
authentication_provider: 'token1',
|
||||
authentication_provider: { type: 'token', name: 'token1' },
|
||||
});
|
||||
authc.getCurrentUser.mockReturnValue(mockUser);
|
||||
authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockUser));
|
||||
|
|
|
@ -4,16 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import nodeCrypto, { Crypto } from '@elastic/node-crypto';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { promisify } from 'util';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { Duration } from 'moment';
|
||||
import { KibanaRequest, Logger } from '../../../../../src/core/server';
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import { ConfigType } from '../config';
|
||||
import { SessionIndex, SessionIndexValue } from './session_index';
|
||||
import { SessionCookie } from './session_cookie';
|
||||
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 { ConfigType } from '../config';
|
||||
import type { SessionIndex, SessionIndexValue } from './session_index';
|
||||
import type { SessionCookie } from './session_cookie';
|
||||
|
||||
/**
|
||||
* The shape of the value that represents user's session information.
|
||||
|
@ -86,21 +85,6 @@ const SID_BYTE_LENGTH = 32;
|
|||
const AAD_BYTE_LENGTH = 32;
|
||||
|
||||
export class Session {
|
||||
/**
|
||||
* Session idle timeout in ms. If `null`, a session will stay active until its max lifespan is reached.
|
||||
*/
|
||||
private readonly idleTimeout: Duration | null;
|
||||
|
||||
/**
|
||||
* Timeout after which idle timeout property is updated in the index.
|
||||
*/
|
||||
private readonly idleIndexUpdateTimeout: number | null;
|
||||
|
||||
/**
|
||||
* Session max lifespan in ms. If `null` session may live indefinitely.
|
||||
*/
|
||||
private readonly lifespan: Duration | null;
|
||||
|
||||
/**
|
||||
* Used to encrypt and decrypt portion of the session value using configured encryption key.
|
||||
*/
|
||||
|
@ -113,14 +97,6 @@ export class Session {
|
|||
|
||||
constructor(private readonly options: Readonly<SessionOptions>) {
|
||||
this.crypto = nodeCrypto({ encryptionKey: this.options.config.encryptionKey });
|
||||
this.idleTimeout = this.options.config.session.idleTimeout;
|
||||
this.lifespan = this.options.config.session.lifespan;
|
||||
|
||||
// The timeout after which we update index is two times longer than configured idle timeout
|
||||
// since index updates are costly and we want to minimize them.
|
||||
this.idleIndexUpdateTimeout = this.options.config.session.idleTimeout
|
||||
? this.options.config.session.idleTimeout.asMilliseconds() * 2
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,7 +170,7 @@ export class Session {
|
|||
const sessionLogger = this.getLoggerForSID(sid);
|
||||
sessionLogger.debug('Creating a new session.');
|
||||
|
||||
const sessionExpirationInfo = this.calculateExpiry();
|
||||
const sessionExpirationInfo = this.calculateExpiry(sessionValue.provider);
|
||||
const { username, state, ...publicSessionValue } = sessionValue;
|
||||
|
||||
// First try to store session in the index and only then in the cookie to make sure cookie is
|
||||
|
@ -227,7 +203,10 @@ export class Session {
|
|||
return null;
|
||||
}
|
||||
|
||||
const sessionExpirationInfo = this.calculateExpiry(sessionCookieValue.lifespanExpiration);
|
||||
const sessionExpirationInfo = this.calculateExpiry(
|
||||
sessionValue.provider,
|
||||
sessionCookieValue.lifespanExpiration
|
||||
);
|
||||
const { username, state, metadata, ...publicSessionInfo } = sessionValue;
|
||||
|
||||
// First try to store session in the index and only then in the cookie to make sure cookie is
|
||||
|
@ -276,7 +255,10 @@ export class Session {
|
|||
|
||||
// We calculate actual expiration values based on the information extracted from the portion of
|
||||
// the session value that is stored in the cookie since it always contains the most recent value.
|
||||
const sessionExpirationInfo = this.calculateExpiry(sessionCookieValue.lifespanExpiration);
|
||||
const sessionExpirationInfo = this.calculateExpiry(
|
||||
sessionValue.provider,
|
||||
sessionCookieValue.lifespanExpiration
|
||||
);
|
||||
if (
|
||||
sessionExpirationInfo.idleTimeoutExpiration === sessionValue.idleTimeoutExpiration &&
|
||||
sessionExpirationInfo.lifespanExpiration === sessionValue.lifespanExpiration
|
||||
|
@ -311,17 +293,24 @@ export class Session {
|
|||
'Session lifespan configuration has changed, session index will be updated.'
|
||||
);
|
||||
updateSessionIndex = true;
|
||||
} else if (
|
||||
this.idleIndexUpdateTimeout !== null &&
|
||||
this.idleIndexUpdateTimeout <
|
||||
sessionExpirationInfo.idleTimeoutExpiration! -
|
||||
sessionValue.metadata.index.idleTimeoutExpiration!
|
||||
) {
|
||||
// 3. If idle timeout was updated a while ago.
|
||||
sessionLogger.debug(
|
||||
'Session idle timeout stored in the index is too old and will be updated.'
|
||||
} else {
|
||||
// The timeout after which we update index is two times longer than configured idle timeout
|
||||
// since index updates are costly and we want to minimize them.
|
||||
const { idleTimeout } = this.options.config.session.getExpirationTimeouts(
|
||||
sessionValue.provider
|
||||
);
|
||||
updateSessionIndex = true;
|
||||
if (
|
||||
idleTimeout !== null &&
|
||||
idleTimeout.asMilliseconds() * 2 <
|
||||
sessionExpirationInfo.idleTimeoutExpiration! -
|
||||
sessionValue.metadata.index.idleTimeoutExpiration!
|
||||
) {
|
||||
// 3. If idle timeout was updated a while ago.
|
||||
sessionLogger.debug(
|
||||
'Session idle timeout stored in the index is too old and will be updated.'
|
||||
);
|
||||
updateSessionIndex = true;
|
||||
}
|
||||
}
|
||||
|
||||
// First try to store session in the index and only then in the cookie to make sure cookie is
|
||||
|
@ -375,18 +364,21 @@ export class Session {
|
|||
}
|
||||
|
||||
private calculateExpiry(
|
||||
provider: AuthenticationProvider,
|
||||
currentLifespanExpiration?: number | null
|
||||
): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } {
|
||||
const now = Date.now();
|
||||
const { idleTimeout, lifespan } = this.options.config.session.getExpirationTimeouts(provider);
|
||||
|
||||
// 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 =
|
||||
currentLifespanExpiration && this.lifespan
|
||||
currentLifespanExpiration && lifespan
|
||||
? currentLifespanExpiration
|
||||
: this.lifespan && now + this.lifespan.asMilliseconds();
|
||||
const idleTimeoutExpiration = this.idleTimeout && now + this.idleTimeout.asMilliseconds();
|
||||
: lifespan && now + lifespan.asMilliseconds();
|
||||
const idleTimeoutExpiration = idleTimeout && now + idleTimeout.asMilliseconds();
|
||||
|
||||
return { idleTimeoutExpiration, lifespanExpiration };
|
||||
}
|
||||
|
|
|
@ -194,8 +194,39 @@ describe('Session index', () => {
|
|||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
// All expired sessions based on the lifespan, no matter which provider they belong to.
|
||||
{ range: { lifespanExpiration: { lte: now } } },
|
||||
{ range: { idleTimeoutExpiration: { lte: now } } },
|
||||
// All sessions that belong to the providers that aren't configured.
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider that are expired based on the idle timeout.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -226,9 +257,49 @@ describe('Session index', () => {
|
|||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
// All expired sessions based on the lifespan, no matter which provider they belong to.
|
||||
{ range: { lifespanExpiration: { lte: now } } },
|
||||
{ bool: { must_not: { exists: { field: 'lifespanExpiration' } } } },
|
||||
{ range: { idleTimeoutExpiration: { lte: now } } },
|
||||
// All sessions that belong to the providers that aren't configured.
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider but don't have a configured lifespan.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
must_not: { exists: { field: 'lifespanExpiration' } },
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider that are expired based on the idle timeout.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -260,9 +331,43 @@ describe('Session index', () => {
|
|||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
// All expired sessions based on the lifespan, no matter which provider they belong to.
|
||||
{ range: { lifespanExpiration: { lte: now } } },
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
// All sessions that belong to the providers that aren't configured.
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider that are either expired based on the idle timeout
|
||||
// or don't have it configured at all.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
should: [
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -294,10 +399,179 @@ describe('Session index', () => {
|
|||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
// All expired sessions based on the lifespan, no matter which provider they belong to.
|
||||
{ range: { lifespanExpiration: { lte: now } } },
|
||||
{ bool: { must_not: { exists: { field: 'lifespanExpiration' } } } },
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
// All sessions that belong to the providers that aren't configured.
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider but don't have a configured lifespan.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
must_not: { exists: { field: 'lifespanExpiration' } },
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a particular provider that are either expired based on the idle timeout
|
||||
// or don't have it configured at all.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic' } },
|
||||
],
|
||||
should: [
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('when both `lifespan` and `idleTimeout` are configured and multiple providers are enabled', async () => {
|
||||
const globalIdleTimeout = 123;
|
||||
const samlIdleTimeout = 33221;
|
||||
sessionIndex = new SessionIndex({
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
kibanaIndexName: '.kibana_some_tenant',
|
||||
config: createConfig(
|
||||
ConfigSchema.validate({
|
||||
session: { idleTimeout: globalIdleTimeout, lifespan: 456 },
|
||||
authc: {
|
||||
providers: {
|
||||
basic: { basic1: { order: 0 } },
|
||||
saml: {
|
||||
saml1: {
|
||||
order: 1,
|
||||
realm: 'saml-realm',
|
||||
session: { idleTimeout: samlIdleTimeout },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
loggingSystemMock.createLogger(),
|
||||
{ isTLSEnabled: false }
|
||||
),
|
||||
clusterClient: mockClusterClient,
|
||||
});
|
||||
|
||||
await sessionIndex.cleanUp();
|
||||
|
||||
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('deleteByQuery', {
|
||||
index: indexName,
|
||||
refresh: 'wait_for',
|
||||
ignore: [409, 404],
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
// All expired sessions based on the lifespan, no matter which provider they belong to.
|
||||
{ range: { lifespanExpiration: { lte: now } } },
|
||||
// All sessions that belong to the providers that aren't configured.
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic1' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'saml' } },
|
||||
{ term: { 'provider.name': 'saml1' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a Basic provider but don't have a configured lifespan.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic1' } },
|
||||
],
|
||||
must_not: { exists: { field: 'lifespanExpiration' } },
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a Basic provider that are either expired based on the idle timeout
|
||||
// or don't have it configured at all.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'basic' } },
|
||||
{ term: { 'provider.name': 'basic1' } },
|
||||
],
|
||||
should: [
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * globalIdleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a SAML provider but don't have a configured lifespan.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'saml' } },
|
||||
{ term: { 'provider.name': 'saml1' } },
|
||||
],
|
||||
must_not: { exists: { field: 'lifespanExpiration' } },
|
||||
},
|
||||
},
|
||||
// The sessions that belong to a SAML provider that are either expired based on the idle timeout
|
||||
// or don't have it configured at all.
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': 'saml' } },
|
||||
{ term: { 'provider.name': 'saml1' } },
|
||||
],
|
||||
should: [
|
||||
{ range: { idleTimeoutExpiration: { lte: now - 3 * samlIdleTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ILegacyClusterClient, Logger } from '../../../../../src/core/server';
|
||||
import { AuthenticationProvider } from '../../common/types';
|
||||
import { ConfigType } from '../config';
|
||||
import type { ILegacyClusterClient, Logger } from '../../../../../src/core/server';
|
||||
import type { AuthenticationProvider } from '../../common/types';
|
||||
import type { ConfigType } from '../config';
|
||||
|
||||
export interface SessionIndexOptions {
|
||||
readonly clusterClient: ILegacyClusterClient;
|
||||
readonly kibanaIndexName: string;
|
||||
readonly config: Pick<ConfigType, 'session'>;
|
||||
readonly config: Pick<ConfigType, 'session' | 'authc'>;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
|
@ -120,12 +120,6 @@ export class SessionIndex {
|
|||
*/
|
||||
private readonly indexName = `${this.options.kibanaIndexName}_security_session_${SESSION_INDEX_TEMPLATE_VERSION}`;
|
||||
|
||||
/**
|
||||
* Timeout after which session with the expired idle timeout _may_ be removed from the index
|
||||
* during regular cleanup routine.
|
||||
*/
|
||||
private readonly idleIndexCleanupTimeout: number | null;
|
||||
|
||||
/**
|
||||
* Promise that tracks session index initialization process. We'll need to get rid of this as soon
|
||||
* as Core provides support for plugin statuses (https://github.com/elastic/kibana/issues/41983).
|
||||
|
@ -134,14 +128,7 @@ export class SessionIndex {
|
|||
*/
|
||||
private indexInitialization?: Promise<void>;
|
||||
|
||||
constructor(private readonly options: Readonly<SessionIndexOptions>) {
|
||||
// This timeout is intentionally larger than the `idleIndexUpdateTimeout` (idleTimeout * 2)
|
||||
// configured in `Session` to be sure that the session value is definitely expired and may be
|
||||
// safely cleaned up.
|
||||
this.idleIndexCleanupTimeout = this.options.config.session.idleTimeout
|
||||
? this.options.config.session.idleTimeout.asMilliseconds() * 3
|
||||
: null;
|
||||
}
|
||||
constructor(private readonly options: Readonly<SessionIndexOptions>) {}
|
||||
|
||||
/**
|
||||
* Retrieves session value with the specified ID from the index. If session value isn't found
|
||||
|
@ -353,26 +340,62 @@ export class SessionIndex {
|
|||
this.options.logger.debug(`Running cleanup routine.`);
|
||||
|
||||
const now = Date.now();
|
||||
const providersSessionConfig = this.options.config.authc.sortedProviders.map((provider) => {
|
||||
return {
|
||||
boolQuery: {
|
||||
bool: {
|
||||
must: [
|
||||
{ term: { 'provider.type': provider.type } },
|
||||
{ term: { 'provider.name': provider.name } },
|
||||
],
|
||||
},
|
||||
},
|
||||
...this.options.config.session.getExpirationTimeouts(provider),
|
||||
};
|
||||
});
|
||||
|
||||
// Always try to delete sessions with expired lifespan (even if it's not configured right now).
|
||||
const deleteQueries: object[] = [{ range: { lifespanExpiration: { lte: now } } }];
|
||||
|
||||
// If lifespan is configured we should remove any sessions that were created without one.
|
||||
if (this.options.config.session.lifespan) {
|
||||
deleteQueries.push({ bool: { must_not: { exists: { field: 'lifespanExpiration' } } } });
|
||||
}
|
||||
// If session belongs to a not configured provider we should also remove it.
|
||||
deleteQueries.push({
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: providersSessionConfig.map(({ boolQuery }) => boolQuery),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If idle timeout is configured we should delete all sessions without specified idle timeout
|
||||
// or if that session hasn't been updated for a while meaning that session is expired.
|
||||
if (this.idleIndexCleanupTimeout) {
|
||||
deleteQueries.push(
|
||||
{ range: { idleTimeoutExpiration: { lte: now - this.idleIndexCleanupTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } }
|
||||
);
|
||||
} else {
|
||||
// Otherwise just delete all expired sessions that were previously created with the idle
|
||||
// timeout.
|
||||
deleteQueries.push({ range: { idleTimeoutExpiration: { lte: now } } });
|
||||
for (const { boolQuery, lifespan, idleTimeout } of providersSessionConfig) {
|
||||
// If lifespan is configured we should remove any sessions that were created without one.
|
||||
if (lifespan) {
|
||||
deleteQueries.push({
|
||||
bool: { ...boolQuery.bool, must_not: { exists: { field: 'lifespanExpiration' } } },
|
||||
});
|
||||
}
|
||||
|
||||
// This timeout is intentionally larger than the timeout used in `Session` to update idle
|
||||
// timeout in the session index (idleTimeout * 2) to be sure that the session value is
|
||||
// definitely expired and may be safely cleaned up.
|
||||
const idleIndexCleanupTimeout = idleTimeout ? idleTimeout.asMilliseconds() * 3 : null;
|
||||
deleteQueries.push({
|
||||
bool: {
|
||||
...boolQuery.bool,
|
||||
// If idle timeout is configured we should delete all sessions without specified idle timeout
|
||||
// or if that session hasn't been updated for a while meaning that session is expired. Otherwise
|
||||
// just delete all expired sessions that were previously created with the idle timeout.
|
||||
should: idleIndexCleanupTimeout
|
||||
? [
|
||||
{ range: { idleTimeoutExpiration: { lte: now - idleIndexCleanupTimeout } } },
|
||||
{ bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
|
||||
]
|
||||
: [{ range: { idleTimeoutExpiration: { lte: now } } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -147,9 +147,9 @@ export default function ({ getService }) {
|
|||
'authentication_type',
|
||||
]);
|
||||
expect(apiResponse.body.username).to.be(validUsername);
|
||||
expect(apiResponse.body.authentication_provider).to.eql('__http__');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'http', name: '__http__' });
|
||||
expect(apiResponse.body.authentication_type).to.be('realm');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
// Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud
|
||||
});
|
||||
|
||||
describe('with session cookie', () => {
|
||||
|
@ -193,9 +193,9 @@ export default function ({ getService }) {
|
|||
'authentication_type',
|
||||
]);
|
||||
expect(apiResponse.body.username).to.be(validUsername);
|
||||
expect(apiResponse.body.authentication_provider).to.eql('basic');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
expect(apiResponse.body.authentication_type).to.be('realm');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
// Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud
|
||||
});
|
||||
|
||||
it('should extend cookie on every successful non-system API call', async () => {
|
||||
|
|
|
@ -79,9 +79,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(user.username).to.eql(username);
|
||||
expect(user.authentication_provider).to.eql('basic');
|
||||
expect(user.authentication_provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
expect(user.authentication_type).to.eql('realm');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
// Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud
|
||||
});
|
||||
|
||||
describe('initiating SPNEGO', () => {
|
||||
|
@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
enabled: true,
|
||||
authentication_realm: { name: 'kerb1', type: 'kerberos' },
|
||||
lookup_realm: { name: 'kerb1', type: 'kerberos' },
|
||||
authentication_provider: 'kerberos',
|
||||
authentication_provider: { type: 'kerberos', name: 'kerberos' },
|
||||
authentication_type: 'token',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(user.username).to.eql(username);
|
||||
expect(user.authentication_provider).to.eql('basic');
|
||||
expect(user.authentication_provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
expect(user.authentication_type).to.be('realm');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
});
|
||||
|
@ -235,7 +235,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(apiResponse.body.username).to.be('user1');
|
||||
expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' });
|
||||
expect(apiResponse.body.authentication_provider).to.eql('oidc');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'oidc', name: 'oidc' });
|
||||
expect(apiResponse.body.authentication_type).to.be('token');
|
||||
});
|
||||
});
|
||||
|
@ -289,7 +289,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(apiResponse.body.username).to.be('user2');
|
||||
expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' });
|
||||
expect(apiResponse.body.authentication_provider).to.eql('oidc');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'oidc', name: 'oidc' });
|
||||
expect(apiResponse.body.authentication_type).to.be('token');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -156,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(apiResponse.body.username).to.be('user1');
|
||||
expect(apiResponse.body.authentication_realm).to.eql({ name: 'oidc1', type: 'oidc' });
|
||||
expect(apiResponse.body.authentication_provider).to.eql('oidc');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'oidc', name: 'oidc' });
|
||||
expect(apiResponse.body.authentication_type).to.be('token');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,8 +93,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(user.username).to.eql(username);
|
||||
expect(user.authentication_provider).to.eql('basic');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
expect(user.authentication_provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
// Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud
|
||||
});
|
||||
|
||||
it('should properly set cookie and authenticate user', async () => {
|
||||
|
@ -123,7 +123,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
authentication_realm: { name: 'pki1', type: 'pki' },
|
||||
lookup_realm: { name: 'pki1', type: 'pki' },
|
||||
authentication_provider: 'pki',
|
||||
authentication_provider: { name: 'pki', type: 'pki' },
|
||||
authentication_type: 'token',
|
||||
});
|
||||
|
||||
|
@ -168,7 +168,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
authentication_realm: { name: 'pki1', type: 'pki' },
|
||||
lookup_realm: { name: 'pki1', type: 'pki' },
|
||||
authentication_provider: 'pki',
|
||||
authentication_provider: { name: 'pki', type: 'pki' },
|
||||
authentication_type: 'token',
|
||||
});
|
||||
|
||||
|
|
|
@ -15,16 +15,35 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
);
|
||||
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
|
||||
|
||||
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
|
||||
const idpPath = resolve(__dirname, './fixtures/saml/idp_metadata.xml');
|
||||
|
||||
return {
|
||||
testFiles: [resolve(__dirname, './tests/session_idle')],
|
||||
|
||||
services: {
|
||||
randomness: kibanaAPITestsConfig.get('services.randomness'),
|
||||
legacyEs: kibanaAPITestsConfig.get('services.legacyEs'),
|
||||
supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'),
|
||||
},
|
||||
|
||||
servers: xPackAPITestsConfig.get('servers'),
|
||||
esTestCluster: xPackAPITestsConfig.get('esTestCluster'),
|
||||
|
||||
esTestCluster: {
|
||||
...xPackAPITestsConfig.get('esTestCluster'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('esTestCluster.serverArgs'),
|
||||
'xpack.security.authc.token.enabled=true',
|
||||
'xpack.security.authc.token.timeout=15s',
|
||||
'xpack.security.authc.realms.saml.saml1.order=0',
|
||||
`xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`,
|
||||
'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1',
|
||||
`xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`,
|
||||
`xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`,
|
||||
`xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`,
|
||||
'xpack.security.authc.realms.saml.saml1.attributes.principal=urn:oid:0.0.7',
|
||||
],
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
|
@ -32,6 +51,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.security.session.idleTimeout=5s',
|
||||
'--xpack.security.session.cleanupInterval=10s',
|
||||
`--xpack.security.authc.providers=${JSON.stringify({
|
||||
basic: { basic1: { order: 0 } },
|
||||
saml: {
|
||||
saml_fallback: { order: 1, realm: 'saml1' },
|
||||
saml_override: { order: 2, realm: 'saml1', session: { idleTimeout: '1m' } },
|
||||
saml_disable: { order: 3, realm: 'saml1', session: { idleTimeout: 0 } },
|
||||
},
|
||||
})}`,
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -15,16 +15,35 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
);
|
||||
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
|
||||
|
||||
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
|
||||
const idpPath = resolve(__dirname, './fixtures/saml/idp_metadata.xml');
|
||||
|
||||
return {
|
||||
testFiles: [resolve(__dirname, './tests/session_lifespan')],
|
||||
|
||||
services: {
|
||||
randomness: kibanaAPITestsConfig.get('services.randomness'),
|
||||
legacyEs: kibanaAPITestsConfig.get('services.legacyEs'),
|
||||
supertestWithoutAuth: xPackAPITestsConfig.get('services.supertestWithoutAuth'),
|
||||
},
|
||||
|
||||
servers: xPackAPITestsConfig.get('servers'),
|
||||
esTestCluster: xPackAPITestsConfig.get('esTestCluster'),
|
||||
|
||||
esTestCluster: {
|
||||
...xPackAPITestsConfig.get('esTestCluster'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('esTestCluster.serverArgs'),
|
||||
'xpack.security.authc.token.enabled=true',
|
||||
'xpack.security.authc.token.timeout=15s',
|
||||
'xpack.security.authc.realms.saml.saml1.order=0',
|
||||
`xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`,
|
||||
'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1',
|
||||
`xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`,
|
||||
`xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`,
|
||||
`xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`,
|
||||
'xpack.security.authc.realms.saml.saml1.attributes.principal=urn:oid:0.0.7',
|
||||
],
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
|
@ -32,6 +51,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--xpack.security.session.lifespan=5s',
|
||||
'--xpack.security.session.cleanupInterval=10s',
|
||||
`--xpack.security.authc.providers=${JSON.stringify({
|
||||
basic: { basic1: { order: 0 } },
|
||||
saml: {
|
||||
saml_fallback: { order: 1, realm: 'saml1' },
|
||||
saml_override: { order: 2, realm: 'saml1', session: { lifespan: '1m' } },
|
||||
saml_disable: { order: 3, realm: 'saml1', session: { lifespan: 0 } },
|
||||
},
|
||||
})}`,
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -10,6 +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 { getStateAndNonce } from '../../../oidc_api_integration/fixtures/oidc_tools';
|
||||
import {
|
||||
getMutualAuthenticationResponseToken,
|
||||
|
@ -35,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
async function checkSessionCookie(
|
||||
sessionCookie: Cookie,
|
||||
username: string,
|
||||
providerName: string,
|
||||
provider: AuthenticationProvider,
|
||||
authenticationRealm: { name: string; type: string } | null,
|
||||
authenticationType: string
|
||||
) {
|
||||
|
@ -66,7 +67,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
]);
|
||||
|
||||
expect(apiResponse.body.username).to.be(username);
|
||||
expect(apiResponse.body.authentication_provider).to.be(providerName);
|
||||
expect(apiResponse.body.authentication_provider).to.eql(provider);
|
||||
if (authenticationRealm) {
|
||||
expect(apiResponse.body.authentication_realm).to.eql(authenticationRealm);
|
||||
}
|
||||
|
@ -146,11 +147,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'a@b.c',
|
||||
providerName,
|
||||
{
|
||||
name: providerName,
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: providerName },
|
||||
{ name: providerName, type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
}
|
||||
|
@ -182,11 +180,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'a@b.c',
|
||||
providerName,
|
||||
{
|
||||
name: providerName,
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: providerName },
|
||||
{ name: providerName, type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
}
|
||||
|
@ -215,11 +210,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'a@b.c',
|
||||
providerName,
|
||||
{
|
||||
name: providerName,
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: providerName },
|
||||
{ name: providerName, type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
}
|
||||
|
@ -244,7 +236,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
)!;
|
||||
// Skip auth provider check since this comes from the reserved realm,
|
||||
// which is not available when running on ESS
|
||||
await checkSessionCookie(basicSessionCookie, 'elastic', 'basic1', null, 'realm');
|
||||
await checkSessionCookie(
|
||||
basicSessionCookie,
|
||||
'elastic',
|
||||
{ type: 'basic', name: 'basic1' },
|
||||
null,
|
||||
'realm'
|
||||
);
|
||||
|
||||
const authenticationResponse = await supertest
|
||||
.post('/api/security/saml/callback')
|
||||
|
@ -267,11 +265,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'a@b.c',
|
||||
providerName,
|
||||
{
|
||||
name: providerName,
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: providerName },
|
||||
{ name: providerName, type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
}
|
||||
|
@ -293,11 +288,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
saml1SessionCookie,
|
||||
'a@b.c',
|
||||
'saml1',
|
||||
{
|
||||
name: 'saml1',
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: 'saml1' },
|
||||
{ name: 'saml1', type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
|
||||
|
@ -321,11 +313,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
saml2SessionCookie,
|
||||
'a@b.c',
|
||||
'saml2',
|
||||
{
|
||||
name: 'saml2',
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: 'saml2' },
|
||||
{ name: 'saml2', type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -346,11 +335,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
saml1SessionCookie,
|
||||
'a@b.c',
|
||||
'saml1',
|
||||
{
|
||||
name: 'saml1',
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: 'saml1' },
|
||||
{ name: 'saml1', type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
|
||||
|
@ -376,11 +362,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
saml2SessionCookie,
|
||||
'a@b.c',
|
||||
'saml2',
|
||||
{
|
||||
name: 'saml2',
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: 'saml2' },
|
||||
{ name: 'saml2', type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -466,11 +449,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'a@b.c',
|
||||
providerName,
|
||||
{
|
||||
name: providerName,
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: providerName },
|
||||
{ name: providerName, type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
}
|
||||
|
@ -537,11 +517,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
saml2SessionCookie,
|
||||
'a@b.c',
|
||||
'saml2',
|
||||
{
|
||||
name: 'saml2',
|
||||
type: 'saml',
|
||||
},
|
||||
{ type: 'saml', name: 'saml2' },
|
||||
{ name: 'saml2', type: 'saml' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -586,11 +563,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'tester@TEST.ELASTIC.CO',
|
||||
'kerberos1',
|
||||
{
|
||||
name: 'kerb1',
|
||||
type: 'kerberos',
|
||||
},
|
||||
{ type: 'kerberos', name: 'kerberos1' },
|
||||
{ name: 'kerb1', type: 'kerberos' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -635,11 +609,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'tester@TEST.ELASTIC.CO',
|
||||
'kerberos1',
|
||||
{
|
||||
name: 'kerb1',
|
||||
type: 'kerberos',
|
||||
},
|
||||
{ type: 'kerberos', name: 'kerberos1' },
|
||||
{ name: 'kerb1', type: 'kerberos' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -677,11 +648,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'user2',
|
||||
'oidc1',
|
||||
{
|
||||
name: 'oidc1',
|
||||
type: 'oidc',
|
||||
},
|
||||
{ type: 'oidc', name: 'oidc1' },
|
||||
{ name: 'oidc1', type: 'oidc' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -737,11 +705,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'user1',
|
||||
'oidc1',
|
||||
{
|
||||
name: 'oidc1',
|
||||
type: 'oidc',
|
||||
},
|
||||
{ type: 'oidc', name: 'oidc1' },
|
||||
{ name: 'oidc1', type: 'oidc' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
@ -779,11 +744,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await checkSessionCookie(
|
||||
request.cookie(cookies[0])!,
|
||||
'first_client',
|
||||
'pki1',
|
||||
{
|
||||
name: 'pki1',
|
||||
type: 'pki',
|
||||
},
|
||||
{ type: 'pki', name: 'pki1' },
|
||||
{ name: 'pki1', type: 'pki' },
|
||||
'token'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -65,7 +65,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(apiResponse.body.username).to.be(username);
|
||||
expect(apiResponse.body.authentication_realm).to.eql({ name: 'saml1', type: 'saml' });
|
||||
expect(apiResponse.body.authentication_provider).to.eql('saml');
|
||||
expect(apiResponse.body.authentication_provider).to.eql({ type: 'saml', name: 'saml' });
|
||||
expect(apiResponse.body.authentication_type).to.be('token');
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(user.username).to.eql(username);
|
||||
expect(user.authentication_provider).to.eql('basic');
|
||||
expect(user.authentication_provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
expect(user.authentication_type).to.be('realm');
|
||||
// Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import request, { Cookie } from 'request';
|
||||
import { delay } from 'bluebird';
|
||||
import expect from '@kbn/expect';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
|
||||
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -14,9 +16,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const es = getService('legacyEs');
|
||||
const config = getService('config');
|
||||
const log = getService('log');
|
||||
const [username, password] = config.get('servers.elasticsearch.auth').split(':');
|
||||
const randomness = getService('randomness');
|
||||
const [basicUsername, basicPassword] = config.get('servers.elasticsearch.auth').split(':');
|
||||
const kibanaServerConfig = config.get('servers.kibana');
|
||||
|
||||
async function checkSessionCookie(sessionCookie: Cookie, providerName: string) {
|
||||
async function checkSessionCookie(
|
||||
sessionCookie: Cookie,
|
||||
username: string,
|
||||
provider: AuthenticationProvider
|
||||
) {
|
||||
const apiResponse = await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
|
@ -24,9 +32,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(apiResponse.body.username).to.be(username);
|
||||
expect(apiResponse.body.authentication_provider).to.be(providerName);
|
||||
expect(apiResponse.body.authentication_provider).to.eql(provider);
|
||||
|
||||
return request.cookie(apiResponse.headers['set-cookie'][0])!;
|
||||
return Array.isArray(apiResponse.headers['set-cookie'])
|
||||
? request.cookie(apiResponse.headers['set-cookie'][0])!
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function getNumberOfSessionDocuments() {
|
||||
|
@ -35,6 +45,31 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
}).value;
|
||||
}
|
||||
|
||||
async function loginWithSAML(providerName: string) {
|
||||
const handshakeResponse = await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ providerType: 'saml', providerName, currentURL: '' })
|
||||
.expect(200);
|
||||
|
||||
const authenticationResponse = await supertest
|
||||
.post('/api/security/saml/callback')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', request.cookie(handshakeResponse.headers['set-cookie'][0])!.cookieString())
|
||||
.send({
|
||||
SAMLResponse: await getSAMLResponse({
|
||||
destination: `http://localhost:${kibanaServerConfig.port}/api/security/saml/callback`,
|
||||
sessionIndex: String(randomness.naturalNumber()),
|
||||
inResponseTo: await getSAMLRequestId(handshakeResponse.body.location),
|
||||
}),
|
||||
})
|
||||
.expect(302);
|
||||
|
||||
const cookie = request.cookie(authenticationResponse.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(cookie, 'a@b.c', { type: 'saml', name: providerName });
|
||||
return cookie;
|
||||
}
|
||||
|
||||
describe('Session Idle cleanup', () => {
|
||||
beforeEach(async () => {
|
||||
await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' });
|
||||
|
@ -52,14 +87,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username, password },
|
||||
params: { username: basicUsername, password: basicPassword },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const sessionCookie = request.cookie(response.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(sessionCookie, 'basic');
|
||||
await checkSessionCookie(sessionCookie, basicUsername, { type: 'basic', name: 'basic1' });
|
||||
expect(await getNumberOfSessionDocuments()).to.be(1);
|
||||
|
||||
// Cleanup routine runs every 10s, and idle timeout threshold is three times larger than 5s
|
||||
|
@ -76,6 +111,66 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(401);
|
||||
});
|
||||
|
||||
it('should properly clean up session expired because of idle timeout when providers override global session config', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
const [
|
||||
samlDisableSessionCookie,
|
||||
samlOverrideSessionCookie,
|
||||
samlFallbackSessionCookie,
|
||||
] = await Promise.all([
|
||||
loginWithSAML('saml_disable'),
|
||||
loginWithSAML('saml_override'),
|
||||
loginWithSAML('saml_fallback'),
|
||||
]);
|
||||
|
||||
const response = await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username: basicUsername, password: basicPassword },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const basicSessionCookie = request.cookie(response.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(basicSessionCookie, basicUsername, {
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
});
|
||||
expect(await getNumberOfSessionDocuments()).to.be(4);
|
||||
|
||||
// Cleanup routine runs every 10s, and idle timeout threshold is three times larger than 5s
|
||||
// idle timeout, let's wait for 30s to make sure cleanup routine runs when idle timeout
|
||||
// threshold is exceeded.
|
||||
await delay(30000);
|
||||
|
||||
// Session for basic and SAML that used global session settings should not be valid anymore.
|
||||
expect(await getNumberOfSessionDocuments()).to.be(2);
|
||||
await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', basicSessionCookie.cookieString())
|
||||
.expect(401);
|
||||
await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', samlFallbackSessionCookie.cookieString())
|
||||
.expect(401);
|
||||
|
||||
// But sessions for the SAML with overridden and disabled lifespan should still be valid.
|
||||
await checkSessionCookie(samlOverrideSessionCookie, 'a@b.c', {
|
||||
type: 'saml',
|
||||
name: 'saml_override',
|
||||
});
|
||||
await checkSessionCookie(samlDisableSessionCookie, 'a@b.c', {
|
||||
type: 'saml',
|
||||
name: 'saml_disable',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clean up session if user is active', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
|
@ -84,14 +179,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username, password },
|
||||
params: { username: basicUsername, password: basicPassword },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
let sessionCookie = request.cookie(response.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(sessionCookie, 'basic');
|
||||
await checkSessionCookie(sessionCookie, basicUsername, { type: 'basic', name: 'basic1' });
|
||||
expect(await getNumberOfSessionDocuments()).to.be(1);
|
||||
|
||||
// Run 20 consequent requests with 1.5s delay, during this time cleanup procedure should run at
|
||||
|
@ -100,7 +195,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
// Session idle timeout is 15s, let's wait 10s and make a new request that would extend the session.
|
||||
await delay(1500);
|
||||
|
||||
sessionCookie = await checkSessionCookie(sessionCookie, 'basic');
|
||||
sessionCookie = (await checkSessionCookie(sessionCookie, basicUsername, {
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
}))!;
|
||||
log.debug(`Session is still valid after ${(counter + 1) * 1.5}s`);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username: validUsername, password: validPassword },
|
||||
})
|
||||
|
@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(body.now).to.be.a('number');
|
||||
expect(body.idleTimeoutExpiration).to.be.a('number');
|
||||
expect(body.lifespanExpiration).to.be(null);
|
||||
expect(body.provider).to.eql({ type: 'basic', name: 'basic' });
|
||||
expect(body.provider).to.eql({ type: 'basic', name: 'basic1' });
|
||||
});
|
||||
|
||||
it('should not extend the session', async () => {
|
||||
|
|
|
@ -7,15 +7,23 @@
|
|||
import request, { Cookie } from 'request';
|
||||
import { delay } from 'bluebird';
|
||||
import expect from '@kbn/expect';
|
||||
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
|
||||
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const es = getService('legacyEs');
|
||||
const config = getService('config');
|
||||
const [username, password] = config.get('servers.elasticsearch.auth').split(':');
|
||||
const randomness = getService('randomness');
|
||||
const [basicUsername, basicPassword] = config.get('servers.elasticsearch.auth').split(':');
|
||||
const kibanaServerConfig = config.get('servers.kibana');
|
||||
|
||||
async function checkSessionCookie(sessionCookie: Cookie, providerName: string) {
|
||||
async function checkSessionCookie(
|
||||
sessionCookie: Cookie,
|
||||
username: string,
|
||||
provider: AuthenticationProvider
|
||||
) {
|
||||
const apiResponse = await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
|
@ -23,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
expect(apiResponse.body.username).to.be(username);
|
||||
expect(apiResponse.body.authentication_provider).to.be(providerName);
|
||||
expect(apiResponse.body.authentication_provider).to.eql(provider);
|
||||
}
|
||||
|
||||
async function getNumberOfSessionDocuments() {
|
||||
|
@ -32,6 +40,31 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
}).value;
|
||||
}
|
||||
|
||||
async function loginWithSAML(providerName: string) {
|
||||
const handshakeResponse = await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ providerType: 'saml', providerName, currentURL: '' })
|
||||
.expect(200);
|
||||
|
||||
const authenticationResponse = await supertest
|
||||
.post('/api/security/saml/callback')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', request.cookie(handshakeResponse.headers['set-cookie'][0])!.cookieString())
|
||||
.send({
|
||||
SAMLResponse: await getSAMLResponse({
|
||||
destination: `http://localhost:${kibanaServerConfig.port}/api/security/saml/callback`,
|
||||
sessionIndex: String(randomness.naturalNumber()),
|
||||
inResponseTo: await getSAMLRequestId(handshakeResponse.body.location),
|
||||
}),
|
||||
})
|
||||
.expect(302);
|
||||
|
||||
const cookie = request.cookie(authenticationResponse.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(cookie, 'a@b.c', { type: 'saml', name: providerName });
|
||||
return cookie;
|
||||
}
|
||||
|
||||
describe('Session Lifespan cleanup', () => {
|
||||
beforeEach(async () => {
|
||||
await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' });
|
||||
|
@ -49,14 +82,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username, password },
|
||||
params: { username: basicUsername, password: basicPassword },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const sessionCookie = request.cookie(response.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(sessionCookie, 'basic');
|
||||
await checkSessionCookie(sessionCookie, basicUsername, {
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
});
|
||||
expect(await getNumberOfSessionDocuments()).to.be(1);
|
||||
|
||||
// Cleanup routine runs every 10s, let's wait for 30s to make sure it runs multiple times and
|
||||
|
@ -71,5 +107,63 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should properly clean up session expired because of lifespan when providers override global session config', async function () {
|
||||
this.timeout(60000);
|
||||
|
||||
const [
|
||||
samlDisableSessionCookie,
|
||||
samlOverrideSessionCookie,
|
||||
samlFallbackSessionCookie,
|
||||
] = await Promise.all([
|
||||
loginWithSAML('saml_disable'),
|
||||
loginWithSAML('saml_override'),
|
||||
loginWithSAML('saml_fallback'),
|
||||
]);
|
||||
|
||||
const response = await supertest
|
||||
.post('/internal/security/login')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
providerType: 'basic',
|
||||
providerName: 'basic1',
|
||||
currentURL: '/',
|
||||
params: { username: basicUsername, password: basicPassword },
|
||||
})
|
||||
.expect(200);
|
||||
const basicSessionCookie = request.cookie(response.headers['set-cookie'][0])!;
|
||||
await checkSessionCookie(basicSessionCookie, basicUsername, {
|
||||
type: 'basic',
|
||||
name: 'basic1',
|
||||
});
|
||||
expect(await getNumberOfSessionDocuments()).to.be(4);
|
||||
|
||||
// Cleanup routine runs every 10s, let's wait for 30s to make sure it runs multiple times and
|
||||
// when lifespan is exceeded.
|
||||
await delay(30000);
|
||||
|
||||
// Session for basic and SAML that used global session settings should not be valid anymore.
|
||||
expect(await getNumberOfSessionDocuments()).to.be(2);
|
||||
await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', basicSessionCookie.cookieString())
|
||||
.expect(401);
|
||||
await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', samlFallbackSessionCookie.cookieString())
|
||||
.expect(401);
|
||||
|
||||
// But sessions for the SAML with overridden and disabled lifespan should still be valid.
|
||||
await checkSessionCookie(samlOverrideSessionCookie, 'a@b.c', {
|
||||
type: 'saml',
|
||||
name: 'saml_override',
|
||||
});
|
||||
await checkSessionCookie(samlDisableSessionCookie, 'a@b.c', {
|
||||
type: 'saml',
|
||||
name: 'saml_disable',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue