Skip to content

Commit f265cde

Browse files
jochen-kressingithub-actions[bot]
authored andcommitted
Cookie compression and splitting for JWT (#1651)
Signed-off-by: Jochen Kressin <jochen.kressin-gh@eliatra.com> Co-authored-by: Craig Perkins <cwperx@amazon.com> (cherry picked from commit 7cad47c)
1 parent 60c3ca8 commit f265cde

File tree

5 files changed

+293
-16
lines changed

5 files changed

+293
-16
lines changed

server/auth/types/jwt/jwt_auth.ts

+91-8
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,21 @@ import {
2525
AuthToolkit,
2626
IOpenSearchDashboardsResponse,
2727
} from 'opensearch-dashboards/server';
28+
import { ServerStateCookieOptions } from '@hapi/hapi';
2829
import { SecurityPluginConfigType } from '../../..';
2930
import { SecuritySessionCookie } from '../../../session/security_cookie';
3031
import { AuthenticationType } from '../authentication_type';
3132
import { JwtAuthRoutes } from './routes';
33+
import {
34+
ExtraAuthStorageOptions,
35+
getExtraAuthStorageValue,
36+
setExtraAuthStorage,
37+
} from '../../../session/cookie_splitter';
38+
39+
export const JWT_DEFAULT_EXTRA_STORAGE_OPTIONS: ExtraAuthStorageOptions = {
40+
cookiePrefix: 'security_authentication_jwt',
41+
additionalCookies: 5,
42+
};
3243

3344
export class JwtAuthentication extends AuthenticationType {
3445
public readonly type: string = 'jwt';
@@ -48,10 +59,47 @@ export class JwtAuthentication extends AuthenticationType {
4859
}
4960

5061
public async init() {
51-
const routes = new JwtAuthRoutes(this.router, this.sessionStorageFactory);
62+
this.createExtraStorage();
63+
const routes = new JwtAuthRoutes(this.router, this.sessionStorageFactory, this.config);
5264
routes.setupRoutes();
5365
}
5466

67+
createExtraStorage() {
68+
// @ts-ignore
69+
const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server;
70+
71+
const { cookiePrefix, additionalCookies } = this.getExtraAuthStorageOptions();
72+
const extraCookieSettings: ServerStateCookieOptions = {
73+
isSecure: this.config.cookie.secure,
74+
isSameSite: this.config.cookie.isSameSite,
75+
password: this.config.cookie.password,
76+
domain: this.config.cookie.domain,
77+
path: this.coreSetup.http.basePath.serverBasePath || '/',
78+
clearInvalid: false,
79+
isHttpOnly: true,
80+
ignoreErrors: true,
81+
encoding: 'iron', // Same as hapi auth cookie
82+
};
83+
84+
for (let i = 1; i <= additionalCookies; i++) {
85+
hapiServer.states.add(cookiePrefix + i, extraCookieSettings);
86+
}
87+
}
88+
89+
private getExtraAuthStorageOptions(): ExtraAuthStorageOptions {
90+
const extraAuthStorageOptions: ExtraAuthStorageOptions = {
91+
cookiePrefix:
92+
this.config.jwt?.extra_storage.cookie_prefix ||
93+
JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
94+
additionalCookies:
95+
this.config.jwt?.extra_storage.additional_cookies ||
96+
JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
97+
logger: this.logger,
98+
};
99+
100+
return extraAuthStorageOptions;
101+
}
102+
55103
private getTokenFromUrlParam(request: OpenSearchDashboardsRequest): string | undefined {
56104
const urlParamName = this.config.jwt?.url_param;
57105
if (urlParamName) {
@@ -77,6 +125,7 @@ export class JwtAuthentication extends AuthenticationType {
77125
if (request.headers[this.authHeaderName]) {
78126
return true;
79127
}
128+
80129
const urlParamName = this.config.jwt?.url_param;
81130
if (urlParamName && request.url.searchParams.get(urlParamName)) {
82131
return true;
@@ -100,22 +149,29 @@ export class JwtAuthentication extends AuthenticationType {
100149
request: OpenSearchDashboardsRequest<unknown, unknown, unknown, any>,
101150
authInfo: any
102151
): SecuritySessionCookie {
152+
setExtraAuthStorage(
153+
request,
154+
this.getBearerToken(request) || '',
155+
this.getExtraAuthStorageOptions()
156+
);
103157
return {
104158
username: authInfo.user_name,
105159
credentials: {
106-
authHeaderValue: this.getBearerToken(request),
160+
authHeaderValueExtra: true,
107161
},
108162
authType: this.type,
109163
expiryTime: Date.now() + this.config.session.ttl,
110164
};
111165
}
112166

113-
async isValidCookie(cookie: SecuritySessionCookie): Promise<boolean> {
167+
async isValidCookie(
168+
cookie: SecuritySessionCookie,
169+
request: OpenSearchDashboardsRequest
170+
): Promise<boolean> {
171+
const hasAuthHeaderValue =
172+
cookie.credentials?.authHeaderValue || this.getExtraAuthStorageValue(request, cookie);
114173
return (
115-
cookie.authType === this.type &&
116-
cookie.username &&
117-
cookie.expiryTime &&
118-
cookie.credentials?.authHeaderValue
174+
cookie.authType === this.type && cookie.username && cookie.expiryTime && hasAuthHeaderValue
119175
);
120176
}
121177

@@ -127,8 +183,35 @@ export class JwtAuthentication extends AuthenticationType {
127183
return response.unauthorized();
128184
}
129185

130-
buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
186+
getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) {
187+
let extraValue = '';
188+
if (!cookie.credentials?.authHeaderValueExtra) {
189+
return extraValue;
190+
}
191+
192+
try {
193+
extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions());
194+
} catch (error) {
195+
this.logger.info(error);
196+
}
197+
198+
return extraValue;
199+
}
200+
201+
buildAuthHeaderFromCookie(
202+
cookie: SecuritySessionCookie,
203+
request: OpenSearchDashboardsRequest
204+
): any {
131205
const header: any = {};
206+
if (cookie.credentials.authHeaderValueExtra) {
207+
try {
208+
const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie);
209+
header.authorization = extraAuthStorageValue;
210+
return header;
211+
} catch (error) {
212+
this.logger.error(error);
213+
}
214+
}
132215
const authHeaderValue = cookie.credentials?.authHeaderValue;
133216
if (authHeaderValue) {
134217
header[this.authHeaderName] = authHeaderValue;

server/auth/types/jwt/jwt_helper.test.ts

+133-4
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,58 @@
1414
*/
1515

1616
import { getAuthenticationHandler } from '../../auth_handler_factory';
17+
import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS } from './jwt_auth';
18+
import {
19+
CoreSetup,
20+
ILegacyClusterClient,
21+
IRouter,
22+
Logger,
23+
OpenSearchDashboardsRequest,
24+
SessionStorageFactory,
25+
} from '../../../../../../src/core/server';
26+
import { SecuritySessionCookie } from '../../../session/security_cookie';
27+
import { SecurityPluginConfigType } from '../../../index';
28+
import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
29+
import { deflateValue } from '../../../utils/compression';
1730

1831
describe('test jwt auth library', () => {
19-
const router: IRouter = { post: (body) => {} };
20-
let core: CoreSetup;
32+
const router: Partial<IRouter> = { post: (body) => {} };
33+
const core = {
34+
http: {
35+
basePath: {
36+
serverBasePath: '/',
37+
},
38+
},
39+
} as CoreSetup;
2140
let esClient: ILegacyClusterClient;
22-
let sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie>;
41+
const sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie> = {
42+
asScoped: jest.fn().mockImplementation(() => {
43+
return {
44+
server: {
45+
states: {
46+
add: jest.fn(),
47+
},
48+
},
49+
};
50+
}),
51+
};
2352
let logger: Logger;
2453

54+
const cookieConfig: Partial<SecurityPluginConfigType> = {
55+
cookie: {
56+
secure: false,
57+
name: 'test_cookie_name',
58+
password: 'secret',
59+
ttl: 60 * 60 * 1000,
60+
domain: null,
61+
isSameSite: false,
62+
},
63+
};
64+
2565
function getTestJWTAuthenticationHandlerWithConfig(config: SecurityPluginConfigType) {
2666
return getAuthenticationHandler(
2767
'jwt',
28-
router,
68+
router as IRouter,
2969
config,
3070
core,
3171
esClient,
@@ -36,9 +76,14 @@ describe('test jwt auth library', () => {
3676

3777
test('test getTokenFromUrlParam', async () => {
3878
const config = {
79+
...cookieConfig,
3980
jwt: {
4081
header: 'Authorization',
4182
url_param: 'authorization',
83+
extra_storage: {
84+
cookie_prefix: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
85+
additional_cookies: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
86+
},
4287
},
4388
};
4489
const auth = await getTestJWTAuthenticationHandlerWithConfig(config);
@@ -55,9 +100,14 @@ describe('test jwt auth library', () => {
55100

56101
test('test getTokenFromUrlParam incorrect url_param', async () => {
57102
const config = {
103+
...cookieConfig,
58104
jwt: {
59105
header: 'Authorization',
60106
url_param: 'urlParamName',
107+
extra_storage: {
108+
cookie_prefix: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
109+
additional_cookies: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
110+
},
61111
},
62112
};
63113
const auth = await getTestJWTAuthenticationHandlerWithConfig(config);
@@ -71,4 +121,83 @@ describe('test jwt auth library', () => {
71121
const token = auth.getTokenFromUrlParam(request);
72122
expect(token).toEqual(expectedToken);
73123
});
124+
125+
test('make sure that cookies with authHeaderValue instead of split cookies are still valid', async () => {
126+
const config = {
127+
...cookieConfig,
128+
jwt: {
129+
header: 'Authorization',
130+
url_param: 'authorization',
131+
extra_storage: {
132+
cookie_prefix: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
133+
additional_cookies: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
134+
},
135+
},
136+
} as SecurityPluginConfigType;
137+
138+
const jwtAuthentication = await getTestJWTAuthenticationHandlerWithConfig(config);
139+
140+
const mockRequest = httpServerMock.createRawRequest();
141+
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);
142+
143+
const cookie: SecuritySessionCookie = {
144+
credentials: {
145+
authHeaderValue: 'Bearer eyToken',
146+
},
147+
};
148+
149+
const expectedHeaders = {
150+
authorization: 'Bearer eyToken',
151+
};
152+
153+
const headers = jwtAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);
154+
155+
expect(headers).toEqual(expectedHeaders);
156+
});
157+
158+
test('get authHeaderValue from split cookies', async () => {
159+
const config = {
160+
...cookieConfig,
161+
jwt: {
162+
header: 'Authorization',
163+
url_param: 'authorization',
164+
extra_storage: {
165+
cookie_prefix: 'testcookie',
166+
additional_cookies: 2,
167+
},
168+
},
169+
} as SecurityPluginConfigType;
170+
171+
const jwtAuthentication = await getTestJWTAuthenticationHandlerWithConfig(config);
172+
173+
const testString = 'Bearer eyCombinedToken';
174+
const testStringBuffer: Buffer = deflateValue(testString);
175+
const cookieValue = testStringBuffer.toString('base64');
176+
const cookiePrefix = config.jwt!.extra_storage.cookie_prefix;
177+
const splitValueAt = Math.ceil(
178+
cookieValue.length / config.jwt!.extra_storage.additional_cookies
179+
);
180+
const mockRequest = httpServerMock.createRawRequest({
181+
state: {
182+
[cookiePrefix + '1']: cookieValue.substring(0, splitValueAt),
183+
[cookiePrefix + '2']: cookieValue.substring(splitValueAt),
184+
},
185+
});
186+
187+
const osRequest = OpenSearchDashboardsRequest.from(mockRequest);
188+
189+
const cookie: SecuritySessionCookie = {
190+
credentials: {
191+
authHeaderValueExtra: true,
192+
},
193+
};
194+
195+
const expectedHeaders = {
196+
authorization: testString,
197+
};
198+
199+
const headers = jwtAuthentication.buildAuthHeaderFromCookie(cookie, osRequest);
200+
201+
expect(headers).toEqual(expectedHeaders);
202+
});
74203
});

server/auth/types/jwt/routes.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,34 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16-
import { IRouter, SessionStorageFactory } from 'opensearch-dashboards/server';
16+
import { IRouter, Logger, SessionStorageFactory } from 'opensearch-dashboards/server';
1717
import { SecuritySessionCookie } from '../../../session/security_cookie';
1818
import { API_AUTH_LOGOUT, API_PREFIX } from '../../../../common';
19+
import { clearSplitCookies, ExtraAuthStorageOptions } from '../../../session/cookie_splitter';
20+
import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS } from './jwt_auth';
21+
import { SecurityPluginConfigType } from '../../../index';
1922

2023
export class JwtAuthRoutes {
2124
constructor(
2225
private readonly router: IRouter,
23-
private readonly sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie>
26+
private readonly sessionStorageFactory: SessionStorageFactory<SecuritySessionCookie>,
27+
private readonly config: SecurityPluginConfigType
2428
) {}
2529

30+
private getExtraAuthStorageOptions(logger?: Logger): ExtraAuthStorageOptions {
31+
const extraAuthStorageOptions: ExtraAuthStorageOptions = {
32+
cookiePrefix:
33+
this.config.jwt?.extra_storage.cookie_prefix ||
34+
JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
35+
additionalCookies:
36+
this.config.jwt?.extra_storage.additional_cookies ||
37+
JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
38+
logger,
39+
};
40+
41+
return extraAuthStorageOptions;
42+
}
43+
2644
public setupRoutes() {
2745
this.router.post(
2846
{
@@ -33,6 +51,7 @@ export class JwtAuthRoutes {
3351
},
3452
},
3553
async (context, request, response) => {
54+
await clearSplitCookies(request, this.getExtraAuthStorageOptions());
3655
this.sessionStorageFactory.asScoped(request).clear();
3756
return response.ok();
3857
}

server/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { schema, TypeOf } from '@osd/config-schema';
1717
import { PluginInitializerContext, PluginConfigDescriptor } from '../../../src/core/server';
1818
import { SecurityPlugin } from './plugin';
1919
import { AuthType } from '../common';
20+
import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS } from './auth/types/jwt/jwt_auth';
2021

2122
const validateAuthType = (value: string[]) => {
2223
const supportedAuthTypes = [
@@ -233,6 +234,16 @@ export const configSchema = schema.object({
233234
login_endpoint: schema.maybe(schema.string()),
234235
url_param: schema.string({ defaultValue: 'authorization' }),
235236
header: schema.string({ defaultValue: 'Authorization' }),
237+
extra_storage: schema.object({
238+
cookie_prefix: schema.string({
239+
defaultValue: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.cookiePrefix,
240+
minLength: 2,
241+
}),
242+
additional_cookies: schema.number({
243+
min: 1,
244+
defaultValue: JWT_DEFAULT_EXTRA_STORAGE_OPTIONS.additionalCookies,
245+
}),
246+
}),
236247
})
237248
),
238249
ui: schema.object({

0 commit comments

Comments
 (0)