Skip to content

Commit

Permalink
Backport cookie compression
Browse files Browse the repository at this point in the history
Signed-off-by: Jochen Kressin <[email protected]>
  • Loading branch information
jochen-kressin committed Oct 25, 2023
1 parent fba07b8 commit 335c1da
Show file tree
Hide file tree
Showing 14 changed files with 919 additions and 28 deletions.
3 changes: 2 additions & 1 deletion common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export const DEFAULT_TENANT = 'default';
export const GLOBAL_TENANT_RENDERING_TEXT = 'Global';
export const PRIVATE_TENANT_RENDERING_TEXT = 'Private';
export const globalTenantName = 'global_tenant';

export const MAX_LENGTH_OF_COOKIE_BYTES = 4000;
export const ESTIMATED_IRON_COOKIE_OVERHEAD = 1.5;
export enum AuthType {
BASIC = 'basicauth',
OPEN_ID = 'openid',
Expand Down
18 changes: 14 additions & 4 deletions server/auth/types/authentication_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export abstract class AuthenticationType implements IAuthenticationType {
cookie = undefined;
}

if (!cookie || !(await this.isValidCookie(cookie))) {
if (!cookie || !(await this.isValidCookie(cookie, request))) {
// clear cookie
this.sessionStorageFactory.asScoped(request).clear();

Expand All @@ -140,7 +140,7 @@ export abstract class AuthenticationType implements IAuthenticationType {
}
// cookie is valid
// build auth header
const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!);
const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!, request);
Object.assign(authHeaders, authHeadersFromCookie);
const additonalAuthHeader = this.getAdditionalAuthHeader(request);
Object.assign(authHeaders, additonalAuthHeader);
Expand Down Expand Up @@ -236,11 +236,21 @@ export abstract class AuthenticationType implements IAuthenticationType {
request: OpenSearchDashboardsRequest,
authInfo: any
): SecuritySessionCookie;
protected abstract async isValidCookie(cookie: SecuritySessionCookie): Promise<boolean>;

public abstract isValidCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
): Promise<boolean>;

protected abstract handleUnauthedRequest(
request: OpenSearchDashboardsRequest,
response: LifecycleResponseFactory,
toolkit: AuthToolkit
): IOpenSearchDashboardsResponse | AuthResult;
protected abstract buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any;

public abstract buildAuthHeaderFromCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
): any;
public abstract init(): Promise<void>;
}
120 changes: 120 additions & 0 deletions server/auth/types/openid/openid_auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright OpenSearch Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';

import { OpenSearchDashboardsRequest } from '../../../../../../src/core/server/http/router';

import { OpenIdAuthentication } from './openid_auth';
import { SecurityPluginConfigType } from '../../../index';
import { SecuritySessionCookie } from '../../../session/security_cookie';
import { deflateValue } from '../../../utils/compression';
import {
IRouter,
CoreSetup,
ILegacyClusterClient,
Logger,
SessionStorageFactory,
} from '../../../../../../src/core/server';

describe('test OpenId authHeaderValue', () => {
let router: IRouter;
let core: CoreSetup;
let esClient: ILegacyClusterClient;
let sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie>;
let logger: Logger;

// Consistent with auth_handler_factory.test.ts
beforeEach(() => {});

const config = ({
openid: {
header: 'authorization',
scope: [],
extra_storage: {
cookie_prefix: 'testcookie',
additional_cookies: 5,
},
},
} as unknown) as SecurityPluginConfigType;

test('make sure that cookies with authHeaderValue are still valid', async () => {
const openIdAuthentication = new OpenIdAuthentication(
config,
sessionStorageFactory,
router,
esClient,
core,
logger
);

const mockRequest = httpServerMock.createRawRequest();
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);

const cookie: SecuritySessionCookie = {
credentials: {
authHeaderValue: 'Bearer eyToken',
},
};

const expectedHeaders = {
authorization: 'Bearer eyToken',
};

const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);

expect(headers).toEqual(expectedHeaders);
});

test('get authHeaderValue from split cookies', async () => {
const openIdAuthentication = new OpenIdAuthentication(
config,
sessionStorageFactory,
router,
esClient,
core,
logger
);

const testString = 'Bearer eyCombinedToken';
const testStringBuffer: Buffer = deflateValue(testString);
const cookieValue = testStringBuffer.toString('base64');
const cookiePrefix = config.openid!.extra_storage.cookie_prefix;
const splitValueAt = Math.ceil(
cookieValue.length / config.openid!.extra_storage.additional_cookies
);
const mockRequest = httpServerMock.createRawRequest({
state: {
[cookiePrefix + '1']: cookieValue.substring(0, splitValueAt),
[cookiePrefix + '2']: cookieValue.substring(splitValueAt),
},
});
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);

const cookie: SecuritySessionCookie = {
credentials: {
authHeaderValueExtra: true,
},
};

const expectedHeaders = {
authorization: testString,
};

const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);

expect(headers).toEqual(expectedHeaders);
});
});
95 changes: 90 additions & 5 deletions server/auth/types/openid/openid_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ import {
import HTTP from 'http';
import HTTPS from 'https';
import { PeerCertificate } from 'tls';
import { Server, ServerStateCookieOptions } from '@hapi/hapi';
import { SecurityPluginConfigType } from '../../..';
import { SecuritySessionCookie } from '../../../session/security_cookie';
import { OpenIdAuthRoutes } from './routes';
import { AuthenticationType } from '../authentication_type';
import { callTokenEndpoint } from './helper';
import { composeNextUrlQeuryParam } from '../../../utils/next_url';
import {
ExtraAuthStorageOptions,
getExtraAuthStorageValue,
setExtraAuthStorage,
} from '../../../session/cookie_splitter';

export interface OpenIdAuthConfig {
authorizationEndpoint?: string;
Expand Down Expand Up @@ -93,6 +99,8 @@ export class OpenIdAuthentication extends AuthenticationType {
this.openIdAuthConfig.tokenEndpoint = payload.token_endpoint;
this.openIdAuthConfig.endSessionEndpoint = payload.end_session_endpoint || undefined;

this.createExtraStorage();

const routes = new OpenIdAuthRoutes(
this.router,
this.config,
Expand Down Expand Up @@ -135,6 +143,37 @@ export class OpenIdAuthentication extends AuthenticationType {
}
}

createExtraStorage() {
// @ts-ignore
const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server;

const extraCookiePrefix = this.config.openid!.extra_storage.cookie_prefix;
const extraCookieSettings: ServerStateCookieOptions = {
isSecure: this.config.cookie.secure,
isSameSite: this.config.cookie.isSameSite,
password: this.config.cookie.password,
domain: this.config.cookie.domain,
path: this.coreSetup.http.basePath.serverBasePath || '/',
clearInvalid: false,
isHttpOnly: true,
ignoreErrors: true,
encoding: 'iron', // Same as hapi auth cookie
};

for (let i = 1; i <= this.config.openid!.extra_storage.additional_cookies; i++) {
hapiServer.states.add(extraCookiePrefix + i, extraCookieSettings);
}
}

private getExtraAuthStorageOptions(): ExtraAuthStorageOptions {
// If we're here, we will always have the openid configuration
return {
cookiePrefix: this.config.openid!.extra_storage.cookie_prefix,
additionalCookies: this.config.openid!.extra_storage.additional_cookies,
logger: this.logger,
};
}

requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean {
return request.headers.authorization ? true : false;
}
Expand All @@ -144,27 +183,37 @@ export class OpenIdAuthentication extends AuthenticationType {
}

getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
setExtraAuthStorage(
request,
request.headers.authorization as string,
this.getExtraAuthStorageOptions()
);

return {
username: authInfo.user_name,
credentials: {
authHeaderValue: request.headers.authorization,
authHeaderValueExtra: true,
},
authType: this.type,
expiryTime: Date.now() + this.config.session.ttl,
};
}

// TODO: Add token expiration check here
async isValidCookie(cookie: SecuritySessionCookie): Promise<boolean> {
async isValidCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
): Promise<boolean> {
if (
cookie.authType !== this.type ||
!cookie.username ||
!cookie.expiryTime ||
!cookie.credentials?.authHeaderValue ||
(!cookie.credentials?.authHeaderValue && !this.getExtraAuthStorageValue(request, cookie)) ||
!cookie.credentials?.expires_at
) {
return false;
}

if (cookie.credentials?.expires_at > Date.now()) {
return true;
}
Expand All @@ -187,10 +236,17 @@ export class OpenIdAuthentication extends AuthenticationType {
// if no id_token from refresh token call, maybe the Idp doesn't allow refresh id_token
if (refreshTokenResponse.idToken) {
cookie.credentials = {
authHeaderValue: `Bearer ${refreshTokenResponse.idToken}`,
authHeaderValueExtra: true,
refresh_token: refreshTokenResponse.refreshToken,
expires_at: Date.now() + refreshTokenResponse.expiresIn! * 1000, // expiresIn is in second
};

setExtraAuthStorage(
request,
`Bearer ${refreshTokenResponse.idToken}`,
this.getExtraAuthStorageOptions()
);

return true;
} else {
return false;
Expand Down Expand Up @@ -226,8 +282,37 @@ export class OpenIdAuthentication extends AuthenticationType {
}
}

buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) {
let extraValue = '';
if (!cookie.credentials?.authHeaderValueExtra) {
return extraValue;
}

try {
extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions());
} catch (error) {
this.logger.info(error);
}

return extraValue;
}

buildAuthHeaderFromCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
): any {
const header: any = {};
if (cookie.credentials.authHeaderValueExtra) {
try {
const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie);
header.authorization = extraAuthStorageValue;
return header;
} catch (error) {
this.logger.error(error);
// TODO Re-throw?
// throw error;
}
}
const authHeaderValue = cookie.credentials?.authHeaderValue;
if (authHeaderValue) {
header.authorization = authHeaderValue;
Expand Down
Loading

0 comments on commit 335c1da

Please sign in to comment.