Skip to content

Commit

Permalink
Consolidate session handling code.
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Jun 10, 2020
1 parent 50d47a2 commit f75a899
Show file tree
Hide file tree
Showing 15 changed files with 811 additions and 735 deletions.
551 changes: 96 additions & 455 deletions x-pack/plugins/security/server/authentication/authenticator.test.ts

Large diffs are not rendered by default.

271 changes: 60 additions & 211 deletions x-pack/plugins/security/server/authentication/authenticator.ts

Large diffs are not rendered by default.

79 changes: 49 additions & 30 deletions x-pack/plugins/security/server/authentication/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { licenseMock } from '../../common/licensing/index.mock';

jest.mock('./api_keys');
jest.mock('./authenticator');
jest.mock('./session');

import Boom from 'boom';

Expand All @@ -18,8 +17,11 @@ import {
httpServiceMock,
elasticsearchServiceMock,
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
import { securityAuditLoggerMock } from '../audit/index.mock';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
import { sessionMock } from '../session_management/session.mock';

import {
AuthenticationHandler,
Expand All @@ -41,9 +43,10 @@ import {
InvalidateAPIKeyParams,
} from './api_keys';
import { SecurityLicense } from '../../common/licensing';
import { SessionInfo } from '../../common/types';
import { SecurityAuditLogger } from '../audit';
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
import { Session } from '../session_management';

describe('setupAuthentication()', () => {
let mockSetupAuthenticationParams: {
Expand Down Expand Up @@ -85,33 +88,6 @@ describe('setupAuthentication()', () => {

afterEach(() => jest.clearAllMocks());

it('properly initializes session storage and registers auth handler', async () => {
const config = {
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
cookieName: 'my-sid-cookie',
};

await setupAuthentication(mockSetupAuthenticationParams);

expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);

expect(
mockSetupAuthenticationParams.http.createCookieSessionStorageFactory
).toHaveBeenCalledTimes(1);
expect(
mockSetupAuthenticationParams.http.createCookieSessionStorageFactory
).toHaveBeenCalledWith({
encryptionKey: config.encryptionKey,
isSecure: config.secureCookies,
name: config.cookieName,
validate: expect.any(Function),
});
});

describe('authentication handler', () => {
let authHandler: AuthenticationHandler;
let authenticate: jest.SpyInstance<Promise<AuthenticationResult>, [KibanaRequest]>;
Expand All @@ -121,6 +97,11 @@ describe('setupAuthentication()', () => {

await setupAuthentication(mockSetupAuthenticationParams);

expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);

authHandler = mockSetupAuthenticationParams.http.registerAuth.mock.calls[0][0];
authenticate = jest.requireMock('./authenticator').Authenticator.mock.instances[0]
.authenticate;
Expand Down Expand Up @@ -319,6 +300,44 @@ describe('setupAuthentication()', () => {
});
});

describe('getSessionInfo()', () => {
let sessionMockInstance: jest.Mocked<PublicMethodsOf<Session>>;
let getSessionInfo: (r: KibanaRequest) => Promise<SessionInfo | null>;
beforeEach(async () => {
sessionMockInstance = sessionMock.create();
jest.requireMock('./session').Session.mockImplementation(() => sessionMockInstance);

getSessionInfo = (await setupAuthentication(mockSetupAuthenticationParams)).getSessionInfo;
});

it('returns current session info if session exists.', async () => {
const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf();
const mockInfo = {
now: currentDate,
idleTimeoutExpiration: currentDate + 60000,
lifespanExpiration: currentDate + 120000,
provider: { type: 'basic', name: 'basic1' },
};

sessionMockInstance.get.mockResolvedValue({
provider: mockInfo.provider,
idleTimeoutExpiration: mockInfo.idleTimeoutExpiration,
lifespanExpiration: mockInfo.lifespanExpiration,
state: { authorization: 'Basic xxx' },
path: mockSetupAuthenticationParams.http.basePath.serverBasePath,
});
jest.spyOn(Date, 'now').mockImplementation(() => currentDate);

await expect(getSessionInfo(httpServerMock.createKibanaRequest())).resolves.toEqual(mockInfo);
});

it('returns `null` if session does not exist.', async () => {
sessionMockInstance.get.mockResolvedValue(null);

await expect(getSessionInfo(httpServerMock.createKibanaRequest())).resolves.toBeNull();
});
});

describe('isAuthenticated()', () => {
let isAuthenticated: (r: KibanaRequest) => boolean;
beforeEach(async () => {
Expand Down
71 changes: 37 additions & 34 deletions x-pack/plugins/security/server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { SessionInfo } from '../../common/types';
import { SecurityAuditLogger } from '../audit';
import { ConfigType } from '../config';
import { getErrorStatusCode } from '../errors';
import { Authenticator, ProviderSession } from './authenticator';
import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys';
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { Session } from '../session_management';
import { Authenticator } from './authenticator';
import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys';

export { canRedirectRequest } from './can_redirect_request';
export { Authenticator, ProviderLoginAttempt } from './authenticator';
Expand Down Expand Up @@ -71,45 +73,46 @@ export async function setupAuthentication({
return (http.auth.get(request).state ?? null) as AuthenticatedUser | null;
};

const isValid = (sessionValue: ProviderSession) => {
// ensure that this cookie was created with the current Kibana configuration
const { path, idleTimeoutExpiration, lifespanExpiration } = sessionValue;
if (path !== undefined && path !== (http.basePath.serverBasePath || '/')) {
authLogger.debug(`Outdated session value with path "${sessionValue.path}"`);
return false;
}
// ensure that this cookie is not expired
if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) {
return false;
} else if (lifespanExpiration && lifespanExpiration < Date.now()) {
return false;
/**
* Returns session information for the current request.
* @param request Request instance.
*/
const getSessionInfo = async (request: KibanaRequest): Promise<SessionInfo | null> => {
const sessionValue = await session.get(request);
if (!sessionValue) {
return null;
}
return true;

// We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return
// the current server time -- that way the client can calculate the relative time to expiration.
return {
now: Date.now(),
idleTimeoutExpiration: sessionValue.idleTimeoutExpiration,
lifespanExpiration: sessionValue.lifespanExpiration,
provider: sessionValue.provider,
};
};

const session = new Session({
auditLogger,
logger: loggers.get('session'),
clusterClient,
config,
http,
});

authLogger.debug('Successfully initialized session.');

const authenticator = new Authenticator({
auditLogger,
getFeatureUsageService,
getCurrentUser,
loggers,
clusterClient,
basePath: http.basePath,
config: { session: config.session, authc: config.authc },
config: { authc: config.authc },
getCurrentUser,
getFeatureUsageService,
license,
loggers,
sessionStorageFactory: await http.createCookieSessionStorageFactory({
encryptionKey: config.encryptionKey,
isSecure: config.secureCookies,
name: config.cookieName,
validate: (session: ProviderSession | ProviderSession[]) => {
const array: ProviderSession[] = Array.isArray(session) ? session : [session];
for (const sess of array) {
if (!isValid(sess)) {
return { isValid: false, path: sess.path };
}
}
return { isValid: true };
},
}),
session,
});

authLogger.debug('Successfully initialized authenticator.');
Expand Down Expand Up @@ -179,7 +182,7 @@ export async function setupAuthentication({
return {
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
getSessionInfo: authenticator.getSessionInfo.bind(authenticator),
getSessionInfo,
isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator),
acknowledgeAccessAgreement: authenticator.acknowledgeAccessAgreement.bind(authenticator),
getCurrentUser,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { of } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
import { ICustomClusterClient } from '../../../../src/core/server';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';
import { elasticsearchClientPlugin } from './elasticsearch/elasticsearch_client_plugin';
import { Plugin, PluginSetupDependencies } from './plugin';

import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks';
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { defineRoutes } from './routes';
import { SecurityLicenseService, SecurityLicense } from '../common/licensing';
import { setupSavedObjects } from './saved_objects';
import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';
import { elasticsearchClientPlugin } from './elasticsearch/elasticsearch_client_plugin';
import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage';

export type SpacesService = Pick<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { sessionMock } from './session.mock';
7 changes: 7 additions & 0 deletions x-pack/plugins/security/server/session_management/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { Session, SessionValue } from './session';
15 changes: 15 additions & 0 deletions x-pack/plugins/security/server/session_management/session.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { Session } from './session';

export const sessionMock = {
create: (): jest.Mocked<PublicMethodsOf<Session>> => ({
get: jest.fn(),
set: jest.fn().mockImplementation(async (request, sessionValue) => sessionValue),
clear: jest.fn(),
}),
};
Loading

0 comments on commit f75a899

Please sign in to comment.