[6.7] Force user to re-authenticate if token refresh fails with 400 status code. (#33839)

This commit is contained in:
Aleh Zasypkin 2019-03-26 15:10:48 +01:00 committed by GitHub
parent ab00b2e4f8
commit a8dcac07d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 440 additions and 200 deletions

View file

@ -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');
});
});
});

View file

@ -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);
});
});
});

View file

@ -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' } });

View file

@ -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;
}
}

View file

@ -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
);
});
});
});

View file

@ -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
);

View file

@ -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();

View file

@ -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}`;
}
}

View file

@ -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 });
}

View 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);
});
});
});

View 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;
}