From 54d2f23ee7fa45b92e980c120e50f721c7823d49 Mon Sep 17 00:00:00 2001 From: Kurt Greiner Date: Thu, 14 Jul 2022 12:42:49 -0400 Subject: [PATCH] Adding deprecation warning for Interactive Users using ApiKeys --- docs/user/security/api-keys/index.asciidoc | 5 ++ .../security/authentication/index.asciidoc | 6 ++ .../analytics/authentication_type.test.ts | 87 +++++++++++++++++++ .../routes/analytics/authentication_type.ts | 30 +++++++ 4 files changed, 128 insertions(+) diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc index bc277609d43e4..458a6a73bb04a 100644 --- a/docs/user/security/api-keys/index.asciidoc +++ b/docs/user/security/api-keys/index.asciidoc @@ -45,6 +45,11 @@ curl --location --request GET 'http://localhost:5601/api/security/role' \ --header 'kbn-xsrf: true' \ --header 'Authorization: ApiKey aVZlLUMzSUJuYndxdDJvN0k1bU46aGxlYUpNS2lTa2FKeVZua1FnY1VEdw==' \ +[IMPORTANT] +============================================================================ +Interactive users authenticating via ApiKey is deprecated and will be removed in a future version. +============================================================================ + [float] [[view-api-keys]] === View and delete API keys diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 007d1af017df3..7c6f7d0d0e8df 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -394,6 +394,12 @@ HTTP protocol provides a simple authentication framework that can be used by a c This type of authentication is usually useful for machine-to-machine interaction that requires authentication and where human intervention is not desired or just infeasible. There are a number of use cases when HTTP authentication support comes in handy for {kib} users as well. +[IMPORTANT] +============================================================================ +Interactive users authenticating via ApiKey is deprecated and will be removed in a future version. +============================================================================ + + By default {kib} supports <> authentication scheme _and_ any scheme supported by the currently enabled authentication provider. For example, `Basic` authentication scheme is automatically supported when basic authentication provider is enabled, or `Bearer` scheme when any of the token based authentication providers is enabled (Token, SAML, OpenID Connect, PKI or Kerberos). But it's also possible to add support for any other authentication scheme in the `kibana.yml` configuration file, as follows: NOTE: Don't forget to explicitly specify the default `apikey` and `bearer` schemes when you just want to add a new one to the list. diff --git a/x-pack/plugins/security/server/routes/analytics/authentication_type.test.ts b/x-pack/plugins/security/server/routes/analytics/authentication_type.test.ts index 3ea35308347a1..34334833fe849 100644 --- a/x-pack/plugins/security/server/routes/analytics/authentication_type.test.ts +++ b/x-pack/plugins/security/server/routes/analytics/authentication_type.test.ts @@ -19,6 +19,7 @@ import { routeDefinitionParamsMock } from '../index.mock'; import { defineRecordAnalyticsOnAuthTypeRoutes } from './authentication_type'; const FAKE_TIMESTAMP = 1637665318135; + function getMockContext( licenseCheckResult: { state: string; message?: string } = { state: 'valid' } ) { @@ -33,14 +34,18 @@ describe('POST /internal/security/analytics/_record_auth_type', () => { }); let routeHandler: RequestHandler; + let routeParamsMock: DeeplyMockedKeys; + beforeEach(() => { routeParamsMock = routeDefinitionParamsMock.create(); + defineRecordAnalyticsOnAuthTypeRoutes(routeParamsMock); const [, recordAnalyticsOnAuthTypeRouteHandler] = routeParamsMock.router.post.mock.calls.find( ([{ path }]) => path === '/internal/security/analytics/_record_auth_type' )!; + routeHandler = recordAnalyticsOnAuthTypeRouteHandler; }); @@ -49,6 +54,10 @@ describe('POST /internal/security/analytics/_record_auth_type', () => { const response = await routeHandler(getMockContext(), request, kibanaResponseFactory); expect(response.status).toBe(204); + expect(routeParamsMock.logger.warn).toBeCalledWith( + 'Cannot record authentication type: current user could not be retrieved.' + ); + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).not.toHaveBeenCalled(); }); @@ -286,19 +295,23 @@ describe('POST /internal/security/analytics/_record_auth_type', () => { }); const mockAuthc = authenticationServiceMock.createStart(); + mockAuthc.getCurrentUser.mockReturnValue( mockAuthenticatedUser({ authentication_provider: { type: HTTPAuthenticationProvider.type, name: '__http__' }, }) ); + routeParamsMock.getAuthenticationService.mockReturnValue(mockAuthc); const response = await routeHandler(getMockContext(), request, kibanaResponseFactory); + expect(response.status).toBe(200); expect(response.payload).toEqual({ timestamp: FAKE_TIMESTAMP, signature: 'f4f6b485690816127c33d5aa13cd6cd12c9892641ba23b5d58e5c6590cd43db0', }); + routeParamsMock.analyticsService.reportAuthenticationTypeEvent.mockClear(); initialTimestamp = response.payload.timestamp; @@ -312,16 +325,20 @@ describe('POST /internal/security/analytics/_record_auth_type', () => { }); const response = await routeHandler(getMockContext(), request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(response.payload).toEqual({ timestamp: initialTimestamp, signature: '46d5841ad21d29ca6c7c1c639adc6294c176c394adb0b40dfc05797cfe29218e', }); + expect(response.payload.signature).not.toEqual(initialSignature); expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledTimes( 1 ); + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledWith({ authenticationProviderType: 'http', authenticationRealmType: 'native', @@ -329,4 +346,74 @@ describe('POST /internal/security/analytics/_record_auth_type', () => { }); }); }); + + describe('logApiKeyWithInteractiveUserDeprecated', () => { + it('should log a deprecation warning if interactive user is using API Key', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'ApiKey' }, + }); + + const mockAuthc = authenticationServiceMock.createStart(); + + mockAuthc.getCurrentUser.mockReturnValue( + mockAuthenticatedUser({ + authentication_provider: { type: 'http', name: '__http__' }, + }) + ); + + routeParamsMock.getAuthenticationService.mockReturnValue(mockAuthc); + + const response = await routeHandler(getMockContext(), request, kibanaResponseFactory); + + expect(response.status).toBe(200); + + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledTimes( + 1 + ); + + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledWith({ + authenticationProviderType: 'http', + authenticationRealmType: 'native', + httpAuthenticationScheme: 'ApiKey', + }); + + expect(routeParamsMock.logger.warn).toHaveBeenCalledTimes(1); + expect(routeParamsMock.logger.warn).toBeCalledWith( + 'Using API Key authentication as an interactive user is deprecated and will stop working in the next major version.', + { tags: ['deprecation'] } + ); + }); + + it('should not log a deprecation warning if interactive user is using API Key', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Basic' }, + }); + + const mockAuthc = authenticationServiceMock.createStart(); + + mockAuthc.getCurrentUser.mockReturnValue( + mockAuthenticatedUser({ + authentication_provider: { type: 'http', name: '__http__' }, + }) + ); + + routeParamsMock.getAuthenticationService.mockReturnValue(mockAuthc); + + const response = await routeHandler(getMockContext(), request, kibanaResponseFactory); + + expect(response.status).toBe(200); + + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledTimes( + 1 + ); + + expect(routeParamsMock.analyticsService.reportAuthenticationTypeEvent).toHaveBeenCalledWith({ + authenticationProviderType: 'http', + authenticationRealmType: 'native', + httpAuthenticationScheme: 'Basic', + }); + + expect(routeParamsMock.logger.warn).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts index c3246667c1aa7..6ff12664a62bd 100644 --- a/x-pack/plugins/security/server/routes/analytics/authentication_type.ts +++ b/x-pack/plugins/security/server/routes/analytics/authentication_type.ts @@ -8,6 +8,7 @@ import { createHash } from 'crypto'; import { schema } from '@kbn/config-schema'; +import type { Logger } from '@kbn/logging'; import type { RouteDefinitionParams } from '..'; import type { AuthenticationTypeAnalyticsEvent } from '../../analytics'; @@ -39,12 +40,15 @@ export function defineRecordAnalyticsOnAuthTypeRoutes({ createLicensedRouteHandler(async (context, request, response) => { try { const authUser = getAuthenticationService().getCurrentUser(request); + if (!authUser) { logger.warn('Cannot record authentication type: current user could not be retrieved.'); + return response.noContent(); } let timestamp = new Date().getTime(); + const { signature: previouslyRegisteredSignature, timestamp: previousRegistrationTimestamp, @@ -69,11 +73,17 @@ export function defineRecordAnalyticsOnAuthTypeRoutes({ .digest('hex'); const elapsedTimeInHrs = (timestamp - previousRegistrationTimestamp) / (1000 * 60 * 60); + if ( elapsedTimeInHrs >= MINIMUM_ELAPSED_TIME_HOURS || previouslyRegisteredSignature !== signature ) { analyticsService.reportAuthenticationTypeEvent(authTypeEventToReport); + + logApiKeyWithInteractiveUserDeprecated( + authTypeEventToReport.httpAuthenticationScheme, + logger + ); } else { timestamp = previousRegistrationTimestamp; } @@ -88,3 +98,23 @@ export function defineRecordAnalyticsOnAuthTypeRoutes({ }) ); } + +/** + * API Key authentication by interactive users is deprecated, this method logs a deprecation warning + * + * @param httpAuthenticationScheme A string representing the authentication type event's scheme (ApiKey, etc.) by an interactive user + * @param logger A reference to the Logger to log the deprecation message + */ +function logApiKeyWithInteractiveUserDeprecated( + httpAuthenticationScheme: string = '', + logger: Logger +): void { + const isUsingApiKey = httpAuthenticationScheme?.toLowerCase() === 'apikey'; + + if (isUsingApiKey) { + logger.warn( + `Using API Key authentication as an interactive user is deprecated and will not be supported in a future version`, + { tags: ['deprecation'] } + ); + } +}