Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail out of auth flow on first provider failure #26648

Merged
merged 1 commit into from
Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('Authenticator', () => {
}
});

it('fails if all authentication providers fail.', async () => {
it('fails if any authentication providers fail.', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we can't really test the distinction between any and all without fundamental changes to how providers are configured for the authenticator. Since they are currently hardcoded as an implementation detail that cannot be modified at runtime, there's no way to configure mock providers that verify the nuances of failure handling in different combinations of providers.

When we make auth providers pluggable, this problem will go away and we'll be able to test this properly.

const request = requestFixture({ headers: { authorization: 'Basic ***' } });
session.get.withArgs(request).returns(Promise.resolve(null));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ class Authenticator {
}
}

if (authenticationResult.failed()) {
return authenticationResult;
}

if (authenticationResult.succeeded()) {
// we have to do this here, as the auth scope's could be dependent on this
await this._authorizationMode.initialize(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from 'boom';
import expect from 'expect.js';
import sinon from 'sinon';
import { requestFixture } from '../../../__tests__/__fixtures__/request';
Expand Down Expand Up @@ -97,16 +96,15 @@ describe('BasicAuthenticationProvider', () => {
sinon.assert.calledOnce(callWithRequest);
});

it('fails if `authorization` header has unsupported schema even if state contains valid credentials.', async () => {
it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => {
const request = requestFixture({ headers: { authorization: 'Bearer ***' } });
const authorization = generateAuthorizationHeader('user', 'password');

const authenticationResult = await provider.authenticate(request, { authorization });

sinon.assert.notCalled(callWithRequest);
expect(request.headers.authorization).to.be('Bearer ***');
expect(authenticationResult.failed()).to.be(true);
expect(authenticationResult.error).to.eql(Boom.badRequest('Unsupported authentication schema: Bearer'));
expect(authenticationResult.notHandled()).to.be(true);
});

it('fails if state contains invalid credentials.', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('SAMLAuthenticationProvider', () => {
expect(authenticationResult.state).to.be(undefined);
});

it('fails if `authorization` header has unsupported schema even if state contains a valid token.', async () => {
it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => {
const request = requestFixture({ headers: { authorization: 'Basic some:credentials' } });

const authenticationResult = await provider.authenticate(request, {
Expand All @@ -216,8 +216,7 @@ describe('SAMLAuthenticationProvider', () => {

sinon.assert.notCalled(callWithRequest);
expect(request.headers.authorization).to.be('Basic some:credentials');
expect(authenticationResult.failed()).to.be(true);
expect(authenticationResult.error).to.eql(Boom.badRequest('Unsupported authentication schema: Basic'));
expect(authenticationResult.notHandled()).to.be(true);
});

it('fails if token from the state is rejected because of unknown reason.', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from 'boom';
import { canRedirectRequest } from '../../can_redirect_request';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
Expand All @@ -21,6 +20,14 @@ import { DeauthenticationResult } from '../deauthentication_result';
* }} ProviderOptions
*/

/**
* Object that represents return value of internal header auth
* @typedef {{
* authenticationResult: AuthenticationResult,
* headerNotRecognized?: boolean
* }} HeaderAuthAttempt
*/

/**
* Provider that supports request authentication via Basic HTTP Authentication.
*/
Expand Down Expand Up @@ -49,7 +56,13 @@ export class BasicAuthenticationProvider {
async authenticate(request, state) {
this._options.log(['debug', 'security', 'basic'], `Trying to authenticate user request to ${request.url.path}.`);

let authenticationResult = await this._authenticateViaHeader(request);
let {
authenticationResult,
headerNotRecognized, // eslint-disable-line prefer-const
} = await this._authenticateViaHeader(request);
if (headerNotRecognized) {
return authenticationResult;
}

if (authenticationResult.notHandled() && state) {
authenticationResult = await this._authenticateViaState(request, state);
Expand Down Expand Up @@ -81,7 +94,7 @@ export class BasicAuthenticationProvider {
* Validates whether request contains `Basic ***` Authorization header and just passes it
* forward to Elasticsearch backend.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<AuthenticationResult>}
* @returns {Promise.<HeaderAuthAttempt>}
* @private
*/
async _authenticateViaHeader(request) {
Expand All @@ -90,30 +103,33 @@ export class BasicAuthenticationProvider {
const authorization = request.headers.authorization;
if (!authorization) {
this._options.log(['debug', 'security', 'basic'], 'Authorization header is not presented.');
return AuthenticationResult.notHandled();
return {
authenticationResult: AuthenticationResult.notHandled()
};
}

const authenticationSchema = authorization.split(/\s+/)[0];
if (authenticationSchema.toLowerCase() !== 'basic') {
this._options.log(['debug', 'security', 'basic'], `Unsupported authentication schema: ${authenticationSchema}`);

// It's essential that we fail if non-empty, but unsupported authentication schema
// is provided to allow authenticator to consult other authentication providers
// that may support that schema.
return AuthenticationResult.failed(
Boom.badRequest(`Unsupported authentication schema: ${authenticationSchema}`)
);
return {
authenticationResult: AuthenticationResult.notHandled(),
headerNotRecognized: true
};
}

try {
const user = await this._options.client.callWithRequest(request, 'shield.authenticate');

this._options.log(['debug', 'security', 'basic'], 'Request has been authenticated via header.');

return AuthenticationResult.succeeded(user, { authorization });
return {
authenticationResult: AuthenticationResult.succeeded(user, { authorization })
};
} catch(err) {
this._options.log(['debug', 'security', 'basic'], `Failed to authenticate request via header: ${err.message}`);
return AuthenticationResult.failed(err);
return {
authenticationResult: AuthenticationResult.failed(err)
};
}
}

Expand Down
42 changes: 30 additions & 12 deletions x-pack/plugins/security/server/lib/authentication/providers/saml.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import { DeauthenticationResult } from '../deauthentication_result';
* }} ProviderOptions
*/

/**
* Object that represents return value of internal header auth
* @typedef {{
* authenticationResult: AuthenticationResult,
* headerNotRecognized?: boolean
* }} HeaderAuthAttempt
*/

/**
* Checks the error returned by Elasticsearch as the result of `authenticate` call and returns `true` if request
* has been rejected because of expired token, otherwise returns `false`.
Expand Down Expand Up @@ -74,7 +82,14 @@ export class SAMLAuthenticationProvider {
async authenticate(request, state) {
this._options.log(['debug', 'security', 'saml'], `Trying to authenticate user request to ${request.url.path}.`);

let authenticationResult = await this._authenticateViaHeader(request);
let {
authenticationResult,
headerNotRecognized, // eslint-disable-line prefer-const
} = await this._authenticateViaHeader(request);
if (headerNotRecognized) {
return authenticationResult;
}

if (state && authenticationResult.notHandled()) {
authenticationResult = await this._authenticateViaState(request, state);
if (authenticationResult.failed() && isAccessTokenExpiredError(authenticationResult.error)) {
Expand All @@ -98,7 +113,7 @@ export class SAMLAuthenticationProvider {
* Validates whether request contains `Bearer ***` Authorization header and just passes it
* forward to Elasticsearch backend.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<AuthenticationResult>}
* @returns {Promise.<HeaderAuthAttempt>}
* @private
*/
async _authenticateViaHeader(request) {
Expand All @@ -107,19 +122,18 @@ export class SAMLAuthenticationProvider {
const authorization = request.headers.authorization;
if (!authorization) {
this._options.log(['debug', 'security', 'saml'], 'Authorization header is not presented.');
return AuthenticationResult.notHandled();
return {
authenticationResult: AuthenticationResult.notHandled()
};
}

const authenticationSchema = authorization.split(/\s+/)[0];
if (authenticationSchema.toLowerCase() !== 'bearer') {
this._options.log(['debug', 'security', 'saml'], `Unsupported authentication schema: ${authenticationSchema}`);

// It's essential that we fail if non-empty, but unsupported authentication schema
// is provided to allow authenticator to consult other authentication providers
// that may support that schema.
return AuthenticationResult.failed(
Boom.badRequest(`Unsupported authentication schema: ${authenticationSchema}`)
);
return {
authenticationResult: AuthenticationResult.notHandled(),
headerNotRecognized: true
};
}

try {
Expand All @@ -130,10 +144,14 @@ export class SAMLAuthenticationProvider {

this._options.log(['debug', 'security', 'saml'], 'Request has been authenticated via header.');

return AuthenticationResult.succeeded(user);
return {
authenticationResult: AuthenticationResult.succeeded(user)
};
} catch(err) {
this._options.log(['debug', 'security', 'saml'], `Failed to authenticate request via header: ${err.message}`);
return AuthenticationResult.failed(err);
return {
authenticationResult: AuthenticationResult.failed(err)
};
}
}

Expand Down
2 changes: 1 addition & 1 deletion x-pack/test/api_integration/apis/security/basic_login.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export default function ({ getService }) {
.set('kbn-xsrf', 'xxx')
.set('Authorization', 'Bearer AbCdEf')
.set('Cookie', sessionCookie.cookieString())
.expect(400);
.expect(401);

expect(apiResponse.headers['set-cookie']).to.be(undefined);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export default function ({ getService }) {
.set('kbn-xsrf', 'xxx')
.set('Authorization', 'Basic AbCdEf')
.set('Cookie', sessionCookie.cookieString())
.expect(400);
.expect(401);

expect(apiResponse.headers['set-cookie']).to.be(undefined);
});
Expand Down