Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cookie expiry issues from IDP/JWT auth methods, disables keepalive for JWT/IDP #1773

Merged
merged 31 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
183b502
Fix SAML timeout issues when keepalive is true
derek-ho Feb 8, 2024
a1848b0
lint
derek-ho Feb 8, 2024
9afd31e
Fix oidc flow
derek-ho Feb 9, 2024
b621d10
Merge branch 'main' of github.com:opensearch-project/security-dashboa…
derek-ho Feb 13, 2024
1da9739
Add keep alive test
derek-ho Feb 13, 2024
cf0b857
Lint
derek-ho Feb 13, 2024
bc96a3f
Add openid valid cookie test
derek-ho Feb 13, 2024
5883c80
Push up stale work
derek-ho Feb 13, 2024
278c40d
remove const assignment
derek-ho Feb 13, 2024
364827f
Introduce abstract method to block jwt implementations from keep alive
derek-ho Feb 13, 2024
8e3f434
Lint
derek-ho Feb 13, 2024
cf2b4ae
Fix compile issues
derek-ho Feb 13, 2024
52fd304
Fix warnings
derek-ho Feb 15, 2024
b9528e8
Lint
derek-ho Feb 15, 2024
1202368
Add test for JWT and fix warnings
derek-ho Feb 16, 2024
934ca76
Remove unused testing code
derek-ho Feb 16, 2024
804cce7
Remove test after PR changed
derek-ho Feb 16, 2024
3b1d5d3
Merge branch 'main' of github.com:opensearch-project/security-dashboa…
derek-ho Feb 19, 2024
529ded1
Refactor keep alive for JWT and no-op for SAML, OIDC
derek-ho Feb 19, 2024
0549a52
Lint
derek-ho Feb 19, 2024
ace02e2
Refactor test for readability
derek-ho Feb 19, 2024
6b76c4c
Add tests for each auth type
derek-ho Feb 19, 2024
275d569
Fix test to make it clear that it is taking value from date.now + ttl
derek-ho Feb 19, 2024
40878d8
Remove console log
derek-ho Feb 21, 2024
30ff169
PR feedback
derek-ho Feb 21, 2024
4c1e448
Lint
derek-ho Feb 21, 2024
1c542fc
Test fixes
derek-ho Feb 22, 2024
8721eef
Merge branch 'main' into idp-timeout
derek-ho Feb 22, 2024
d426aae
Fix variable names
derek-ho Feb 22, 2024
da0f7b9
Merge branch 'main' of github.com:opensearch-project/security-dashboa…
derek-ho Feb 22, 2024
5e7e45b
Merge branch 'idp-timeout' of github.com:derek-ho/security-dashboards…
derek-ho Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions server/auth/types/authentication_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,23 @@
* permissions and limitations under the License.
*/

/* eslint-disable max-classes-per-file */
// This file has extra classes used for testing

import { SecurityPluginConfigType } from '../..';
import { AuthenticationType } from './authentication_type';
import { httpServerMock } from '../../../../../src/core/server/mocks';
import {
SessionStorageFactory,
SessionStorage,
OpenSearchDashboardsRequest,
} from '../../../../../src/core/server';
import { SecuritySessionCookie } from '../../session/security_cookie';

class DummyAuthType extends AuthenticationType {
authNotRequired(request: OpenSearchDashboardsRequest): boolean {
return false;
}
buildAuthHeaderFromCookie() {}
getAdditionalAuthHeader() {}
handleUnauthedRequest() {}
Expand All @@ -33,6 +45,46 @@ class DummyAuthType extends AuthenticationType {
resolveTenant(): Promise<string | undefined> {
return Promise.resolve('dummy_tenant');
}
public supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
return Promise.resolve(true);
}
}

// Implementation of SessionStorage using browser's sessionStorage
class BrowserSessionStorage<T> implements SessionStorage<T> {
private readonly storageKey: string;

constructor(storageKey: string) {
this.storageKey = storageKey;
}

async get(): Promise<T | null> {
const storedValue = sessionStorage.getItem(this.storageKey);
return storedValue ? JSON.parse(storedValue) : null;
}

set(sessionValue: T): void {
const serializedValue = JSON.stringify(sessionValue);
sessionStorage.setItem(this.storageKey, serializedValue);
}

clear(): void {
sessionStorage.removeItem(this.storageKey);
}
}

// Implementation of SessionStorageFactory using the browser's sessionStorage
class BrowserSessionStorageFactory<T> implements SessionStorageFactory<T> {
private readonly storageKey: string;

constructor(storageKey: string) {
this.storageKey = storageKey;
}

// This method returns a new instance of the browser's SessionStorage for each request
asScoped(request: OpenSearchDashboardsRequest): SessionStorage<T> {
return new BrowserSessionStorage<T>(this.storageKey);
}
}

describe('test tenant header', () => {
Expand Down Expand Up @@ -106,4 +158,52 @@ describe('test tenant header', () => {
const result = await dummyAuthType.authHandler(request, response, toolkit);
expect(result.requestHeaders.securitytenant).toEqual('dummy_tenant');
});

it(`keepalive should not shorten the cookie expiry`, async () => {
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
const realDateNow = Date.now.bind(global.Date);
const dateNowStub = jest.fn(() => 0);
global.Date.now = dateNowStub;

const keepAliveConfig = {
multitenancy: {
enabled: true,
},
auth: {
unauthenticated_routes: [] as string[],
},
session: {
keepalive: true,
ttl: 1000,
},
} as SecurityPluginConfigType;
const keepAliveDummyAuth = new DummyAuthType(
keepAliveConfig,
new BrowserSessionStorageFactory('security_cookie'),
router,
esClient,
coreSetup,
logger
);
const testCookie: SecuritySessionCookie = {
credentials: {
authHeaderValueExtra: true,
},
expiryTime: 2000,
};
// Set cookie
sessionStorage.setItem('security_cookie', JSON.stringify(testCookie));
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/internal/v1',
});
const response = jest.fn();
const toolkit = {
authenticated: jest.fn((value) => value),
};
await keepAliveDummyAuth.authHandler(request, response, toolkit);
const cookieAfterRequest = sessionStorage.getItem('security_cookie');
expect(JSON.parse(cookieAfterRequest!).expiryTime).toBe(2000);
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
global.Date.now = realDateNow;
});
});

/* eslint-enable max-classes-per-file */
5 changes: 3 additions & 2 deletions server/auth/types/authentication_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export abstract class AuthenticationType implements IAuthenticationType {
}

// extend session expiration time
if (this.config.session.keepalive) {
cookie!.expiryTime = Date.now() + this.config.session.ttl;
if (this.supportsKeepAlive(request) && this.config.session.keepalive) {
cookie!.expiryTime = Math.max(Date.now() + this.config.session.ttl, cookie.expiryTime || 0);
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
this.sessionStorageFactory.asScoped(request).set(cookie!);
}
// cookie is valid
Expand Down Expand Up @@ -277,6 +277,7 @@ export abstract class AuthenticationType implements IAuthenticationType {
request: OpenSearchDashboardsRequest,
authInfo: any
): SecuritySessionCookie;
public abstract supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean>;
public abstract isValidCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
Expand Down
4 changes: 4 additions & 0 deletions server/auth/types/basic/basic_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export class BasicAuthentication extends AuthenticationType {
return request.headers[AUTH_HEADER_NAME] ? true : false;
}

public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
return true;
}

async getAdditionalAuthHeader(
request: OpenSearchDashboardsRequest<unknown, unknown, unknown, any>
): Promise<any> {
Expand Down
5 changes: 5 additions & 0 deletions server/auth/types/jwt/jwt_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ export class JwtAuthentication extends AuthenticationType {
);
}

// JWT auth types should get expiry from the JWT, and not override this value
public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
return false;
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
}

handleUnauthedRequest(
request: OpenSearchDashboardsRequest,
response: LifecycleResponseFactory,
Expand Down
17 changes: 16 additions & 1 deletion server/auth/types/multiple/multi_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
LifecycleResponseFactory,
AuthToolkit,
} from '../../../../opensearch-dashboards/server';
import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router';
import {
OpenSearchDashboardsRequest,
OpenSearchDashboardsResponse,
} from '../../../../../../src/core/server/http/router';
import { SecurityPluginConfigType } from '../../..';
import { AuthenticationType, IAuthenticationType } from '../authentication_type';
import { ANONYMOUS_AUTH_LOGIN, AuthType, LOGIN_PAGE_URI } from '../../../../common';
Expand Down Expand Up @@ -130,6 +133,18 @@ export class MultipleAuthentication extends AuthenticationType {
return {};
}

public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
const cookie = await this.sessionStorageFactory.asScoped(request).get();
const reqAuthType = cookie?.authType?.toLowerCase();

if (reqAuthType && this.authHandlers.has(reqAuthType)) {
return this.authHandlers.get(reqAuthType)!.supportsKeepAlive(request);
} else {
// default to true
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
return true;
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
}
}

async isValidCookie(
cookie: SecuritySessionCookie,
request: OpenSearchDashboardsRequest
Expand Down
39 changes: 39 additions & 0 deletions server/auth/types/openid/openid_auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,43 @@ describe('test OpenId authHeaderValue', () => {
expect(wreckHttpsOptions.cert).toBeUndefined();
expect(wreckHttpsOptions.passphrase).toBeUndefined();
});

test('Ensure expiryTime is being used to test validity of cookie', async () => {
const realDateNow = Date.now.bind(global.Date);
const dateNowStub = jest.fn(() => 0);
global.Date.now = dateNowStub;
const customConfig = {
openid: {
pfx: 'test/certs/keyStore.p12',
certificate: 'test/certs/cert.pem',
private_key: 'test/certs/private-key.pem',
passphrase: '',
header: 'authorization',
scope: [],
},
};

const openidConfig = (customConfig as unknown) as SecurityPluginConfigType;

const openIdAuthentication = new OpenIdAuthentication(
openidConfig,
sessionStorageFactory,
router,
esClient,
core,
logger
);
const testCookie: SecuritySessionCookie = {
credentials: {
authHeaderValue: 'Bearer eyToken',
expiry_time: -1,
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
},
expiryTime: 2000,
username: 'admin',
authType: 'openid',
};

expect(await openIdAuthentication.isValidCookie(testCookie, {})).toBe(true);
global.Date.now = realDateNow;
});
});
12 changes: 8 additions & 4 deletions server/auth/types/openid/openid_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ export class OpenIdAuthentication extends AuthenticationType {
};
}

// JWT auth types should get expiry from the JWT, and not override this value
public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
return false;
}

// TODO: Add token expiration check here
async isValidCookie(
cookie: SecuritySessionCookie,
Expand All @@ -260,13 +265,12 @@ export class OpenIdAuthentication extends AuthenticationType {
cookie.authType !== this.type ||
!cookie.username ||
!cookie.expiryTime ||
(!cookie.credentials?.authHeaderValue && !this.getExtraAuthStorageValue(request, cookie)) ||
!cookie.credentials?.expires_at
(!cookie.credentials?.authHeaderValue && !this.getExtraAuthStorageValue(request, cookie))
) {
return false;
}

if (cookie.credentials?.expires_at > Date.now()) {
if (cookie.expiryTime > Date.now()) {
return true;
}

Expand All @@ -290,8 +294,8 @@ export class OpenIdAuthentication extends AuthenticationType {
cookie.credentials = {
authHeaderValueExtra: true,
refresh_token: refreshTokenResponse.refreshToken,
expires_at: getExpirationDate(refreshTokenResponse), // expiresIn is in second
};
cookie.expiryTime = getExpirationDate(refreshTokenResponse);

setExtraAuthStorage(
request,
Expand Down
3 changes: 1 addition & 2 deletions server/auth/types/openid/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,9 @@ export class OpenIdAuthRoutes {
username: user.username,
credentials: {
authHeaderValueExtra: true,
expires_at: getExpirationDate(tokenResponse),
},
authType: AuthType.OPEN_ID,
expiryTime: Date.now() + this.config.session.ttl,
expiryTime: getExpirationDate(tokenResponse),
};
if (this.config.openid?.refresh_tokens && tokenResponse.refreshToken) {
Object.assign(sessionStorage.credentials, {
Expand Down
4 changes: 4 additions & 0 deletions server/auth/types/proxy/proxy_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export class ProxyAuthentication extends AuthenticationType {
: false;
}

public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

async getAdditionalAuthHeader(request: OpenSearchDashboardsRequest): Promise<any> {
const authHeaders: any = {};
const customProxyHeader = this.config.proxycache?.proxy_header;
Expand Down
5 changes: 5 additions & 0 deletions server/auth/types/saml/saml_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ export class SamlAuthentication extends AuthenticationType {
return {};
}

// JWT auth types should get expiry from the JWT, and not override this value
derek-ho marked this conversation as resolved.
Show resolved Hide resolved
public async supportsKeepAlive(request: OpenSearchDashboardsRequest): Promise<boolean> {
return false;
}

getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
const authorizationHeaderValue: string = request.headers[
SamlAuthentication.AUTH_HEADER_NAME
Expand Down
Loading