mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Fail out of auth flow on first provider failure (#26648)
In practical terms, the flexibility afforded by providers being able to recover from the failures of previously configured providers isn't compelling, but the ambiguity is not ideal.
This commit is contained in:
parent
8b9a422dc1
commit
4d0424529e
8 changed files with 70 additions and 35 deletions
|
@ -102,7 +102,7 @@ describe('Authenticator', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if all authentication providers fail.', async () => {
|
it('fails if any authentication providers fail.', async () => {
|
||||||
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
|
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
|
||||||
session.get.withArgs(request).returns(Promise.resolve(null));
|
session.get.withArgs(request).returns(Promise.resolve(null));
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,10 @@ class Authenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authenticationResult.failed()) {
|
||||||
|
return authenticationResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (authenticationResult.succeeded()) {
|
if (authenticationResult.succeeded()) {
|
||||||
// we have to do this here, as the auth scope's could be dependent on this
|
// we have to do this here, as the auth scope's could be dependent on this
|
||||||
await this._authorizationMode.initialize(request);
|
await this._authorizationMode.initialize(request);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Boom from 'boom';
|
|
||||||
import expect from 'expect.js';
|
import expect from 'expect.js';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { requestFixture } from '../../../__tests__/__fixtures__/request';
|
import { requestFixture } from '../../../__tests__/__fixtures__/request';
|
||||||
|
@ -97,7 +96,7 @@ describe('BasicAuthenticationProvider', () => {
|
||||||
sinon.assert.calledOnce(callWithRequest);
|
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 request = requestFixture({ headers: { authorization: 'Bearer ***' } });
|
||||||
const authorization = generateAuthorizationHeader('user', 'password');
|
const authorization = generateAuthorizationHeader('user', 'password');
|
||||||
|
|
||||||
|
@ -105,8 +104,7 @@ describe('BasicAuthenticationProvider', () => {
|
||||||
|
|
||||||
sinon.assert.notCalled(callWithRequest);
|
sinon.assert.notCalled(callWithRequest);
|
||||||
expect(request.headers.authorization).to.be('Bearer ***');
|
expect(request.headers.authorization).to.be('Bearer ***');
|
||||||
expect(authenticationResult.failed()).to.be(true);
|
expect(authenticationResult.notHandled()).to.be(true);
|
||||||
expect(authenticationResult.error).to.eql(Boom.badRequest('Unsupported authentication schema: Bearer'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if state contains invalid credentials.', async () => {
|
it('fails if state contains invalid credentials.', async () => {
|
||||||
|
|
|
@ -206,7 +206,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
expect(authenticationResult.state).to.be(undefined);
|
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 request = requestFixture({ headers: { authorization: 'Basic some:credentials' } });
|
||||||
|
|
||||||
const authenticationResult = await provider.authenticate(request, {
|
const authenticationResult = await provider.authenticate(request, {
|
||||||
|
@ -216,8 +216,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
|
|
||||||
sinon.assert.notCalled(callWithRequest);
|
sinon.assert.notCalled(callWithRequest);
|
||||||
expect(request.headers.authorization).to.be('Basic some:credentials');
|
expect(request.headers.authorization).to.be('Basic some:credentials');
|
||||||
expect(authenticationResult.failed()).to.be(true);
|
expect(authenticationResult.notHandled()).to.be(true);
|
||||||
expect(authenticationResult.error).to.eql(Boom.badRequest('Unsupported authentication schema: Basic'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if token from the state is rejected because of unknown reason.', async () => {
|
it('fails if token from the state is rejected because of unknown reason.', async () => {
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Boom from 'boom';
|
|
||||||
import { canRedirectRequest } from '../../can_redirect_request';
|
import { canRedirectRequest } from '../../can_redirect_request';
|
||||||
import { AuthenticationResult } from '../authentication_result';
|
import { AuthenticationResult } from '../authentication_result';
|
||||||
import { DeauthenticationResult } from '../deauthentication_result';
|
import { DeauthenticationResult } from '../deauthentication_result';
|
||||||
|
@ -21,6 +20,14 @@ import { DeauthenticationResult } from '../deauthentication_result';
|
||||||
* }} ProviderOptions
|
* }} 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.
|
* Provider that supports request authentication via Basic HTTP Authentication.
|
||||||
*/
|
*/
|
||||||
|
@ -49,7 +56,13 @@ export class BasicAuthenticationProvider {
|
||||||
async authenticate(request, state) {
|
async authenticate(request, state) {
|
||||||
this._options.log(['debug', 'security', 'basic'], `Trying to authenticate user request to ${request.url.path}.`);
|
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) {
|
if (authenticationResult.notHandled() && state) {
|
||||||
authenticationResult = await this._authenticateViaState(request, state);
|
authenticationResult = await this._authenticateViaState(request, state);
|
||||||
|
@ -81,7 +94,7 @@ export class BasicAuthenticationProvider {
|
||||||
* Validates whether request contains `Basic ***` Authorization header and just passes it
|
* Validates whether request contains `Basic ***` Authorization header and just passes it
|
||||||
* forward to Elasticsearch backend.
|
* forward to Elasticsearch backend.
|
||||||
* @param {Hapi.Request} request HapiJS request instance.
|
* @param {Hapi.Request} request HapiJS request instance.
|
||||||
* @returns {Promise.<AuthenticationResult>}
|
* @returns {Promise.<HeaderAuthAttempt>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _authenticateViaHeader(request) {
|
async _authenticateViaHeader(request) {
|
||||||
|
@ -90,19 +103,18 @@ export class BasicAuthenticationProvider {
|
||||||
const authorization = request.headers.authorization;
|
const authorization = request.headers.authorization;
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
this._options.log(['debug', 'security', 'basic'], 'Authorization header is not presented.');
|
this._options.log(['debug', 'security', 'basic'], 'Authorization header is not presented.');
|
||||||
return AuthenticationResult.notHandled();
|
return {
|
||||||
|
authenticationResult: AuthenticationResult.notHandled()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticationSchema = authorization.split(/\s+/)[0];
|
const authenticationSchema = authorization.split(/\s+/)[0];
|
||||||
if (authenticationSchema.toLowerCase() !== 'basic') {
|
if (authenticationSchema.toLowerCase() !== 'basic') {
|
||||||
this._options.log(['debug', 'security', 'basic'], `Unsupported authentication schema: ${authenticationSchema}`);
|
this._options.log(['debug', 'security', 'basic'], `Unsupported authentication schema: ${authenticationSchema}`);
|
||||||
|
return {
|
||||||
// It's essential that we fail if non-empty, but unsupported authentication schema
|
authenticationResult: AuthenticationResult.notHandled(),
|
||||||
// is provided to allow authenticator to consult other authentication providers
|
headerNotRecognized: true
|
||||||
// that may support that schema.
|
};
|
||||||
return AuthenticationResult.failed(
|
|
||||||
Boom.badRequest(`Unsupported authentication schema: ${authenticationSchema}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -110,10 +122,14 @@ export class BasicAuthenticationProvider {
|
||||||
|
|
||||||
this._options.log(['debug', 'security', 'basic'], 'Request has been authenticated via header.');
|
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) {
|
} catch(err) {
|
||||||
this._options.log(['debug', 'security', 'basic'], `Failed to authenticate request via header: ${err.message}`);
|
this._options.log(['debug', 'security', 'basic'], `Failed to authenticate request via header: ${err.message}`);
|
||||||
return AuthenticationResult.failed(err);
|
return {
|
||||||
|
authenticationResult: AuthenticationResult.failed(err)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,14 @@ import { DeauthenticationResult } from '../deauthentication_result';
|
||||||
* }} ProviderOptions
|
* }} 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
|
* 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`.
|
* has been rejected because of expired token, otherwise returns `false`.
|
||||||
|
@ -74,7 +82,14 @@ export class SAMLAuthenticationProvider {
|
||||||
async authenticate(request, state) {
|
async authenticate(request, state) {
|
||||||
this._options.log(['debug', 'security', 'saml'], `Trying to authenticate user request to ${request.url.path}.`);
|
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()) {
|
if (state && authenticationResult.notHandled()) {
|
||||||
authenticationResult = await this._authenticateViaState(request, state);
|
authenticationResult = await this._authenticateViaState(request, state);
|
||||||
if (authenticationResult.failed() && isAccessTokenExpiredError(authenticationResult.error)) {
|
if (authenticationResult.failed() && isAccessTokenExpiredError(authenticationResult.error)) {
|
||||||
|
@ -98,7 +113,7 @@ export class SAMLAuthenticationProvider {
|
||||||
* Validates whether request contains `Bearer ***` Authorization header and just passes it
|
* Validates whether request contains `Bearer ***` Authorization header and just passes it
|
||||||
* forward to Elasticsearch backend.
|
* forward to Elasticsearch backend.
|
||||||
* @param {Hapi.Request} request HapiJS request instance.
|
* @param {Hapi.Request} request HapiJS request instance.
|
||||||
* @returns {Promise.<AuthenticationResult>}
|
* @returns {Promise.<HeaderAuthAttempt>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _authenticateViaHeader(request) {
|
async _authenticateViaHeader(request) {
|
||||||
|
@ -107,19 +122,18 @@ export class SAMLAuthenticationProvider {
|
||||||
const authorization = request.headers.authorization;
|
const authorization = request.headers.authorization;
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
this._options.log(['debug', 'security', 'saml'], 'Authorization header is not presented.');
|
this._options.log(['debug', 'security', 'saml'], 'Authorization header is not presented.');
|
||||||
return AuthenticationResult.notHandled();
|
return {
|
||||||
|
authenticationResult: AuthenticationResult.notHandled()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticationSchema = authorization.split(/\s+/)[0];
|
const authenticationSchema = authorization.split(/\s+/)[0];
|
||||||
if (authenticationSchema.toLowerCase() !== 'bearer') {
|
if (authenticationSchema.toLowerCase() !== 'bearer') {
|
||||||
this._options.log(['debug', 'security', 'saml'], `Unsupported authentication schema: ${authenticationSchema}`);
|
this._options.log(['debug', 'security', 'saml'], `Unsupported authentication schema: ${authenticationSchema}`);
|
||||||
|
return {
|
||||||
// It's essential that we fail if non-empty, but unsupported authentication schema
|
authenticationResult: AuthenticationResult.notHandled(),
|
||||||
// is provided to allow authenticator to consult other authentication providers
|
headerNotRecognized: true
|
||||||
// that may support that schema.
|
};
|
||||||
return AuthenticationResult.failed(
|
|
||||||
Boom.badRequest(`Unsupported authentication schema: ${authenticationSchema}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -130,10 +144,14 @@ export class SAMLAuthenticationProvider {
|
||||||
|
|
||||||
this._options.log(['debug', 'security', 'saml'], 'Request has been authenticated via header.');
|
this._options.log(['debug', 'security', 'saml'], 'Request has been authenticated via header.');
|
||||||
|
|
||||||
return AuthenticationResult.succeeded(user);
|
return {
|
||||||
|
authenticationResult: AuthenticationResult.succeeded(user)
|
||||||
|
};
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
this._options.log(['debug', 'security', 'saml'], `Failed to authenticate request via header: ${err.message}`);
|
this._options.log(['debug', 'security', 'saml'], `Failed to authenticate request via header: ${err.message}`);
|
||||||
return AuthenticationResult.failed(err);
|
return {
|
||||||
|
authenticationResult: AuthenticationResult.failed(err)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -189,7 +189,7 @@ export default function ({ getService }) {
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.set('Authorization', 'Bearer AbCdEf')
|
.set('Authorization', 'Bearer AbCdEf')
|
||||||
.set('Cookie', sessionCookie.cookieString())
|
.set('Cookie', sessionCookie.cookieString())
|
||||||
.expect(400);
|
.expect(401);
|
||||||
|
|
||||||
expect(apiResponse.headers['set-cookie']).to.be(undefined);
|
expect(apiResponse.headers['set-cookie']).to.be(undefined);
|
||||||
});
|
});
|
||||||
|
|
|
@ -275,7 +275,7 @@ export default function ({ getService }) {
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.set('Authorization', 'Basic AbCdEf')
|
.set('Authorization', 'Basic AbCdEf')
|
||||||
.set('Cookie', sessionCookie.cookieString())
|
.set('Cookie', sessionCookie.cookieString())
|
||||||
.expect(400);
|
.expect(401);
|
||||||
|
|
||||||
expect(apiResponse.headers['set-cookie']).to.be(undefined);
|
expect(apiResponse.headers['set-cookie']).to.be(undefined);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue