diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts index 2d38dbdccb414d5..65616e58e65b2ab 100644 --- a/x-pack/plugins/security/common/types.ts +++ b/x-pack/plugins/security/common/types.ts @@ -15,6 +15,7 @@ export interface SessionInfo { export enum LogoutReason { 'SESSION_EXPIRED' = 'SESSION_EXPIRED', + 'CONCURRENCY_LIMIT' = 'CONCURRENCY_LIMIT', 'AUTHENTICATION_ERROR' = 'AUTHENTICATION_ERROR', 'LOGGED_OUT' = 'LOGGED_OUT', 'UNAUTHENTICATED' = 'UNAUTHENTICATED', diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index cef47c3fbfa6a61..30c4aa678f8ccae 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -67,6 +67,12 @@ const loginFormMessages: Record { fetchMock.restore(); }); -for (const reason of ['AUTHENTICATION_ERROR', 'SESSION_EXPIRED']) { +for (const reason of [ + LogoutReason.AUTHENTICATION_ERROR, + LogoutReason.SESSION_EXPIRED, + LogoutReason.CONCURRENCY_LIMIT, +]) { const headers = - reason === 'SESSION_EXPIRED' ? { [SESSION_ERROR_REASON_HEADER]: reason } : undefined; + reason === LogoutReason.SESSION_EXPIRED || reason === LogoutReason.CONCURRENCY_LIMIT + ? { [SESSION_ERROR_REASON_HEADER]: reason } + : undefined; it(`logs out 401 responses (reason: ${reason})`, async () => { const http = setupHttp('/foo'); diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts index cac7ec90e7dab33..e09a9e5f26c312a 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.ts @@ -43,8 +43,8 @@ export class UnauthorizedResponseHttpInterceptor implements HttpInterceptor { if (response.status === 401) { const reason = response.headers.get(SESSION_ERROR_REASON_HEADER); this.sessionExpired.logout( - reason === LogoutReason.SESSION_EXPIRED - ? LogoutReason.SESSION_EXPIRED + reason === LogoutReason.SESSION_EXPIRED || reason === LogoutReason.CONCURRENCY_LIMIT + ? reason : LogoutReason.AUTHENTICATION_ERROR ); controller.halt(); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index f743244630412ec..7edf3ea05059067 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -35,6 +35,7 @@ import { ConfigSchema, createConfig } from '../config'; import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; import { securityMock } from '../mocks'; import { + SessionConcurrencyLimitError, type SessionError, SessionExpiredError, SessionMissingError, @@ -1356,7 +1357,12 @@ describe('Authenticator', () => { expectAuditEvents({ action: 'user_login', outcome: 'failure' }); }); - for (const FailureClass of [SessionMissingError, SessionExpiredError, SessionUnexpectedError]) { + for (const FailureClass of [ + SessionMissingError, + SessionExpiredError, + SessionConcurrencyLimitError, + SessionUnexpectedError, + ]) { describe(`session.get results in ${FailureClass.name}`, () => { it('fails as expected for redirectable requests', async () => { const request = httpServerMock.createKibanaRequest(); @@ -1455,7 +1461,10 @@ describe('Authenticator', () => { const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); - if (failureReason instanceof SessionExpiredError) { + if ( + failureReason instanceof SessionExpiredError || + failureReason instanceof SessionConcurrencyLimitError + ) { expect(authenticationResult.redirectURL).toBe( redirectUrl + '&msg=' + failureReason.code ); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index d984783df4c02cc..8599eb287989ed8 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -27,6 +27,7 @@ import { getErrorStatusCode } from '../errors'; import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; import { type Session, + SessionConcurrencyLimitError, SessionExpiredError, SessionUnexpectedError, type SessionValue, @@ -386,7 +387,8 @@ export class Authenticator { )}` : '' }${ - existingSession.error instanceof SessionExpiredError + existingSession.error instanceof SessionExpiredError || + existingSession.error instanceof SessionConcurrencyLimitError ? `&${LOGOUT_REASON_QUERY_STRING_PARAMETER}=${encodeURIComponent( existingSession.error.code )}` @@ -420,7 +422,8 @@ export class Authenticator { if (requestIsRedirectable) { if ( - existingSession.error instanceof SessionExpiredError && + (existingSession.error instanceof SessionExpiredError || + existingSession.error instanceof SessionConcurrencyLimitError) && authenticationResult.redirectURL?.startsWith( `${this.options.basePath.get(request)}/login?` ) @@ -479,9 +482,9 @@ export class Authenticator { } } } - if ( existingSession.error instanceof SessionExpiredError || + existingSession.error instanceof SessionConcurrencyLimitError || existingSession.error instanceof SessionUnexpectedError ) { const options = requestIsRedirectable diff --git a/x-pack/plugins/security/server/session_management/index.ts b/x-pack/plugins/security/server/session_management/index.ts index 5624c9d6ba0c9a3..992b9c4b3797509 100644 --- a/x-pack/plugins/security/server/session_management/index.ts +++ b/x-pack/plugins/security/server/session_management/index.ts @@ -12,6 +12,7 @@ export { SessionMissingError, SessionExpiredError, SessionUnexpectedError, + SessionConcurrencyLimitError, } from './session_errors'; export type { SessionManagementServiceStart } from './session_management_service'; export { SessionManagementService } from './session_management_service'; diff --git a/x-pack/plugins/security/server/session_management/session.test.ts b/x-pack/plugins/security/server/session_management/session.test.ts index d5ad9ce3b31d468..ca6a669ecd5e411 100644 --- a/x-pack/plugins/security/server/session_management/session.test.ts +++ b/x-pack/plugins/security/server/session_management/session.test.ts @@ -19,7 +19,12 @@ import { ConfigSchema, createConfig } from '../config'; import { sessionCookieMock, sessionIndexMock, sessionMock } from './index.mock'; import { getPrintableSessionId, Session, type SessionValueContentToEncrypt } from './session'; import type { SessionCookie } from './session_cookie'; -import { SessionExpiredError, SessionMissingError, SessionUnexpectedError } from './session_errors'; +import { + SessionConcurrencyLimitError, + SessionExpiredError, + SessionMissingError, + SessionUnexpectedError, +} from './session_errors'; import type { SessionIndex } from './session_index'; describe('Session', () => { @@ -233,7 +238,7 @@ describe('Session', () => { mockSessionIndex.isWithinConcurrentSessionLimit.mockResolvedValue(false); await expect(session.get(httpServerMock.createKibanaRequest())).resolves.toEqual({ - error: expect.any(SessionUnexpectedError), + error: expect.any(SessionConcurrencyLimitError), value: null, }); expect(mockSessionCookie.clear).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 8969cc593908245..228f9b05e575174 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -18,7 +18,12 @@ import type { AuthenticationProvider } from '../../common'; import { userSessionConcurrentLimitLogoutEvent } from '../audit'; import type { ConfigType } from '../config'; import type { SessionCookie } from './session_cookie'; -import { SessionExpiredError, SessionMissingError, SessionUnexpectedError } from './session_errors'; +import { + SessionConcurrencyLimitError, + SessionExpiredError, + SessionMissingError, + SessionUnexpectedError, +} from './session_errors'; import type { SessionIndex, SessionIndexValue } from './session_index'; /** @@ -214,7 +219,7 @@ export class Session { 'Session is outside the concurrent session limit and will be invalidated.' ); await this.invalidate(request, { match: 'current' }); - return { error: new SessionUnexpectedError(), value: null }; + return { error: new SessionConcurrencyLimitError(), value: null }; } return { diff --git a/x-pack/plugins/security/server/session_management/session_errors/index.ts b/x-pack/plugins/security/server/session_management/session_errors/index.ts index be1cea2bfd5b72c..6f8e0bd605ddab3 100644 --- a/x-pack/plugins/security/server/session_management/session_errors/index.ts +++ b/x-pack/plugins/security/server/session_management/session_errors/index.ts @@ -8,4 +8,5 @@ export { SessionError } from './session_error'; export { SessionMissingError } from './session_missing_error'; export { SessionExpiredError } from './session_expired_error'; +export { SessionConcurrencyLimitError } from './session_concurrency_limit_error'; export { SessionUnexpectedError } from './session_unexpected_error'; diff --git a/x-pack/plugins/security/server/session_management/session_errors/session_concurrency_limit_error.ts b/x-pack/plugins/security/server/session_management/session_errors/session_concurrency_limit_error.ts new file mode 100644 index 000000000000000..0e13f99458c38df --- /dev/null +++ b/x-pack/plugins/security/server/session_management/session_errors/session_concurrency_limit_error.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SessionError, SessionErrorReason } from './session_error'; + +export class SessionConcurrencyLimitError extends SessionError { + constructor() { + super(SessionErrorReason.CONCURRENCY_LIMIT, SessionErrorReason.CONCURRENCY_LIMIT); + } +} diff --git a/x-pack/plugins/security/server/session_management/session_errors/session_error.ts b/x-pack/plugins/security/server/session_management/session_errors/session_error.ts index 9974cc000514eec..d720f9c3dcdd423 100644 --- a/x-pack/plugins/security/server/session_management/session_errors/session_error.ts +++ b/x-pack/plugins/security/server/session_management/session_errors/session_error.ts @@ -8,6 +8,7 @@ export enum SessionErrorReason { 'SESSION_MISSING' = 'SESSION_MISSING', 'SESSION_EXPIRED' = 'SESSION_EXPIRED', + 'CONCURRENCY_LIMIT' = 'CONCURRENCY_LIMIT', 'UNEXPECTED_SESSION_ERROR' = 'UNEXPECTED_SESSION_ERROR', }