Skip to content

Commit a999f35

Browse files
jochen-kressinstephen-crawford
authored andcommitted
Split up a value into multiple cookie payloads (opensearch-project#1352)
* PoC for splitting up a value into multiple cookie payloads Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * Cookie splitting for OpenId and SAML Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * Changes after review comments Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * WIP: First unit tests Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * More unit tests Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * Fix for multi auth, request argument was missing Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * Fixed linting errors Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> * Added one additional cookie for the SAML integration tests Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> --------- Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Signed-off-by: leanneeliatra <leanne.laceybyrne@eliatra.com>
1 parent c3c88e4 commit a999f35

15 files changed

+927
-30
lines changed

common/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const PRIVATE_TENANT_RENDERING_TEXT = 'Private';
5050
export const globalTenantName = 'global_tenant';
5151

5252
export const MAX_INTEGER = 2147483647;
53+
export const MAX_LENGTH_OF_COOKIE_BYTES = 4000;
54+
export const ESTIMATED_IRON_COOKIE_OVERHEAD = 1.5;
5355

5456
export enum AuthType {
5557
BASIC = 'basicauth',

server/auth/types/authentication_type.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export abstract class AuthenticationType implements IAuthenticationType {
138138
cookie = undefined;
139139
}
140140

141-
if (!cookie || !(await this.isValidCookie(cookie))) {
141+
if (!cookie || !(await this.isValidCookie(cookie, request))) {
142142
// clear cookie
143143
this.sessionStorageFactory.asScoped(request).clear();
144144

@@ -160,7 +160,7 @@ export abstract class AuthenticationType implements IAuthenticationType {
160160
}
161161
// cookie is valid
162162
// build auth header
163-
const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!);
163+
const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!, request);
164164
Object.assign(authHeaders, authHeadersFromCookie);
165165
const additonalAuthHeader = await this.getAdditionalAuthHeader(request);
166166
Object.assign(authHeaders, additonalAuthHeader);
@@ -269,12 +269,18 @@ export abstract class AuthenticationType implements IAuthenticationType {
269269
request: OpenSearchDashboardsRequest,
270270
authInfo: any
271271
): SecuritySessionCookie;
272-
public abstract isValidCookie(cookie: SecuritySessionCookie): Promise<boolean>;
272+
public abstract isValidCookie(
273+
cookie: SecuritySessionCookie,
274+
request: OpenSearchDashboardsRequest
275+
): Promise<boolean>;
273276
protected abstract handleUnauthedRequest(
274277
request: OpenSearchDashboardsRequest,
275278
response: LifecycleResponseFactory,
276279
toolkit: AuthToolkit
277280
): IOpenSearchDashboardsResponse | AuthResult;
278-
public abstract buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any;
281+
public abstract buildAuthHeaderFromCookie(
282+
cookie: SecuritySessionCookie,
283+
request: OpenSearchDashboardsRequest
284+
): any;
279285
public abstract init(): Promise<void>;
280286
}

server/auth/types/multiple/multi_auth.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,13 @@ export class MultipleAuthentication extends AuthenticationType {
130130
return {};
131131
}
132132

133-
async isValidCookie(cookie: SecuritySessionCookie): Promise<boolean> {
133+
async isValidCookie(
134+
cookie: SecuritySessionCookie,
135+
request: OpenSearchDashboardsRequest
136+
): Promise<boolean> {
134137
const reqAuthType = cookie?.authType?.toLowerCase();
135138
if (reqAuthType && this.authHandlers.has(reqAuthType)) {
136-
return this.authHandlers.get(reqAuthType)!.isValidCookie(cookie);
139+
return this.authHandlers.get(reqAuthType)!.isValidCookie(cookie, request);
137140
} else {
138141
return false;
139142
}
@@ -168,11 +171,14 @@ export class MultipleAuthentication extends AuthenticationType {
168171
}
169172
}
170173

171-
buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
174+
buildAuthHeaderFromCookie(
175+
cookie: SecuritySessionCookie,
176+
request: OpenSearchDashboardsRequest
177+
): any {
172178
const reqAuthType = cookie?.authType?.toLowerCase();
173179

174180
if (reqAuthType && this.authHandlers.has(reqAuthType)) {
175-
return this.authHandlers.get(reqAuthType)!.buildAuthHeaderFromCookie(cookie);
181+
return this.authHandlers.get(reqAuthType)!.buildAuthHeaderFromCookie(cookie, request);
176182
} else {
177183
return {};
178184
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
17+
18+
import { OpenSearchDashboardsRequest } from '../../../../../../src/core/server/http/router';
19+
20+
import { OpenIdAuthentication } from './openid_auth';
21+
import { SecurityPluginConfigType } from '../../../index';
22+
import { SecuritySessionCookie } from '../../../session/security_cookie';
23+
import { deflateValue } from '../../../utils/compression';
24+
import {
25+
IRouter,
26+
CoreSetup,
27+
ILegacyClusterClient,
28+
Logger,
29+
SessionStorageFactory,
30+
} from '../../../../../../src/core/server';
31+
32+
describe('test OpenId authHeaderValue', () => {
33+
let router: IRouter;
34+
let core: CoreSetup;
35+
let esClient: ILegacyClusterClient;
36+
let sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie>;
37+
let logger: Logger;
38+
39+
// Consistent with auth_handler_factory.test.ts
40+
beforeEach(() => {});
41+
42+
const config = ({
43+
openid: {
44+
header: 'authorization',
45+
scope: [],
46+
extra_storage: {
47+
cookie_prefix: 'testcookie',
48+
additional_cookies: 5,
49+
},
50+
},
51+
} as unknown) as SecurityPluginConfigType;
52+
53+
test('make sure that cookies with authHeaderValue are still valid', async () => {
54+
const openIdAuthentication = new OpenIdAuthentication(
55+
config,
56+
sessionStorageFactory,
57+
router,
58+
esClient,
59+
core,
60+
logger
61+
);
62+
63+
const mockRequest = httpServerMock.createRawRequest();
64+
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);
65+
66+
const cookie: SecuritySessionCookie = {
67+
credentials: {
68+
authHeaderValue: 'Bearer eyToken',
69+
},
70+
};
71+
72+
const expectedHeaders = {
73+
authorization: 'Bearer eyToken',
74+
};
75+
76+
const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);
77+
78+
expect(headers).toEqual(expectedHeaders);
79+
});
80+
81+
test('get authHeaderValue from split cookies', async () => {
82+
const openIdAuthentication = new OpenIdAuthentication(
83+
config,
84+
sessionStorageFactory,
85+
router,
86+
esClient,
87+
core,
88+
logger
89+
);
90+
91+
const testString = 'Bearer eyCombinedToken';
92+
const testStringBuffer: Buffer = deflateValue(testString);
93+
const cookieValue = testStringBuffer.toString('base64');
94+
const cookiePrefix = config.openid!.extra_storage.cookie_prefix;
95+
const splitValueAt = Math.ceil(
96+
cookieValue.length / config.openid!.extra_storage.additional_cookies
97+
);
98+
const mockRequest = httpServerMock.createRawRequest({
99+
state: {
100+
[cookiePrefix + '1']: cookieValue.substring(0, splitValueAt),
101+
[cookiePrefix + '2']: cookieValue.substring(splitValueAt),
102+
},
103+
});
104+
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);
105+
106+
const cookie: SecuritySessionCookie = {
107+
credentials: {
108+
authHeaderValueExtra: true,
109+
},
110+
};
111+
112+
const expectedHeaders = {
113+
authorization: testString,
114+
};
115+
116+
const headers = openIdAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);
117+
118+
expect(headers).toEqual(expectedHeaders);
119+
});
120+
});

server/auth/types/openid/openid_auth.ts

+91-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import HTTP from 'http';
3030
import HTTPS from 'https';
3131
import { PeerCertificate } from 'tls';
32+
import { Server, ServerStateCookieOptions } from '@hapi/hapi';
3233
import { SecurityPluginConfigType } from '../../..';
3334
import { SecuritySessionCookie } from '../../../session/security_cookie';
3435
import { OpenIdAuthRoutes } from './routes';
@@ -37,6 +38,11 @@ import { callTokenEndpoint } from './helper';
3738
import { composeNextUrlQueryParam } from '../../../utils/next_url';
3839
import { getExpirationDate } from './helper';
3940
import { AuthType, OPENID_AUTH_LOGIN } from '../../../../common';
41+
import {
42+
ExtraAuthStorageOptions,
43+
getExtraAuthStorageValue,
44+
setExtraAuthStorage,
45+
} from '../../../session/cookie_splitter';
4046

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

102+
this.createExtraStorage();
103+
96104
const routes = new OpenIdAuthRoutes(
97105
this.router,
98106
this.config,
@@ -102,6 +110,7 @@ export class OpenIdAuthentication extends AuthenticationType {
102110
this.coreSetup,
103111
this.wreckClient
104112
);
113+
105114
routes.setupRoutes();
106115
} catch (error: any) {
107116
this.logger.error(error); // TODO: log more info
@@ -135,6 +144,37 @@ export class OpenIdAuthentication extends AuthenticationType {
135144
}
136145
}
137146

147+
createExtraStorage() {
148+
// @ts-ignore
149+
const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server;
150+
151+
const extraCookiePrefix = this.config.openid!.extra_storage.cookie_prefix;
152+
const extraCookieSettings: ServerStateCookieOptions = {
153+
isSecure: this.config.cookie.secure,
154+
isSameSite: this.config.cookie.isSameSite,
155+
password: this.config.cookie.password,
156+
domain: this.config.cookie.domain,
157+
path: this.coreSetup.http.basePath.serverBasePath || '/',
158+
clearInvalid: false,
159+
isHttpOnly: true,
160+
ignoreErrors: true,
161+
encoding: 'iron', // Same as hapi auth cookie
162+
};
163+
164+
for (let i = 1; i <= this.config.openid!.extra_storage.additional_cookies; i++) {
165+
hapiServer.states.add(extraCookiePrefix + i, extraCookieSettings);
166+
}
167+
}
168+
169+
private getExtraAuthStorageOptions(): ExtraAuthStorageOptions {
170+
// If we're here, we will always have the openid configuration
171+
return {
172+
cookiePrefix: this.config.openid!.extra_storage.cookie_prefix,
173+
additionalCookies: this.config.openid!.extra_storage.additional_cookies,
174+
logger: this.logger,
175+
};
176+
}
177+
138178
requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean {
139179
return request.headers.authorization ? true : false;
140180
}
@@ -144,27 +184,37 @@ export class OpenIdAuthentication extends AuthenticationType {
144184
}
145185

146186
getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
187+
setExtraAuthStorage(
188+
request,
189+
request.headers.authorization as string,
190+
this.getExtraAuthStorageOptions()
191+
);
192+
147193
return {
148194
username: authInfo.user_name,
149195
credentials: {
150-
authHeaderValue: request.headers.authorization,
196+
authHeaderValueExtra: true,
151197
},
152198
authType: this.type,
153199
expiryTime: Date.now() + this.config.session.ttl,
154200
};
155201
}
156202

157203
// TODO: Add token expiration check here
158-
async isValidCookie(cookie: SecuritySessionCookie): Promise<boolean> {
204+
async isValidCookie(
205+
cookie: SecuritySessionCookie,
206+
request: OpenSearchDashboardsRequest
207+
): Promise<boolean> {
159208
if (
160209
cookie.authType !== this.type ||
161210
!cookie.username ||
162211
!cookie.expiryTime ||
163-
!cookie.credentials?.authHeaderValue ||
212+
(!cookie.credentials?.authHeaderValue && !this.getExtraAuthStorageValue(request, cookie)) ||
164213
!cookie.credentials?.expires_at
165214
) {
166215
return false;
167216
}
217+
168218
if (cookie.credentials?.expires_at > Date.now()) {
169219
return true;
170220
}
@@ -187,10 +237,17 @@ export class OpenIdAuthentication extends AuthenticationType {
187237
// if no id_token from refresh token call, maybe the Idp doesn't allow refresh id_token
188238
if (refreshTokenResponse.idToken) {
189239
cookie.credentials = {
190-
authHeaderValue: `Bearer ${refreshTokenResponse.idToken}`,
240+
authHeaderValueExtra: true,
191241
refresh_token: refreshTokenResponse.refreshToken,
192242
expires_at: getExpirationDate(refreshTokenResponse), // expiresIn is in second
193243
};
244+
245+
setExtraAuthStorage(
246+
request,
247+
`Bearer ${refreshTokenResponse.idToken}`,
248+
this.getExtraAuthStorageOptions()
249+
);
250+
194251
return true;
195252
} else {
196253
return false;
@@ -226,8 +283,37 @@ export class OpenIdAuthentication extends AuthenticationType {
226283
}
227284
}
228285

229-
buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
286+
getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) {
287+
let extraValue = '';
288+
if (!cookie.credentials?.authHeaderValueExtra) {
289+
return extraValue;
290+
}
291+
292+
try {
293+
extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions());
294+
} catch (error) {
295+
this.logger.info(error);
296+
}
297+
298+
return extraValue;
299+
}
300+
301+
buildAuthHeaderFromCookie(
302+
cookie: SecuritySessionCookie,
303+
request: OpenSearchDashboardsRequest
304+
): any {
230305
const header: any = {};
306+
if (cookie.credentials.authHeaderValueExtra) {
307+
try {
308+
const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie);
309+
header.authorization = extraAuthStorageValue;
310+
return header;
311+
} catch (error) {
312+
this.logger.error(error);
313+
// TODO Re-throw?
314+
// throw error;
315+
}
316+
}
231317
const authHeaderValue = cookie.credentials?.authHeaderValue;
232318
if (authHeaderValue) {
233319
header.authorization = authHeaderValue;

0 commit comments

Comments
 (0)