diff --git a/server/auth/types/authentication_type.test.ts b/server/auth/types/authentication_type.test.ts index d3f6026bb..fdf0397cf 100644 --- a/server/auth/types/authentication_type.test.ts +++ b/server/auth/types/authentication_type.test.ts @@ -16,8 +16,12 @@ import { SecurityPluginConfigType } from '../..'; import { AuthenticationType } from './authentication_type'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { OpenSearchDashboardsRequest } from '../../../../../src/core/server'; class DummyAuthType extends AuthenticationType { + authNotRequired(request: OpenSearchDashboardsRequest): boolean { + return false; + } buildAuthHeaderFromCookie() {} getAdditionalAuthHeader() {} handleUnauthedRequest() {} diff --git a/server/auth/types/authentication_type.ts b/server/auth/types/authentication_type.ts index 3fef38175..8bde376ac 100755 --- a/server/auth/types/authentication_type.ts +++ b/server/auth/types/authentication_type.ts @@ -160,7 +160,7 @@ export abstract class AuthenticationType implements IAuthenticationType { // extend session expiration time if (this.config.session.keepalive) { - cookie!.expiryTime = Date.now() + this.config.session.ttl; + cookie!.expiryTime = this.getKeepAliveExpiry(cookie!, request); this.sessionStorageFactory.asScoped(request).set(cookie!); } // cookie is valid @@ -266,6 +266,13 @@ export abstract class AuthenticationType implements IAuthenticationType { }); } + public getKeepAliveExpiry( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): number { + return Date.now() + this.config.session.ttl; + } + isPageRequest(request: OpenSearchDashboardsRequest) { const path = request.url.pathname || '/'; return path.startsWith('/app/') || path === '/' || path.startsWith('/goto/'); @@ -286,5 +293,10 @@ export abstract class AuthenticationType implements IAuthenticationType { response: LifecycleResponseFactory, toolkit: AuthToolkit ): IOpenSearchDashboardsResponse | AuthResult; + public abstract requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean; + public abstract buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any; public abstract init(): Promise; } diff --git a/server/auth/types/basic/basic_auth.test.ts b/server/auth/types/basic/basic_auth.test.ts new file mode 100644 index 000000000..4698db354 --- /dev/null +++ b/server/auth/types/basic/basic_auth.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { SecurityPluginConfigType } from '../../../index'; +import { SecuritySessionCookie } from '../../../session/security_cookie'; +import { + IRouter, + CoreSetup, + ILegacyClusterClient, + Logger, + SessionStorageFactory, +} from '../../../../../../src/core/server'; +import { BasicAuthentication } from './basic_auth'; + +describe('Basic auth tests', () => { + let router: IRouter; + let core: CoreSetup; + let esClient: ILegacyClusterClient; + let sessionStorageFactory: SessionStorageFactory; + let logger: Logger; + + const config = { + session: { + ttl: 1000, + }, + } as SecurityPluginConfigType; + + test('getKeepAliveExpiry', () => { + const realDateNow = Date.now.bind(global.Date); + const dateNowStub = jest.fn(() => 0); + global.Date.now = dateNowStub; + const basicAuthentication = new BasicAuthentication( + config, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + expiryTime: 0, + }; + + const request = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + }); + + expect(basicAuthentication.getKeepAliveExpiry(cookie, request)).toBe(1000); + global.Date.now = realDateNow; + }); +}); diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts index af7a8727f..f21f86827 100644 --- a/server/auth/types/basic/basic_auth.ts +++ b/server/auth/types/basic/basic_auth.ts @@ -133,7 +133,10 @@ export class BasicAuthentication extends AuthenticationType { } } - buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any { + buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any { if (this.config.auth.anonymous_auth_enabled && cookie.isAnonymousAuth) { return {}; } diff --git a/server/auth/types/jwt/jwt_auth.ts b/server/auth/types/jwt/jwt_auth.ts index 43f17708c..da1c13dc6 100644 --- a/server/auth/types/jwt/jwt_auth.ts +++ b/server/auth/types/jwt/jwt_auth.ts @@ -35,6 +35,7 @@ import { getExtraAuthStorageValue, setExtraAuthStorage, } from '../../../session/cookie_splitter'; +import { getExpirationDate } from './jwt_helper'; export const JWT_DEFAULT_EXTRA_STORAGE_OPTIONS: ExtraAuthStorageOptions = { cookiePrefix: 'security_authentication_jwt', @@ -154,13 +155,17 @@ export class JwtAuthentication extends AuthenticationType { this.getBearerToken(request) || '', this.getExtraAuthStorageOptions() ); + return { username: authInfo.user_name, credentials: { authHeaderValueExtra: true, }, authType: this.type, - expiryTime: Date.now() + this.config.session.ttl, + expiryTime: getExpirationDate( + this.getBearerToken(request), + Date.now() + this.config.session.ttl + ), }; } @@ -175,6 +180,13 @@ export class JwtAuthentication extends AuthenticationType { ); } + getKeepAliveExpiry(cookie: SecuritySessionCookie, request: OpenSearchDashboardsRequest): number { + return getExpirationDate( + this.buildAuthHeaderFromCookie(cookie, request)[this.authHeaderName], + Date.now() + this.config.session.ttl + ); + } + handleUnauthedRequest( request: OpenSearchDashboardsRequest, response: LifecycleResponseFactory, diff --git a/server/auth/types/jwt/jwt_helper.test.ts b/server/auth/types/jwt/jwt_helper.test.ts index 73dd0e0ab..f82621a62 100644 --- a/server/auth/types/jwt/jwt_helper.test.ts +++ b/server/auth/types/jwt/jwt_helper.test.ts @@ -14,7 +14,7 @@ */ import { getAuthenticationHandler } from '../../auth_handler_factory'; -import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS } from './jwt_auth'; +import { JWT_DEFAULT_EXTRA_STORAGE_OPTIONS, JwtAuthentication } from './jwt_auth'; import { CoreSetup, ILegacyClusterClient, @@ -27,41 +27,54 @@ import { SecuritySessionCookie } from '../../../session/security_cookie'; import { SecurityPluginConfigType } from '../../../index'; import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; import { deflateValue } from '../../../utils/compression'; +import { getExpirationDate } from './jwt_helper'; -describe('test jwt auth library', () => { - const router: Partial = { post: (body) => {} }; - const core = { - http: { - basePath: { - serverBasePath: '/', - }, +// TODO: add dependency to a JWT decode/encode library for easier test writing and reading +const JWT_TEST = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiZXhwIjo5MjA4NjkyMDAsIm5hbWUiOiJKb2huIERvZSIsInJvbGVzIjoiYWRtaW4ifQ.q8CtMfAeWOGDCGZ8UB8IIV-YM9hkDS8-pq0DSXh965I'; // A test JWT used for testing various scenarios +const JWT_TEST_NO_EXP = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOiJhZG1pbiJ9.YDDoAKtA6wXd09zZ0aIUEt_IFvOwUd3rk4fW5aNppHM'; // A test JWT with no exp claim +const JWT_TEST_FAR_EXP = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiZXhwIjoxMzAwODE5MzgwMCwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOiJhZG1pbiJ9.ciW9WWtIaA-QJqy0flPSfMNQfGs9GEFqcNFY_LqrdII'; // A test JWT with a far off exp claim + +const JWT_TEST_NEAR_EXP = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiZXhwIjo1MCwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOiJhZG1pbiJ9.96_h7V_OrO-bHzhh1DUIOJ2_J2sEI8y--cjBOBonk2o'; // A test JWT with exp claim of 50 + +const router: Partial = { post: (body) => {} }; +const core = { + http: { + basePath: { + serverBasePath: '/', }, - } as CoreSetup; - let esClient: ILegacyClusterClient; - const sessionStorageFactory: SessionStorageFactory = { - asScoped: jest.fn().mockImplementation(() => { - return { - server: { - states: { - add: jest.fn(), - }, + }, +} as CoreSetup; +let esClient: ILegacyClusterClient; + +const sessionStorageFactory: SessionStorageFactory = { + asScoped: jest.fn().mockImplementation(() => { + return { + server: { + states: { + add: jest.fn(), }, - }; - }), - }; - let logger: Logger; - - const cookieConfig: Partial = { - cookie: { - secure: false, - name: 'test_cookie_name', - password: 'secret', - ttl: 60 * 60 * 1000, - domain: null, - isSameSite: false, - }, - }; + }, + }; + }), +}; +let logger: Logger; + +const cookieConfig: Partial = { + cookie: { + secure: false, + name: 'test_cookie_name', + password: 'secret', + ttl: 60 * 60 * 1000, + domain: null, + isSameSite: false, + }, +}; +describe('test jwt auth library', () => { function getTestJWTAuthenticationHandlerWithConfig(config: SecurityPluginConfigType) { return getAuthenticationHandler( 'jwt', @@ -200,4 +213,180 @@ describe('test jwt auth library', () => { expect(headers).toEqual(expectedHeaders); }); +}); // re-import JWTAuth to change cookie splitter to a no-op + +/* eslint-disable no-shadow, @typescript-eslint/no-var-requires */ +describe('JWT Expiry Tests', () => { + const setExtraAuthStorageMock = jest.fn(); + jest.resetModules(); + jest.doMock('../../../session/cookie_splitter', () => ({ + setExtraAuthStorage: setExtraAuthStorageMock, + })); + const { JwtAuthentication } = require('./jwt_auth'); + + const realDateNow = Date.now.bind(global.Date); + const dateNowStub = jest.fn(() => 0); + global.Date.now = dateNowStub; + const coreSetup = jest.fn(); + + afterAll(() => { + global.Date.now = realDateNow; + }); + + test('getExpirationDate', () => { + expect(getExpirationDate(undefined, 1000)).toBe(1000); // undefined + expect(getExpirationDate('', 1000)).toBe(1000); // empty string + expect(getExpirationDate('Bearer ', 1000)).toBe(1000); // empty token + expect(getExpirationDate('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 1000)).toBe(1000); // malformed token with one part + expect(getExpirationDate(`Bearer ${JWT_TEST_FAR_EXP}`, 1000)).toBe(1000); // JWT with very far expiry defaults to lower value (ttl) + expect(getExpirationDate(`Bearer ${JWT_TEST}`, 920869200001)).toBe(920869200000); // JWT expiry is lower than the default + expect(getExpirationDate(`Bearer ${JWT_TEST_NO_EXP}`, 1000)).toBe(1000); // JWT doesn't include a exp claim + }); + + test('JWT auth type sets expiryTime of cookie JWT exp less than ttl', async () => { + const infiniteTTLConfig = { + session: { + keepalive: true, + ttl: Infinity, + }, + jwt: { + url_param: 'awesome', + header: 'AUTHORIZATION', + extra_storage: { + cookie_prefix: 'testcookie', + additional_cookies: 2, + }, + }, + } as SecurityPluginConfigType; + + const jwtAuth = new JwtAuthentication( + infiniteTTLConfig, + sessionStorageFactory, + router, + esClient, + coreSetup, + logger + ); + + const requestWithHeaders = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + headers: { + authorization: `Bearer ${JWT_TEST}`, + }, + }); + const cookieFromHeaders = jwtAuth.getCookie(requestWithHeaders, {}); + expect(cookieFromHeaders.expiryTime!).toBe(920869200000); + + const requestWithJWTInUrl = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + query: { + awesome: JWT_TEST, + }, + }); + const cookieFromURL = jwtAuth.getCookie(requestWithJWTInUrl, {}); + expect(cookieFromURL.expiryTime!).toBe(920869200000); + }); + + test('JWT auth type sets expiryTime of cookie ttl less than JWT exp', async () => { + const lowTTLConfig = { + session: { + keepalive: true, + ttl: 1000, + }, + jwt: { + url_param: 'awesome', + header: 'AUTHORIZATION', + extra_storage: { + cookie_prefix: 'testcookie', + additional_cookies: 2, + }, + }, + } as SecurityPluginConfigType; + + const jwtAuth = new JwtAuthentication( + lowTTLConfig, + sessionStorageFactory, + router, + esClient, + coreSetup, + logger + ); + + const requestWithHeaders = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + headers: { + authorization: `Bearer ${JWT_TEST}`, + }, + }); + const cookieFromHeaders = jwtAuth.getCookie(requestWithHeaders, {}); + expect(cookieFromHeaders.expiryTime!).toBe(1000); + + const requestWithJWTInUrl = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + query: { + awesome: JWT_TEST, + }, + }); + const cookieFromURL = jwtAuth.getCookie(requestWithJWTInUrl, {}); + expect(cookieFromURL.expiryTime!).toBe(1000); + }); + + test('getKeepAliveExpiry', () => { + const jwtConfig = { + session: { + keepalive: true, + ttl: 100000, + }, + jwt: { + url_param: 'awesome', + header: 'AUTHORIZATION', + extra_storage: { + cookie_prefix: 'testcookie', + additional_cookies: 2, + }, + }, + } as SecurityPluginConfigType; + + const jwtAuth = new JwtAuthentication( + jwtConfig, + sessionStorageFactory, + router, + esClient, + coreSetup, + logger + ); + + const requestWithHeaders = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + headers: { + authorization: `Bearer ${JWT_TEST}`, + }, + }); + + const cookie: SecuritySessionCookie = { + credentials: {}, + expiryTime: 1000, + }; + + // Mock the method with a JWT with far exp + jest.spyOn(jwtAuth, 'buildAuthHeaderFromCookie').mockReturnValue({ + authorization: `Bearer ${JWT_TEST_FAR_EXP}`, + }); + + // getKeepAliveExpiry takes on the value of the ttl, since it is less than the exp claim * 1000 + expect(jwtAuth.getKeepAliveExpiry(cookie, requestWithHeaders)).toBe(100000); + + // Mock the method with a JWT with near exp + jest.spyOn(jwtAuth, 'buildAuthHeaderFromCookie').mockReturnValue({ + authorization: `Bearer ${JWT_TEST_NEAR_EXP}`, + }); + + // getKeepAliveExpiry takes on the value of the exp claim * 1000, since it is less than the ttl + expect(jwtAuth.getKeepAliveExpiry(cookie, requestWithHeaders)).toBe(50000); + + // Restore the original method implementation after the test + jwtAuth.buildAuthHeaderFromCookie.mockRestore(); + }); + + /* eslint-enable no-shadow, @typescript-eslint/no-var-requires */ }); diff --git a/server/auth/types/jwt/jwt_helper.ts b/server/auth/types/jwt/jwt_helper.ts new file mode 100644 index 000000000..e5d35487e --- /dev/null +++ b/server/auth/types/jwt/jwt_helper.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +export function getExpirationDate(authHeader: string | undefined, defaultExpiry: number) { + if (!authHeader) { + return defaultExpiry; + } else if (authHeader.startsWith('Bearer ')) { + // Extract the token part by splitting the string and taking the second part + const token = authHeader.split(' ')[1]; + const parts = token.split('.'); + if (parts.length !== 3) { + return defaultExpiry; + } + const claim = JSON.parse(Buffer.from(parts[1], 'base64').toString()); + if (claim.exp) { + return Math.min(claim.exp * 1000, defaultExpiry); + } + } + return defaultExpiry; +} diff --git a/server/auth/types/multiple/multi_auth.test.ts b/server/auth/types/multiple/multi_auth.test.ts new file mode 100644 index 000000000..fb61d4ed8 --- /dev/null +++ b/server/auth/types/multiple/multi_auth.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { SecurityPluginConfigType } from '../../../index'; +import { SecuritySessionCookie } from '../../../session/security_cookie'; +import { + IRouter, + CoreSetup, + ILegacyClusterClient, + Logger, + SessionStorageFactory, +} from '../../../../../../src/core/server'; +import { MultipleAuthentication } from './multi_auth'; + +describe('Multi auth tests', () => { + let router: IRouter; + let core: CoreSetup; + let esClient: ILegacyClusterClient; + let sessionStorageFactory: SessionStorageFactory; + let logger: Logger; + + const config = ({ + session: { + ttl: 1000, + }, + auth: { + type: 'basic', + }, + } as unknown) as SecurityPluginConfigType; + + test('getKeepAliveExpiry', () => { + const realDateNow = Date.now.bind(global.Date); + const dateNowStub = jest.fn(() => 0); + global.Date.now = dateNowStub; + const multiAuthentication = new MultipleAuthentication( + config, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + expiryTime: 0, + }; + + const request = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + }); + + expect(multiAuthentication.getKeepAliveExpiry(cookie, request)).toBe(1000); // Multi auth using basic auth's implementation + global.Date.now = realDateNow; + }); +}); diff --git a/server/auth/types/multiple/multi_auth.ts b/server/auth/types/multiple/multi_auth.ts index 8763eaa4f..2bcc5e1ba 100644 --- a/server/auth/types/multiple/multi_auth.ts +++ b/server/auth/types/multiple/multi_auth.ts @@ -17,11 +17,11 @@ import { SessionStorageFactory, IRouter, ILegacyClusterClient, - OpenSearchDashboardsRequest, Logger, LifecycleResponseFactory, + OpenSearchDashboardsRequest, AuthToolkit, -} from '../../../../opensearch-dashboards/server'; +} from 'opensearch-dashboards/server'; import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router'; import { SecurityPluginConfigType } from '../../..'; import { AuthenticationType, IAuthenticationType } from '../authentication_type'; @@ -130,6 +130,19 @@ export class MultipleAuthentication extends AuthenticationType { return {}; } + getKeepAliveExpiry( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): number { + const reqAuthType = cookie?.authType?.toLowerCase(); + if (reqAuthType && this.authHandlers.has(reqAuthType)) { + return this.authHandlers.get(reqAuthType)!.getKeepAliveExpiry(cookie, request); + } else { + // default to TTL setting + return Date.now() + this.config.session.ttl; + } + } + async isValidCookie( cookie: SecuritySessionCookie, request: OpenSearchDashboardsRequest diff --git a/server/auth/types/openid/openid_auth.test.ts b/server/auth/types/openid/openid_auth.test.ts index 55f730481..c8cc839e7 100644 --- a/server/auth/types/openid/openid_auth.test.ts +++ b/server/auth/types/openid/openid_auth.test.ts @@ -207,4 +207,61 @@ 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 oidcConfig: unknown = { + openid: { + scope: [], + }, + }; + + const openIdAuthentication = new OpenIdAuthentication( + oidcConfig as SecurityPluginConfigType, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + const testCookie: SecuritySessionCookie = { + credentials: { + authHeaderValue: 'Bearer eyToken', + expiry_time: -1, + }, + expiryTime: 2000, + username: 'admin', + authType: 'openid', + }; + + expect(await openIdAuthentication.isValidCookie(testCookie, {})).toBe(true); + global.Date.now = realDateNow; + }); + + test('getKeepAliveExpiry', () => { + const oidcConfig: unknown = { + openid: { + scope: [], + }, + }; + + const openIdAuthentication = new OpenIdAuthentication( + oidcConfig as SecurityPluginConfigType, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + const testCookie: SecuritySessionCookie = { + credentials: { + authHeaderValue: 'Bearer eyToken', + }, + expiryTime: 1000, + }; + + expect(openIdAuthentication.getKeepAliveExpiry(testCookie, {})).toBe(1000); + }); }); diff --git a/server/auth/types/openid/openid_auth.ts b/server/auth/types/openid/openid_auth.ts index 4d59ea632..61088dfd3 100644 --- a/server/auth/types/openid/openid_auth.ts +++ b/server/auth/types/openid/openid_auth.ts @@ -251,6 +251,14 @@ export class OpenIdAuthentication extends AuthenticationType { }; } + // OIDC expiry time is set by the IDP and refreshed via refreshTokens + getKeepAliveExpiry( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): number { + return cookie.expiryTime!; + } + // TODO: Add token expiration check here async isValidCookie( cookie: SecuritySessionCookie, @@ -260,13 +268,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; } @@ -290,8 +297,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, diff --git a/server/auth/types/openid/routes.ts b/server/auth/types/openid/routes.ts index a9b84e75c..a8dc2e2b1 100644 --- a/server/auth/types/openid/routes.ts +++ b/server/auth/types/openid/routes.ts @@ -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, { diff --git a/server/auth/types/proxy/proxy_auth.test.ts b/server/auth/types/proxy/proxy_auth.test.ts new file mode 100644 index 000000000..c25eaf645 --- /dev/null +++ b/server/auth/types/proxy/proxy_auth.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { SecurityPluginConfigType } from '../../../index'; +import { SecuritySessionCookie } from '../../../session/security_cookie'; +import { + IRouter, + CoreSetup, + ILegacyClusterClient, + Logger, + SessionStorageFactory, +} from '../../../../../../src/core/server'; +import { ProxyAuthentication } from './proxy_auth'; + +describe('Proxy auth tests', () => { + let router: IRouter; + let core: CoreSetup; + let esClient: ILegacyClusterClient; + let sessionStorageFactory: SessionStorageFactory; + let logger: Logger; + + const config = ({ + session: { + ttl: 1000, + }, + } as unknown) as SecurityPluginConfigType; + + test('getKeepAliveExpiry', () => { + const realDateNow = Date.now.bind(global.Date); + const dateNowStub = jest.fn(() => 0); + global.Date.now = dateNowStub; + const proxyAuthentication = new ProxyAuthentication( + config, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + expiryTime: 0, + }; + + const request = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + }); + + expect(proxyAuthentication.getKeepAliveExpiry(cookie, request)).toBe(1000); + global.Date.now = realDateNow; + }); +}); diff --git a/server/auth/types/proxy/proxy_auth.ts b/server/auth/types/proxy/proxy_auth.ts index 346553a50..b3a97d5ed 100644 --- a/server/auth/types/proxy/proxy_auth.ts +++ b/server/auth/types/proxy/proxy_auth.ts @@ -135,7 +135,10 @@ export class ProxyAuthentication extends AuthenticationType { } } - buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any { + buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any { const authHeaders: any = {}; if (get(cookie.credentials, this.userHeaderName)) { authHeaders[this.userHeaderName] = cookie.credentials[this.userHeaderName]; diff --git a/server/auth/types/saml/saml_auth.test.ts b/server/auth/types/saml/saml_auth.test.ts index 355d8b28c..7b1b47112 100644 --- a/server/auth/types/saml/saml_auth.test.ts +++ b/server/auth/types/saml/saml_auth.test.ts @@ -115,4 +115,28 @@ describe('test SAML authHeaderValue', () => { expect(headers).toEqual(expectedHeaders); }); + + test('getKeepAliveExpiry', () => { + const samlAuthentication = new SamlAuthentication( + config, + sessionStorageFactory, + router, + esClient, + core, + logger + ); + + const cookie: SecuritySessionCookie = { + credentials: { + authHeaderValueExtra: true, + }, + expiryTime: 1000, + }; + + const request = httpServerMock.createOpenSearchDashboardsRequest({ + path: '/internal/v1', + }); + + expect(samlAuthentication.getKeepAliveExpiry(cookie, request)).toBe(1000); + }); }); diff --git a/server/auth/types/saml/saml_auth.ts b/server/auth/types/saml/saml_auth.ts index 23c1ba9c2..1a58efb1a 100644 --- a/server/auth/types/saml/saml_auth.ts +++ b/server/auth/types/saml/saml_auth.ts @@ -130,6 +130,14 @@ export class SamlAuthentication extends AuthenticationType { return {}; } + // SAML expiry time is set by the IDP and returned via the security backend. Keep alive should not modify this value. + public getKeepAliveExpiry( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): number { + return cookie.expiryTime!; + } + getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie { const authorizationHeaderValue: string = request.headers[ SamlAuthentication.AUTH_HEADER_NAME