From caed9b3b0bb0ffc1c764217d7da6e3081e94b29c Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 27 Feb 2020 10:30:01 +0100 Subject: [PATCH] Review#1: improve tests. --- .../authentication/providers/basic.test.ts | 153 ++-- .../authentication/providers/http.test.ts | 85 ++- .../server/authentication/providers/http.ts | 6 +- .../authentication/providers/kerberos.test.ts | 285 ++++--- .../authentication/providers/oidc.test.ts | 357 +++++---- .../authentication/providers/pki.test.ts | 230 +++--- .../authentication/providers/saml.test.ts | 710 +++++++++--------- .../authentication/providers/token.test.ts | 242 +++--- 8 files changed, 993 insertions(+), 1075 deletions(-) diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index dfa9639fa3583..c8aaadfe6d390 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -8,12 +8,27 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { mockAuthenticationProviderOptions } from './base.mock'; +import { IClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { BasicAuthenticationProvider } from './basic'; function generateAuthorizationHeader(username: string, password: string) { return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('BasicAuthenticationProvider', () => { let provider: BasicAuthenticationProvider; let mockOptions: ReturnType; @@ -32,22 +47,16 @@ describe('BasicAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login( - httpServerMock.createKibanaRequest({ headers: {} }), - credentials + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} }), credentials) + ).resolves.toEqual( + AuthenticationResult.succeeded(user, { + authHeaders: { authorization }, + state: { authorization }, + }) ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toEqual({ authorization }); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if user cannot be retrieved during login attempt', async () => { @@ -60,20 +69,13 @@ describe('BasicAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -81,54 +83,57 @@ describe('BasicAuthenticationProvider', () => { it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null - ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) ); }); it('does not handle authentication if state exists, but authorization property is missing.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest(), - {} - ); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(httpServerMock.createKibanaRequest(), {}) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); it('does not handle authentication via `authorization` header.', async () => { const authorization = generateAuthorizationHeader('user', 'password'); const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe(authorization); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { const authorization = generateAuthorizationHeader('user', 'password'); const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const authenticationResult = await provider.authenticate(request, { authorization }); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe(authorization); - expect(authenticationResult.notHandled()).toBe(true); }); it('succeeds if only state is available.', async () => { @@ -140,19 +145,11 @@ describe('BasicAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, { authorization }); - - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if state contains invalid credentials.', async () => { @@ -164,40 +161,30 @@ describe('BasicAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, { authorization }); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.error).toBe(authenticationError); }); }); describe('`logout` method', () => { it('always redirects to the login page.', async () => { - const request = httpServerMock.createKibanaRequest(); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); + await expect(provider.logout(httpServerMock.createKibanaRequest())).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); }); it('passes query string parameters to the login page.', async () => { - const request = httpServerMock.createKibanaRequest({ - query: { next: '/app/ml', msg: 'SESSION_EXPIRED' }, - }); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe( - '/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' + await expect( + provider.logout( + httpServerMock.createKibanaRequest({ query: { next: '/app/ml', msg: 'SESSION_EXPIRED' } }) + ) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED') ); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts index bd70ad9537976..94237f9a3d538 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -8,9 +8,27 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { HTTPAuthenticationProvider } from './http'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('HTTPAuthenticationProvider', () => { let mockOptions: MockAuthenticationProviderOptions; beforeEach(() => { @@ -43,12 +61,11 @@ describe('HTTPAuthenticationProvider', () => { autoSchemesEnabled: true, schemes: ['apikey'], }); - const authenticationResult = await provider.login(); + + await expect(provider.login()).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.notHandled()).toBe(true); }); }); @@ -138,33 +155,35 @@ describe('HTTPAuthenticationProvider', () => { ]; it('does not handle authentication for requests without `authorization` header.', async () => { - const request = httpServerMock.createKibanaRequest(); - const provider = new HTTPAuthenticationProvider(mockOptions, { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'], }); - const authenticationResult = await provider.authenticate(request); + + await expect(provider.authenticate(httpServerMock.createKibanaRequest())).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication for requests with empty scheme in `authorization` header.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: { authorization: '' } }); - const provider = new HTTPAuthenticationProvider(mockOptions, { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'], }); - const authenticationResult = await provider.authenticate(request); + + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { authorization: '' } }) + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header if scheme is not supported.', async () => { @@ -182,10 +201,12 @@ describe('HTTPAuthenticationProvider', () => { autoSchemesEnabled, schemes, }); - const authenticationResult = await provider.authenticate(request); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(request.headers.authorization).toBe(header); - expect(authenticationResult.notHandled()).toBe(true); } expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); @@ -208,21 +229,13 @@ describe('HTTPAuthenticationProvider', () => { autoSchemesEnabled, schemes, }); - const authenticationResult = await provider.authenticate(request); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: header }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ ...user, authentication_provider: 'http' }) ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'http' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + expect(request.headers.authorization).toBe(header); } }); @@ -243,20 +256,13 @@ describe('HTTPAuthenticationProvider', () => { autoSchemesEnabled, schemes, }); - const authenticationResult = await provider.authenticate(request); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: header }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + expect(request.headers.authorization).toBe(header); } }); @@ -269,12 +275,11 @@ describe('HTTPAuthenticationProvider', () => { autoSchemesEnabled: true, schemes: ['apikey'], }); - const authenticationResult = await provider.logout(); + + await expect(provider.logout()).resolves.toEqual(DeauthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.notHandled()).toBe(true); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts index 6905c35a9977d..c06958ad7f350 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -39,14 +39,14 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { constructor( protected readonly options: Readonly, - proxyOptions?: Readonly> + httpOptions?: Readonly> ) { super(options); this.supportedSchemes = new Set( - (proxyOptions?.schemes ?? []).map(scheme => scheme.toLowerCase()) + (httpOptions?.schemes ?? []).map(scheme => scheme.toLowerCase()) ); - this.autoSchemesEnabled = proxyOptions?.autoSchemesEnabled ?? false; + this.autoSchemesEnabled = httpOptions?.autoSchemesEnabled ?? false; if (this.supportedSchemes.size === 0 && !this.autoSchemesEnabled) { throw new Error('Supported schemes should be specified'); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index 020a5159d521e..2f0431c98a295 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -11,9 +11,27 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { KerberosAuthenticationProvider } from './kerberos'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('KerberosAuthenticationProvider', () => { let provider: KerberosAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -28,12 +46,13 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header with non-negotiate scheme even if state contains a valid token.', async () => { @@ -45,12 +64,13 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'some-valid-refresh-token', }; - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle requests that can be authenticated without `Negotiate` header.', async () => { @@ -60,16 +80,13 @@ describe('KerberosAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({}); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle requests if backend does not support Kerberos.', async () => { @@ -81,78 +98,71 @@ describe('KerberosAuthenticationProvider', () => { ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - - expect(authenticationResult.notHandled()).toBe(true); }); it('fails if state is present, but backend does not support Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'token', refreshToken: 'refresh-token' }; + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) - ); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails with `Negotiate` challenge if backend supports Kerberos.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, + }) ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); }); it('fails if request authentication is failed with non-401 error.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const failureReason = new errors.ServiceUnavailable(); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable()); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('gets a token pair in exchange to SPNEGO one and stores it in the state.', async () => { @@ -169,28 +179,25 @@ describe('KerberosAuthenticationProvider', () => { refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer some-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('requests auth response header if token pair is complemented with Kerberos response token.', async () => { @@ -208,30 +215,26 @@ describe('KerberosAuthenticationProvider', () => { kerberos_authentication_response_token: 'response-token', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer some-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', - }); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails with `Negotiate response-token` if cannot complete context with a response token.', async () => { @@ -246,18 +249,17 @@ describe('KerberosAuthenticationProvider', () => { ); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + }) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', - }); }); it('fails with `Negotiate` if cannot create context using provided SPNEGO token.', async () => { @@ -272,18 +274,17 @@ describe('KerberosAuthenticationProvider', () => { ); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate', - }); }); it('fails if could not retrieve an access token in exchange to SPNEGO one.', async () => { @@ -294,16 +295,15 @@ describe('KerberosAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -320,23 +320,19 @@ describe('KerberosAuthenticationProvider', () => { refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer some-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { @@ -352,18 +348,16 @@ describe('KerberosAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { authHeaders: { authorization } } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toBeUndefined(); }); it('succeeds with valid session even if requiring a token refresh', async () => { @@ -394,15 +388,19 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'newbar', }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -418,46 +416,43 @@ describe('KerberosAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { authorization: `Bearer ${tokenPair.accessToken}` }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails with `Negotiate` challenge if both access and refresh tokens from the state are expired and backend supports Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'some-valid-refresh-token' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, + }) ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); }); }); @@ -465,11 +460,11 @@ describe('KerberosAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -481,13 +476,12 @@ describe('KerberosAuthenticationProvider', () => { const failureReason = new Error('failed to delete token'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to `/logged_out` page if tokens are invalidated successfully.', async () => { @@ -499,13 +493,12 @@ describe('KerberosAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index 5a0480b59bab1..c2fc4b8a10aff 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -10,9 +10,28 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { ElasticsearchErrorHelpers, KibanaRequest } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + KibanaRequest, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { OIDCAuthenticationProvider, OIDCAuthenticationFlow, ProviderLoginAttempt } from './oidc'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('OIDCAuthenticationProvider', () => { let provider: OIDCAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -51,30 +70,27 @@ describe('OIDCAuthenticationProvider', () => { '&login_hint=loginhint', }); - const authenticationResult = await provider.login(request, { - flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, - iss: 'theissuer', - loginHint: 'loginhint', - }); + await expect( + provider.login(request, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: 'theissuer', + loginHint: 'loginhint', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + + '&login_hint=loginhint', + { state: { state: 'statevalue', nonce: 'noncevalue', nextURL: '/base-path/' } } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { iss: 'theissuer', login_hint: 'loginhint' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + - '&login_hint=loginhint' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/', - }); }); function defineAuthenticationFlowTests( @@ -92,11 +108,17 @@ describe('OIDCAuthenticationProvider', () => { refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', - }); + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/some-path', { + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + }) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', @@ -109,58 +131,52 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/some-path'); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if authentication response is presented but session state does not contain the state parameter.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - nextURL: '/base-path/some-path', - }); - - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { nextURL: '/base-path/some-path' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if authentication response is presented but session state does not contain redirect URL.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - }); - - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { state: 'statevalue', nonce: 'noncevalue' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if session state is not presented.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, {}); + await expect(provider.login(request, attempt, {})).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) + ) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.failed()).toBe(true); }); it('fails if authentication response is not valid.', async () => { @@ -171,11 +187,13 @@ describe('OIDCAuthenticationProvider', () => { ); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', - }); + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', @@ -188,9 +206,6 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); } @@ -226,10 +241,9 @@ describe('OIDCAuthenticationProvider', () => { describe('`authenticate` method', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('redirects non-AJAX request that can not be authenticated to the OpenId Connect Provider.', async () => { @@ -246,25 +260,26 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', - }); }); it('fails if OpenID Connect authentication request preparation fails.', async () => { @@ -273,14 +288,13 @@ describe('OIDCAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { @@ -296,20 +310,16 @@ describe('OIDCAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { authHeaders: { authorization } } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toBeUndefined(); }); it('does not handle authentication via `authorization` header.', async () => { @@ -317,11 +327,12 @@ describe('OIDCAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { @@ -329,14 +340,15 @@ describe('OIDCAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }); + await expect( + provider.authenticate(request, { + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('fails if token from the state is rejected because of unknown reason.', async () => { @@ -352,18 +364,13 @@ describe('OIDCAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -394,21 +401,20 @@ describe('OIDCAuthenticationProvider', () => { refreshToken: 'new-refresh-token', }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }, + } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toEqual({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed too.', async () => { @@ -428,21 +434,16 @@ describe('OIDCAuthenticationProvider', () => { }; mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('redirects to OpenID Connect Provider for non-AJAX requests if refresh token is expired or already refreshed.', async () => { @@ -469,7 +470,22 @@ describe('OIDCAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); @@ -484,20 +500,6 @@ describe('OIDCAuthenticationProvider', () => { expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', - }); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { @@ -513,23 +515,18 @@ describe('OIDCAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { 'kbn-xsrf': 'xsrf', authorization }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); }); }); @@ -537,14 +534,17 @@ describe('OIDCAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, undefined as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, {})).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, { nonce: 'x' }); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, { nonce: 'x' })).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -557,18 +557,14 @@ describe('OIDCAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to /logged_out if `redirect` field in OpenID Connect logout response is null.', async () => { @@ -578,18 +574,14 @@ describe('OIDCAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the OpenID Connect Provider if RP initiated SLO is supported.', async () => { @@ -601,17 +593,14 @@ describe('OIDCAuthenticationProvider', () => { redirect: 'http://fake-idp/logout&id_token_hint=thehint', }); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/logout&id_token_hint=thehint') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/logout&id_token_hint=thehint'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 6cee9f970d9b2..3b5fa1bfa4d39 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -7,17 +7,23 @@ jest.mock('net'); jest.mock('tls'); +import { Socket } from 'net'; import { PeerCertificate, TLSSocket } from 'tls'; +import Boom from 'boom'; import { errors } from 'elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { PKIAuthenticationProvider } from './pki'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server'; -import { Socket } from 'net'; -import { getErrorStatusCode } from '../../errors'; interface MockPeerCertificate extends Partial { issuerCertificate: MockPeerCertificate; @@ -56,6 +62,18 @@ function getMockSocket({ return socket; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('PKIAuthenticationProvider', () => { let provider: PKIAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -72,12 +90,13 @@ describe('PKIAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { @@ -89,12 +108,13 @@ describe('PKIAuthenticationProvider', () => { peerCertificateFingerprint256: '2A:7A:C2:DD', }; - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle requests without certificate.', async () => { @@ -102,9 +122,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -114,9 +135,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -128,12 +150,10 @@ describe('PKIAuthenticationProvider', () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toMatchInlineSnapshot( - `[Error: Peer certificate is not available]` + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(new Error('Peer certificate is not available')) ); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -141,10 +161,9 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -158,10 +177,9 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -184,7 +202,15 @@ describe('PKIAuthenticationProvider', () => { mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -196,22 +222,11 @@ describe('PKIAuthenticationProvider', () => { }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => { @@ -229,29 +244,26 @@ describe('PKIAuthenticationProvider', () => { mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('invalidates existing token and gets a new one if fingerprints do not match.', async () => { @@ -269,7 +281,15 @@ describe('PKIAuthenticationProvider', () => { mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -287,14 +307,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets a new access token even if existing token is expired.', async () => { @@ -316,7 +328,15 @@ describe('PKIAuthenticationProvider', () => { mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -329,14 +349,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('fails with 401 if existing token is expired, but certificate is not present.', async () => { @@ -349,14 +361,13 @@ describe('PKIAuthenticationProvider', () => { ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => { @@ -370,7 +381,9 @@ describe('PKIAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -378,9 +391,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -398,30 +408,27 @@ describe('PKIAuthenticationProvider', () => { mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), @@ -432,37 +439,40 @@ describe('PKIAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { authHeaders: { authorization: `Bearer ${state.accessToken}` } } + ) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: `Bearer ${state.accessToken}`, - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), }), }); + const failureReason = new errors.ServiceUnavailable(); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable()); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); }); }); @@ -470,11 +480,11 @@ describe('PKIAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -486,13 +496,12 @@ describe('PKIAuthenticationProvider', () => { const failureReason = new Error('failed to delete token'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to `/logged_out` page if access token is invalidated successfully.', async () => { @@ -501,13 +510,12 @@ describe('PKIAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 23c2dc51abed3..cbdcfa0f0b025 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -11,8 +11,26 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { SAMLAuthenticationProvider, SAMLLoginStep } from './saml'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} describe('SAMLAuthenticationProvider', () => { let provider: SAMLAuthenticationProvider; @@ -65,41 +83,44 @@ describe('SAMLAuthenticationProvider', () => { refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/test-base-path/some-path#some-app', { + state: { + username: 'user', + accessToken: 'some-token', + refreshToken: 'some-refresh-token', + }, + }) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/test-base-path/some-path#some-app'); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if SAML Response payload is presented but state does not contain SAML Request token.', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - {} + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + {} + ) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('SAML response state does not have corresponding request id.') + ) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('SAML response state does not have corresponding request id.') - ); }); it('redirects to the default location if state contains empty redirect URL.', async () => { @@ -110,23 +131,25 @@ describe('SAMLAuthenticationProvider', () => { refresh_token: 'user-initiated-login-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'user-initiated-login-token', + refreshToken: 'user-initiated-login-refresh-token', + }, + }) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'user-initiated-login-token', - refreshToken: 'user-initiated-login-refresh-token', - }); }); it('redirects to the default location if state is not presented.', async () => { @@ -137,22 +160,24 @@ describe('SAMLAuthenticationProvider', () => { refresh_token: 'idp-initiated-login-refresh-token', }); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.SAMLResponseReceived, - samlResponse: 'saml-response-xml', - }); + await expect( + provider.login(request, { + step: SAMLLoginStep.SAMLResponseReceived, + samlResponse: 'saml-response-xml', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'idp-initiated-login-token', + refreshToken: 'idp-initiated-login-refresh-token', + }, + }) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'idp-initiated-login-token', - refreshToken: 'idp-initiated-login-refresh-token', - }); }); it('fails if SAML Response is rejected.', async () => { @@ -161,19 +186,18 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('SAML response is stale!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } - ); + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); describe('IdP initiated login with existing session', () => { @@ -189,24 +213,19 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('SAML response is invalid!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - } - ); + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' - ); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', @@ -214,9 +233,6 @@ describe('SAMLAuthenticationProvider', () => { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if fails to invalidate existing access/refresh tokens.', async () => { @@ -242,20 +258,15 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('Failed to invalidate token!'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state - ); + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' - ); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', @@ -269,9 +280,6 @@ describe('SAMLAuthenticationProvider', () => { accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to the home page if new SAML Response is for the same user.', async () => { @@ -296,20 +304,23 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' - ); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', @@ -323,9 +334,6 @@ describe('SAMLAuthenticationProvider', () => { accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); }); it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { @@ -350,20 +358,23 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.authenticate' - ); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', @@ -377,9 +388,6 @@ describe('SAMLAuthenticationProvider', () => { accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/overwritten_session'); }); }); @@ -387,34 +395,35 @@ describe('SAMLAuthenticationProvider', () => { it('fails if state is not available', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }); + await expect( + provider.login(request, { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('State does not include URL path to redirect to.') + ) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('State does not include URL path to redirect to.') - ); }); it('does not handle AJAX requests.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.notHandled()).toBe(true); }); it('redirects non-AJAX requests to the IdP remembering combined redirect URL.', async () => { @@ -425,13 +434,25 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#some-fragment', + }, + } + ) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { @@ -439,15 +460,6 @@ describe('SAMLAuthenticationProvider', () => { }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#some-fragment', - }); }); it('prepends redirect URL fragment with `#` if it does not have one.', async () => { @@ -458,13 +470,25 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '../some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '../some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#../some-fragment', + }, + } + ) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { @@ -475,15 +499,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Redirect URL fragment does not start with `#`.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#../some-fragment', - }); }); it('redirects non-AJAX requests to the IdP remembering only redirect URL path if fragment is too large.', async () => { @@ -494,13 +509,25 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment'.repeat(10), - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment'.repeat(10), + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path', + }, + } + ) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { @@ -511,15 +538,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL size should not exceed 100b but it was 165b. Only URL path is captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path', - }); }); it('fails if SAML request preparation fails.', async () => { @@ -528,21 +546,20 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); }); }); @@ -551,9 +568,9 @@ describe('SAMLAuthenticationProvider', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('does not handle authentication via `authorization` header.', async () => { @@ -561,11 +578,12 @@ describe('SAMLAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { @@ -573,15 +591,16 @@ describe('SAMLAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }); + await expect( + provider.authenticate(request, { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('redirects non-AJAX request that can not be authenticated to the "capture fragment" page.', async () => { @@ -592,15 +611,14 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' - ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); }); it('redirects non-AJAX request that can not be authenticated to the IdP if request path is too large.', async () => { @@ -613,7 +631,12 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, @@ -623,12 +646,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); }); it('fails if SAML request preparation fails.', async () => { @@ -639,14 +656,13 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { @@ -663,20 +679,16 @@ describe('SAMLAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { authHeaders: { authorization } } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { @@ -693,18 +705,13 @@ describe('SAMLAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason as any) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -739,22 +746,24 @@ describe('SAMLAuthenticationProvider', () => { refreshToken: 'new-refresh-token', }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { + username: 'user', + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }, + } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed with unknown reason too.', async () => { @@ -778,21 +787,16 @@ describe('SAMLAuthenticationProvider', () => { }; mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { @@ -812,23 +816,18 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { 'kbn-xsrf': 'xsrf', authorization }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); }); it('re-capture URL for non-AJAX requests if refresh token is expired.', async () => { @@ -848,25 +847,19 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' - ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); }); it('initiates SAML handshake for non-AJAX requests if refresh token is expired and request path is too large.', async () => { @@ -894,17 +887,17 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, @@ -914,12 +907,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); }); }); @@ -927,14 +914,13 @@ describe('SAMLAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, {} as any); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, { somethingElse: 'x' } as any); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); + await expect(provider.logout(request, {} as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); + await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -947,19 +933,14 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.failed(failureReason)); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if SAML invalidate call fails.', async () => { @@ -968,15 +949,14 @@ describe('SAMLAuthenticationProvider', () => { const failureReason = new Error('Realm is misconfigured!'); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to /logged_out if `redirect` field in SAML logout response is null.', async () => { @@ -986,19 +966,14 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => { @@ -1008,19 +983,14 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML logout if query string is not empty, but does not include SAMLRequest.', async () => { @@ -1032,19 +1002,14 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML invalidate call even if access token is presented.', async () => { @@ -1052,19 +1017,18 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', - }); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => { @@ -1072,15 +1036,14 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => { @@ -1088,15 +1051,14 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => { @@ -1108,15 +1070,13 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); }); it('redirects user to the IdP if SLO is supported by IdP in case of IdP initiated logout.', async () => { @@ -1126,15 +1086,17 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', - }); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); }); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 501bb1a6f5454..b1efe0a0475b4 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -11,9 +11,27 @@ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { TokenAuthenticationProvider } from './token'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('TokenAuthenticationProvider', () => { let provider: TokenAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -40,24 +58,19 @@ describe('TokenAuthenticationProvider', () => { refresh_token: tokenPair.refreshToken, }); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization }, state: tokenPair } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { body: { grant_type: 'password', ...credentials }, }); - - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual(tokenPair); - expect(authenticationResult.authHeaders).toEqual({ authorization }); }); it('fails if token cannot be generated during login attempt', async () => { @@ -67,7 +80,9 @@ describe('TokenAuthenticationProvider', () => { const authenticationError = new Error('Invalid credentials'); mockOptions.client.callAsInternalUser.mockRejectedValue(authenticationError); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); @@ -77,10 +92,6 @@ describe('TokenAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if user cannot be retrieved during login attempt', async () => { @@ -99,14 +110,11 @@ describe('TokenAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { @@ -114,10 +122,6 @@ describe('TokenAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -127,11 +131,12 @@ describe('TokenAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { @@ -139,36 +144,37 @@ describe('TokenAuthenticationProvider', () => { headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - accessToken: 'foo', - refreshToken: 'bar', - }); + await expect( + provider.authenticate(request, { accessToken: 'foo', refreshToken: 'bar' }) + ).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(request.headers.authorization).toBe('Bearer some-token'); - expect(authenticationResult.notHandled()).toBe(true); }); it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null - ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) ); }); @@ -182,19 +188,15 @@ describe('TokenAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization } } + ) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -226,15 +228,19 @@ describe('TokenAuthenticationProvider', () => { refreshToken: 'newbar', }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -248,20 +254,13 @@ describe('TokenAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if token refresh is rejected with unknown error', async () => { @@ -278,23 +277,16 @@ describe('TokenAuthenticationProvider', () => { const refreshError = new errors.InternalServerError('failed to refresh token'); mockOptions.tokens.refresh.mockRejectedValue(refreshError); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshError) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(refreshError); }); it('redirects non-AJAX requests to /login and clears session if token cannot be refreshed', async () => { @@ -310,26 +302,18 @@ describe('TokenAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/login?next=%2Fbase-path%2Fsome-path', { + state: null, + }) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization }, - }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fsome-path' - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toEqual(null); - expect(authenticationResult.error).toBeUndefined(); }); it('does not redirect AJAX requests if token token cannot be refreshed', async () => { @@ -348,25 +332,18 @@ describe('TokenAuthenticationProvider', () => { mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + expectAuthenticateCall(mockOptions.client, { headers: { 'kbn-xsrf': 'xsrf', authorization }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if new access token is rejected after successful refresh', async () => { @@ -397,16 +374,14 @@ describe('TokenAuthenticationProvider', () => { refreshToken: 'newbar', }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -414,11 +389,13 @@ describe('TokenAuthenticationProvider', () => { it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -430,13 +407,12 @@ describe('TokenAuthenticationProvider', () => { const failureReason = new Error('failed to delete token'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to /login if tokens are invalidated successfully', async () => { @@ -445,13 +421,12 @@ describe('TokenAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); }); it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => { @@ -460,13 +435,12 @@ describe('TokenAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?yep=nope') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?yep=nope'); }); }); });