mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[6.7] Force user to re-authenticate if token refresh fails with 400
status code. (#33839)
This commit is contained in:
parent
ab00b2e4f8
commit
a8dcac07d2
11 changed files with 440 additions and 200 deletions
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
|
||||
import * as errors from '../errors';
|
||||
|
||||
describe('lib/errors', function () {
|
||||
describe('#wrapError', () => {
|
||||
it('returns given object', () => {
|
||||
const err = new Error();
|
||||
const returned = errors.wrapError(err);
|
||||
expect(returned).to.equal(err);
|
||||
});
|
||||
it('error becomes boom error', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err.isBoom).to.equal(true);
|
||||
});
|
||||
it('defaults output.statusCode to 500', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err.output.statusCode).to.equal(500);
|
||||
});
|
||||
it('sets output.statusCode to .status if given', () => {
|
||||
const err = new Error();
|
||||
err.status = 400;
|
||||
errors.wrapError(err);
|
||||
expect(err.output.statusCode).to.equal(400);
|
||||
});
|
||||
it('defaults message to "Internal Server Error"', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err.message).to.equal('Internal Server Error');
|
||||
});
|
||||
it('sets custom message if a 400 level error', () => {
|
||||
const err = new Error('wat');
|
||||
err.status = 499;
|
||||
errors.wrapError(err);
|
||||
expect(err.output.payload.message).to.equal('wat');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
|
||||
describe('AuthenticationResult', () => {
|
||||
describe('notHandled', () => {
|
||||
it('correctly produces `notHandled` authentication result.', () => {
|
||||
const authenticationResult = AuthenticationResult.notHandled();
|
||||
|
||||
expect(authenticationResult.notHandled()).to.be(true);
|
||||
expect(authenticationResult.succeeded()).to.be(false);
|
||||
expect(authenticationResult.failed()).to.be(false);
|
||||
expect(authenticationResult.redirected()).to.be(false);
|
||||
|
||||
expect(authenticationResult.user).to.be(undefined);
|
||||
expect(authenticationResult.state).to.be(undefined);
|
||||
expect(authenticationResult.error).to.be(undefined);
|
||||
expect(authenticationResult.redirectURL).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed', () => {
|
||||
it('fails if error is not specified.', () => {
|
||||
expect(() => AuthenticationResult.failed()).to.throwError('Error should be specified.');
|
||||
});
|
||||
|
||||
it('correctly produces `failed` authentication result.', () => {
|
||||
const failureReason = new Error('Something went wrong.');
|
||||
const authenticationResult = AuthenticationResult.failed(failureReason);
|
||||
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.notHandled()).to.be(false);
|
||||
expect(authenticationResult.succeeded()).to.be(false);
|
||||
expect(authenticationResult.redirected()).to.be(false);
|
||||
|
||||
expect(authenticationResult.error).to.be(failureReason);
|
||||
expect(authenticationResult.user).to.be(undefined);
|
||||
expect(authenticationResult.state).to.be(undefined);
|
||||
expect(authenticationResult.redirectURL).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('succeeded', () => {
|
||||
it('fails if user is not specified.', () => {
|
||||
expect(() => AuthenticationResult.succeeded()).to.throwError('User should be specified.');
|
||||
});
|
||||
|
||||
it('correctly produces `succeeded` authentication result without state.', () => {
|
||||
const user = { username: 'user' };
|
||||
const authenticationResult = AuthenticationResult.succeeded(user);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.failed()).to.be(false);
|
||||
expect(authenticationResult.notHandled()).to.be(false);
|
||||
expect(authenticationResult.redirected()).to.be(false);
|
||||
|
||||
expect(authenticationResult.user).to.be(user);
|
||||
expect(authenticationResult.state).to.be(undefined);
|
||||
expect(authenticationResult.error).to.be(undefined);
|
||||
expect(authenticationResult.redirectURL).to.be(undefined);
|
||||
});
|
||||
|
||||
it('correctly produces `succeeded` authentication result with state.', () => {
|
||||
const user = { username: 'user' };
|
||||
const state = { some: 'state' };
|
||||
const authenticationResult = AuthenticationResult.succeeded(user, state);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.failed()).to.be(false);
|
||||
expect(authenticationResult.notHandled()).to.be(false);
|
||||
expect(authenticationResult.redirected()).to.be(false);
|
||||
|
||||
expect(authenticationResult.user).to.be(user);
|
||||
expect(authenticationResult.state).to.be(state);
|
||||
expect(authenticationResult.error).to.be(undefined);
|
||||
expect(authenticationResult.redirectURL).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirectTo', () => {
|
||||
it('fails if redirect URL is not specified.', () => {
|
||||
expect(() => AuthenticationResult.redirectTo()).to.throwError('Redirect URL must be specified.');
|
||||
});
|
||||
|
||||
it('correctly produces `redirected` authentication result without state.', () => {
|
||||
const redirectURL = '/redirect/url';
|
||||
const authenticationResult = AuthenticationResult.redirectTo(redirectURL);
|
||||
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.succeeded()).to.be(false);
|
||||
expect(authenticationResult.failed()).to.be(false);
|
||||
expect(authenticationResult.notHandled()).to.be(false);
|
||||
|
||||
expect(authenticationResult.redirectURL).to.be(redirectURL);
|
||||
expect(authenticationResult.user).to.be(undefined);
|
||||
expect(authenticationResult.state).to.be(undefined);
|
||||
expect(authenticationResult.error).to.be(undefined);
|
||||
});
|
||||
|
||||
it('correctly produces `redirected` authentication result with state.', () => {
|
||||
const redirectURL = '/redirect/url';
|
||||
const state = { some: 'state' };
|
||||
const authenticationResult = AuthenticationResult.redirectTo(redirectURL, state);
|
||||
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.succeeded()).to.be(false);
|
||||
expect(authenticationResult.failed()).to.be(false);
|
||||
expect(authenticationResult.notHandled()).to.be(false);
|
||||
|
||||
expect(authenticationResult.redirectURL).to.be(redirectURL);
|
||||
expect(authenticationResult.state).to.be(state);
|
||||
expect(authenticationResult.user).to.be(undefined);
|
||||
expect(authenticationResult.error).to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,11 +29,10 @@ describe('Authenticator', () => {
|
|||
session = sinon.createStubInstance(Session);
|
||||
|
||||
config = { get: sinon.stub() };
|
||||
cluster = { callWithRequest: sinon.stub() };
|
||||
|
||||
// Cluster is returned by `getClient` function that is wrapped into `once` making cluster
|
||||
// a static singleton, so we should use sandbox to set/reset its behavior between tests.
|
||||
cluster = sinon.stub({ callWithRequest() {} });
|
||||
cluster = sinon.stub({ callWithRequest() {}, callWithInternalUser() {} });
|
||||
sandbox.stub(ClientShield, 'getClient').returns(cluster);
|
||||
|
||||
authorizationMode = { initialize: sinon.stub() };
|
||||
|
@ -381,6 +380,35 @@ describe('Authenticator', () => {
|
|||
sinon.assert.calledWithExactly(session.clear, notSystemAPIRequest);
|
||||
});
|
||||
|
||||
it('clears session if provider requested it via setting state to `null`.', async () => {
|
||||
// Use `token` provider for this test as it's the only one that does what we want.
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['token']);
|
||||
await initAuthenticator(server);
|
||||
authenticate = server.expose.withArgs('authenticate').lastCall.args[1];
|
||||
|
||||
const request = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
|
||||
session.get.withArgs(request).resolves({
|
||||
state: { accessToken: 'access-xxx', refreshToken: 'refresh-xxx' },
|
||||
provider: 'token'
|
||||
});
|
||||
|
||||
session.clear.resolves();
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).rejects({ body: { error: { reason: 'token expired' } } });
|
||||
|
||||
cluster.callWithInternalUser.withArgs('shield.getAccessToken').rejects(
|
||||
Boom.badRequest('refresh token expired')
|
||||
);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, request);
|
||||
});
|
||||
|
||||
it('does not clear session if provider failed to authenticate request with non-401 reason with active session.',
|
||||
async () => {
|
||||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
|
|
|
@ -185,4 +185,21 @@ export class AuthenticationResult {
|
|||
{ redirectURL, state }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether authentication result implies state update.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldUpdateState() {
|
||||
// State shouldn't be updated in case it wasn't set or was specifically set to `null`.
|
||||
return this._state != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether authentication result implies state clearing.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldClearState() {
|
||||
return this._state === null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AuthenticationResult } from './authentication_result';
|
||||
|
||||
describe('AuthenticationResult', () => {
|
||||
describe('notHandled', () => {
|
||||
it('correctly produces `notHandled` authentication result.', () => {
|
||||
const authenticationResult = AuthenticationResult.notHandled();
|
||||
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
expect(authenticationResult.succeeded()).toBe(false);
|
||||
expect(authenticationResult.failed()).toBe(false);
|
||||
expect(authenticationResult.redirected()).toBe(false);
|
||||
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
expect(authenticationResult.redirectURL).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed', () => {
|
||||
it('fails if error is not specified.', () => {
|
||||
expect(() => AuthenticationResult.failed()).toThrowError(
|
||||
'Error should be specified.'
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly produces `failed` authentication result.', () => {
|
||||
const failureReason = new Error('Something went wrong.');
|
||||
const authenticationResult = AuthenticationResult.failed(failureReason);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.notHandled()).toBe(false);
|
||||
expect(authenticationResult.succeeded()).toBe(false);
|
||||
expect(authenticationResult.redirected()).toBe(false);
|
||||
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.redirectURL).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('succeeded', () => {
|
||||
it('fails if user is not specified.', () => {
|
||||
expect(() => AuthenticationResult.succeeded()).toThrowError(
|
||||
'User should be specified.'
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly produces `succeeded` authentication result without state.', () => {
|
||||
const user = { username: 'user' };
|
||||
const authenticationResult = AuthenticationResult.succeeded(user);
|
||||
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.failed()).toBe(false);
|
||||
expect(authenticationResult.notHandled()).toBe(false);
|
||||
expect(authenticationResult.redirected()).toBe(false);
|
||||
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
expect(authenticationResult.redirectURL).toBeUndefined();
|
||||
});
|
||||
|
||||
it('correctly produces `succeeded` authentication result with state.', () => {
|
||||
const user = { username: 'user' };
|
||||
const state = { some: 'state' };
|
||||
const authenticationResult = AuthenticationResult.succeeded(user, state);
|
||||
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.failed()).toBe(false);
|
||||
expect(authenticationResult.notHandled()).toBe(false);
|
||||
expect(authenticationResult.redirected()).toBe(false);
|
||||
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.state).toBe(state);
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
expect(authenticationResult.redirectURL).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirectTo', () => {
|
||||
it('fails if redirect URL is not specified.', () => {
|
||||
expect(() => AuthenticationResult.redirectTo()).toThrowError(
|
||||
'Redirect URL must be specified.'
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly produces `redirected` authentication result without state.', () => {
|
||||
const redirectURL = '/redirect/url';
|
||||
const authenticationResult = AuthenticationResult.redirectTo(redirectURL);
|
||||
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.succeeded()).toBe(false);
|
||||
expect(authenticationResult.failed()).toBe(false);
|
||||
expect(authenticationResult.notHandled()).toBe(false);
|
||||
|
||||
expect(authenticationResult.redirectURL).toBe(redirectURL);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('correctly produces `redirected` authentication result with state.', () => {
|
||||
const redirectURL = '/redirect/url';
|
||||
const state = { some: 'state' };
|
||||
const authenticationResult = AuthenticationResult.redirectTo(redirectURL, state);
|
||||
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.succeeded()).toBe(false);
|
||||
expect(authenticationResult.failed()).toBe(false);
|
||||
expect(authenticationResult.notHandled()).toBe(false);
|
||||
|
||||
expect(authenticationResult.redirectURL).toBe(redirectURL);
|
||||
expect(authenticationResult.state).toBe(state);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldUpdateState', () => {
|
||||
it('always `false` for `failed`', () => {
|
||||
expect(AuthenticationResult.failed(new Error('error')).shouldUpdateState()).toBe(false);
|
||||
});
|
||||
|
||||
it('always `false` for `notHandled`', () => {
|
||||
expect(AuthenticationResult.notHandled().shouldUpdateState()).toBe(false);
|
||||
});
|
||||
|
||||
it('depends on `state` for `redirected`.', () => {
|
||||
const mockURL = 'some-url';
|
||||
expect(AuthenticationResult.redirectTo(mockURL, 'string').shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, 0).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, true).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, false).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, { prop: 'object' }).shouldUpdateState()).toBe(
|
||||
true
|
||||
);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, { prop: 'object' }).shouldUpdateState()).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
expect(AuthenticationResult.redirectTo(mockURL).shouldUpdateState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, undefined).shouldUpdateState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, null).shouldUpdateState()).toBe(false);
|
||||
});
|
||||
|
||||
it('depends on `state` for `succeeded`.', () => {
|
||||
const mockUser = { username: 'u' };
|
||||
expect(AuthenticationResult.succeeded(mockUser, 'string').shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.succeeded(mockUser, 0).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.succeeded(mockUser, true).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.succeeded(mockUser, false).shouldUpdateState()).toBe(true);
|
||||
expect(AuthenticationResult.succeeded(mockUser, { prop: 'object' }).shouldUpdateState()).toBe(
|
||||
true
|
||||
);
|
||||
expect(AuthenticationResult.succeeded(mockUser, { prop: 'object' }).shouldUpdateState()).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
expect(AuthenticationResult.succeeded(mockUser).shouldUpdateState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, undefined).shouldUpdateState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, null).shouldUpdateState()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldClearState', () => {
|
||||
it('always `false` for `failed`', () => {
|
||||
expect(AuthenticationResult.failed(new Error('error')).shouldClearState()).toBe(false);
|
||||
});
|
||||
|
||||
it('always `false` for `notHandled`', () => {
|
||||
expect(AuthenticationResult.notHandled().shouldClearState()).toBe(false);
|
||||
});
|
||||
|
||||
it('depends on `state` for `redirected`.', () => {
|
||||
const mockURL = 'some-url';
|
||||
expect(AuthenticationResult.redirectTo(mockURL, null).shouldClearState()).toBe(true);
|
||||
|
||||
expect(AuthenticationResult.redirectTo(mockURL).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, undefined).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, 'string').shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, 0).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, true).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, false).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, { prop: 'object' }).shouldClearState()).toBe(
|
||||
false
|
||||
);
|
||||
expect(AuthenticationResult.redirectTo(mockURL, { prop: 'object' }).shouldClearState()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('depends on `state` for `succeeded`.', () => {
|
||||
const mockUser = { username: 'u' };
|
||||
expect(AuthenticationResult.succeeded(mockUser, null).shouldClearState()).toBe(true);
|
||||
|
||||
expect(AuthenticationResult.succeeded(mockUser).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, undefined).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, 'string').shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, 0).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, true).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, false).shouldClearState()).toBe(false);
|
||||
expect(AuthenticationResult.succeeded(mockUser, { prop: 'object' }).shouldClearState()).toBe(
|
||||
false
|
||||
);
|
||||
expect(AuthenticationResult.succeeded(mockUser, { prop: 'object' }).shouldClearState()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { getClient } from '../../../../../server/lib/get_client_shield';
|
||||
import { AuthScopeService } from '../auth_scope_service';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { BasicAuthenticationProvider } from './providers/basic';
|
||||
import { SAMLAuthenticationProvider } from './providers/saml';
|
||||
import { TokenAuthenticationProvider } from './providers/token';
|
||||
|
@ -49,15 +50,6 @@ function getProviderOptions(server) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error code from Boom and Elasticsearch "native" errors.
|
||||
* @param {Error} error Error instance to extract status code from.
|
||||
* @returns {number}
|
||||
*/
|
||||
function getErrorStatusCode(error) {
|
||||
return error.isBoom ? error.output.statusCode : error.statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticator is responsible for authentication of the request using chain of
|
||||
* authentication providers. The chain is essentially a prioritized list of configured
|
||||
|
@ -151,21 +143,24 @@ class Authenticator {
|
|||
ownsSession ? existingSession.state : null
|
||||
);
|
||||
|
||||
if (ownsSession || authenticationResult.state) {
|
||||
if (ownsSession || authenticationResult.shouldUpdateState()) {
|
||||
// If authentication succeeds or requires redirect we should automatically extend existing user session,
|
||||
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
|
||||
// state we should store it in the session regardless of whether it's a system API request or not.
|
||||
const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected())
|
||||
&& (authenticationResult.state || !isSystemApiRequest);
|
||||
&& (authenticationResult.shouldUpdateState() || !isSystemApiRequest);
|
||||
|
||||
// If provider owned the session, but failed to authenticate anyway, that likely means
|
||||
// that session is not valid and we should clear it.
|
||||
if (authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401) {
|
||||
// If provider owned the session, but failed to authenticate anyway, that likely means that
|
||||
// session is not valid and we should clear it. Also provider can specifically ask to clear
|
||||
// session by setting it to `null` even if authentication attempt didn't fail.
|
||||
if (authenticationResult.shouldClearState() || (
|
||||
authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
|
||||
) {
|
||||
await this._session.clear(request);
|
||||
} else if (sessionCanBeUpdated) {
|
||||
await this._session.set(
|
||||
request,
|
||||
authenticationResult.state
|
||||
authenticationResult.shouldUpdateState()
|
||||
? { state: authenticationResult.state, provider: providerType }
|
||||
: existingSession
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import { errors } from 'elasticsearch';
|
||||
import { requestFixture } from '../../../__tests__/__fixtures__/request';
|
||||
import { LoginAttempt } from '../../login_attempt';
|
||||
import { TokenAuthenticationProvider } from '../token';
|
||||
|
@ -327,6 +328,58 @@ describe('TokenAuthenticationProvider', () => {
|
|||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
});
|
||||
|
||||
it('redirects non-AJAX requests to /login and clears session if token refresh fails with 400 error', async () => {
|
||||
const request = requestFixture({ path: '/some-path' });
|
||||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization: 'Bearer foo' } }), 'shield.authenticate')
|
||||
.rejects({ body: { error: { reason: 'token expired' } } });
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.rejects(new errors.BadRequest('failed to refresh token'));
|
||||
|
||||
const accessToken = 'foo';
|
||||
const refreshToken = 'bar';
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken, refreshToken });
|
||||
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/base-path/login?next=%2Fsome-path');
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(null);
|
||||
expect(authenticationResult.error).to.be.eql(undefined);
|
||||
});
|
||||
|
||||
it('does not redirect AJAX requests if token refresh fails with 400 error', async () => {
|
||||
const request = requestFixture({ headers: { 'kbn-xsrf': 'xsrf' }, path: '/some-path' });
|
||||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization: 'Bearer foo' } }), 'shield.authenticate')
|
||||
.rejects({ body: { error: { reason: 'token expired' } } });
|
||||
|
||||
const authenticationError = new errors.BadRequest('failed to refresh token');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.rejects(authenticationError);
|
||||
|
||||
const accessToken = 'foo';
|
||||
const refreshToken = 'bar';
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken, refreshToken });
|
||||
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.error).to.be(authenticationError);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
});
|
||||
|
||||
it('fails if new access token is rejected after successful refresh', async () => {
|
||||
const request = requestFixture();
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
import { canRedirectRequest } from '../../can_redirect_request';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
@ -91,10 +92,7 @@ export class TokenAuthenticationProvider {
|
|||
// finally, if authentication still can not be handled for this
|
||||
// request/state combination, redirect to the login page if appropriate
|
||||
if (authenticationResult.notHandled() && canRedirectRequest(request)) {
|
||||
const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`);
|
||||
authenticationResult = AuthenticationResult.redirectTo(
|
||||
`${this._options.basePath}/login?next=${nextURL}`
|
||||
);
|
||||
authenticationResult = AuthenticationResult.redirectTo(this._getLoginPageURL(request));
|
||||
}
|
||||
|
||||
return authenticationResult;
|
||||
|
@ -348,7 +346,30 @@ export class TokenAuthenticationProvider {
|
|||
// it's called with this request once again down the line (e.g. in the next authentication provider).
|
||||
delete request.headers.authorization;
|
||||
|
||||
// If refresh fails with `400` then refresh token is no longer valid and we should clear session
|
||||
// and redirect user to the login page to re-authenticate.
|
||||
if (getErrorStatusCode(err) === 400 && canRedirectRequest(request)) {
|
||||
this._options.log(
|
||||
['debug', 'security', 'token'],
|
||||
'Clearing session since both access and refresh tokens are expired.'
|
||||
);
|
||||
|
||||
// Set state to `null` to let `Authenticator` know that we want to clear current session.
|
||||
return AuthenticationResult.redirectTo(this._getLoginPageURL(request), null);
|
||||
}
|
||||
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs login page URL using current url path as `next` query string parameter.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_getLoginPageURL(request) {
|
||||
const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`);
|
||||
return `${this._options.basePath}/login?next=${nextURL}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { boomify } from 'boom';
|
||||
|
||||
export function wrapError(error) {
|
||||
return boomify(error, { statusCode: error.status });
|
||||
}
|
68
x-pack/plugins/security/server/lib/errors.test.ts
Normal file
68
x-pack/plugins/security/server/lib/errors.test.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
import * as errors from './errors';
|
||||
|
||||
describe('lib/errors', () => {
|
||||
describe('#wrapError', () => {
|
||||
it('returns given object', () => {
|
||||
const err = new Error();
|
||||
const returned = errors.wrapError(err);
|
||||
expect(returned).toEqual(err);
|
||||
});
|
||||
|
||||
it('error becomes boom error', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err).toHaveProperty('isBoom', true);
|
||||
});
|
||||
|
||||
it('defaults output.statusCode to 500', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err).toHaveProperty('output.statusCode', 500);
|
||||
});
|
||||
|
||||
it('sets output.statusCode to .status if given', () => {
|
||||
const err: any = new Error();
|
||||
err.status = 400;
|
||||
errors.wrapError(err);
|
||||
expect(err).toHaveProperty('output.statusCode', 400);
|
||||
});
|
||||
|
||||
it('defaults message to "Internal Server Error"', () => {
|
||||
const err = new Error();
|
||||
errors.wrapError(err);
|
||||
expect(err.message).toBe('Internal Server Error');
|
||||
});
|
||||
|
||||
it('sets custom message if a 400 level error', () => {
|
||||
const err: any = new Error('wat');
|
||||
err.status = 499;
|
||||
errors.wrapError(err);
|
||||
expect(err).toHaveProperty('output.payload.message', 'wat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getErrorStatusCode', () => {
|
||||
it('extracts status code from Boom error', () => {
|
||||
expect(errors.getErrorStatusCode(Boom.badRequest())).toBe(400);
|
||||
expect(errors.getErrorStatusCode(Boom.unauthorized())).toBe(401);
|
||||
});
|
||||
|
||||
it('extracts status code from Elasticsearch client error', () => {
|
||||
expect(errors.getErrorStatusCode(new esErrors.BadRequest())).toBe(400);
|
||||
expect(errors.getErrorStatusCode(new esErrors.AuthenticationException())).toBe(401);
|
||||
});
|
||||
|
||||
it('extracts status code from `status` property', () => {
|
||||
expect(errors.getErrorStatusCode({ statusText: 'Bad Request', status: 400 })).toBe(400);
|
||||
expect(errors.getErrorStatusCode({ statusText: 'Unauthorized', status: 401 })).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
19
x-pack/plugins/security/server/lib/errors.ts
Normal file
19
x-pack/plugins/security/server/lib/errors.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
|
||||
export function wrapError(error: any) {
|
||||
return Boom.boomify(error, { statusCode: getErrorStatusCode(error) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error code from Boom and Elasticsearch "native" errors.
|
||||
* @param error Error instance to extract status code from.
|
||||
*/
|
||||
export function getErrorStatusCode(error: any): number {
|
||||
return Boom.isBoom(error) ? error.output.statusCode : error.statusCode || error.status;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue