diff --git a/server/auth/types/jwt/jwt_auth.ts b/server/auth/types/jwt/jwt_auth.ts index 3b8ef365a..b584aaec5 100644 --- a/server/auth/types/jwt/jwt_auth.ts +++ b/server/auth/types/jwt/jwt_auth.ts @@ -25,11 +25,18 @@ import { AuthToolkit, IOpenSearchDashboardsResponse, } from 'opensearch-dashboards/server'; +import { Server, ServerStateCookieOptions } from '@hapi/hapi'; import { SecurityPluginConfigType } from '../../..'; import { SecuritySessionCookie } from '../../../session/security_cookie'; import { AuthenticationType } from '../authentication_type'; import { JwtAuthRoutes } from './routes'; +import { + setExtraAuthStorage, + getExtraAuthStorageValue, + ExtraAuthStorageOptions, +} from '../../../session/cookie_splitter'; + export class JwtAuthentication extends AuthenticationType { public readonly type: string = 'jwt'; @@ -48,10 +55,42 @@ export class JwtAuthentication extends AuthenticationType { } public async init() { + this.createExtraStorage(); const routes = new JwtAuthRoutes(this.router, this.sessionStorageFactory); routes.setupRoutes(); } + createExtraStorage() { + // @ts-ignore + const hapiServer: Server = this.sessionStorageFactory.asScoped({}).server; + + const extraCookiePrefix = this.config.jwt.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.saml.extra_storage.additional_cookies; i++) { + hapiServer.states.add(extraCookiePrefix + i, extraCookieSettings); + } + } + + private getExtraAuthStorageOptions(logger?: Logger): ExtraAuthStorageOptions { + // If we're here, we will always have the openid configuration + return { + cookiePrefix: this.config.jwt.extra_storage.cookie_prefix, + additionalCookies: this.config.jwt.extra_storage.additional_cookies, + logger, + }; + } + private getTokenFromUrlParam(request: OpenSearchDashboardsRequest): string | undefined { const urlParamName = this.config.jwt?.url_param; if (urlParamName) { @@ -100,25 +139,50 @@ export class JwtAuthentication extends AuthenticationType { request: OpenSearchDashboardsRequest, authInfo: any ): SecuritySessionCookie { + const authorizationHeaderValue = this.getBearerToken(request) || ''; + + setExtraAuthStorage( + request, + authorizationHeaderValue, + this.getExtraAuthStorageOptions(this.logger) + ); return { username: authInfo.user_name, credentials: { - authHeaderValue: this.getBearerToken(request), + authHeaderValueExtra: true, }, authType: this.type, expiryTime: Date.now() + this.config.session.ttl, }; } - async isValidCookie(cookie: SecuritySessionCookie): Promise { + async isValidCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): Promise { return ( cookie.authType === this.type && cookie.username && cookie.expiryTime && - cookie.credentials?.authHeaderValue + (cookie.credentials?.authHeaderValue || this.getExtraAuthStorageValue(request, cookie)) ); } + getExtraAuthStorageValue(request: OpenSearchDashboardsRequest, cookie: SecuritySessionCookie) { + let extraValue = ''; + if (!cookie.credentials?.authHeaderValueExtra) { + return extraValue; + } + + try { + extraValue = getExtraAuthStorageValue(request, this.getExtraAuthStorageOptions(this.logger)); + } catch (error) { + this.logger.info(error); + } + + return extraValue; + } + handleUnauthedRequest( request: OpenSearchDashboardsRequest, response: LifecycleResponseFactory, @@ -127,12 +191,25 @@ export class JwtAuthentication extends AuthenticationType { return response.unauthorized(); } - buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any { - const header: any = {}; - const authHeaderValue = cookie.credentials?.authHeaderValue; - if (authHeaderValue) { - header[this.authHeaderName] = authHeaderValue; + buildAuthHeaderFromCookie( + cookie: SecuritySessionCookie, + request: OpenSearchDashboardsRequest + ): any { + const headers: any = {}; + + if (cookie.credentials?.authHeaderValueExtra) { + try { + const extraAuthStorageValue = this.getExtraAuthStorageValue(request, cookie); + headers[this.authHeaderName] = extraAuthStorageValue; + } catch (error) { + this.logger.error(error); + // @todo Re-throw? + // throw error; + } + } else { + headers[this.authHeaderName] = cookie.credentials?.authHeaderValue; } - return header; + + return headers; } } diff --git a/server/index.ts b/server/index.ts index 915da0315..a69176e71 100644 --- a/server/index.ts +++ b/server/index.ts @@ -227,14 +227,16 @@ export const configSchema = schema.object({ login_endpoint: schema.maybe(schema.string({ defaultValue: '' })), }) ), - jwt: schema.maybe( - schema.object({ - enabled: schema.boolean({ defaultValue: false }), - login_endpoint: schema.maybe(schema.string()), - url_param: schema.string({ defaultValue: 'authorization' }), - header: schema.string({ defaultValue: 'Authorization' }), - }) - ), + jwt: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + login_endpoint: schema.maybe(schema.string()), + url_param: schema.string({ defaultValue: 'authorization' }), + header: schema.string({ defaultValue: 'Authorization' }), + extra_storage: schema.object({ + cookie_prefix: schema.string({ defaultValue: 'security_authentication_jwt', minLength: 2 }), + additional_cookies: schema.number({ min: 0, defaultValue: 3 }), + }), + }), ui: schema.object({ basicauth: schema.object({ // the login config here is the same as old config `_security.basicauth.login`