mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Typesciptify Token Authentication provider and move its tests to Jest. (#34973)
This commit is contained in:
parent
0e88c26efa
commit
885eba7792
3 changed files with 363 additions and 324 deletions
|
@ -130,7 +130,7 @@ export class BasicAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains a login payload and authenticates the
|
||||
* user if necessary.
|
||||
* @param HapiJS request instance.
|
||||
* @param request HapiJS request instance.
|
||||
*/
|
||||
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
|
||||
this.debug('Trying to authenticate via login attempt.');
|
||||
|
|
|
@ -4,25 +4,26 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import sinon from 'sinon';
|
||||
import { errors } from 'elasticsearch';
|
||||
import { requestFixture } from '../../../__tests__/__fixtures__/request';
|
||||
import { LoginAttempt } from '../../login_attempt';
|
||||
import { TokenAuthenticationProvider } from '../token';
|
||||
import sinon from 'sinon';
|
||||
import { requestFixture } from '../../__tests__/__fixtures__/request';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
import { TokenAuthenticationProvider } from './token';
|
||||
|
||||
describe('TokenAuthenticationProvider', () => {
|
||||
describe('`authenticate` method', () => {
|
||||
let provider;
|
||||
let callWithRequest;
|
||||
let callWithInternalUser;
|
||||
let provider: TokenAuthenticationProvider;
|
||||
let callWithRequest: sinon.SinonStub;
|
||||
let callWithInternalUser: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithRequest = sinon.stub();
|
||||
callWithInternalUser = sinon.stub();
|
||||
provider = new TokenAuthenticationProvider({
|
||||
client: { callWithRequest, callWithInternalUser },
|
||||
log() {},
|
||||
basePath: '/base-path'
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -34,7 +35,7 @@ describe('TokenAuthenticationProvider', () => {
|
|||
null
|
||||
);
|
||||
|
||||
expect(authenticationResult.notHandled()).to.be(true);
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
});
|
||||
|
||||
it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => {
|
||||
|
@ -43,43 +44,42 @@ describe('TokenAuthenticationProvider', () => {
|
|||
null
|
||||
);
|
||||
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be(
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe(
|
||||
'/base-path/login?next=%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not handle authentication if state exists, but accessToken property is missing.',
|
||||
async () => {
|
||||
const authenticationResult = await provider.authenticate(
|
||||
requestFixture({ headers: { 'kbn-xsrf': 'xsrf' } }),
|
||||
{}
|
||||
);
|
||||
it('does not handle authentication if state exists, but accessToken property is missing.', async () => {
|
||||
const authenticationResult = await provider.authenticate(
|
||||
requestFixture({ headers: { 'kbn-xsrf': 'xsrf' } }),
|
||||
{}
|
||||
);
|
||||
|
||||
expect(authenticationResult.notHandled()).to.be(true);
|
||||
});
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
});
|
||||
|
||||
it('succeeds with valid login attempt and stores in session', async () => {
|
||||
const user = { username: 'user' };
|
||||
const request = requestFixture();
|
||||
const loginAttempt = new LoginAttempt();
|
||||
loginAttempt.setCredentials('user', 'password');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'password', username: 'user', password: 'password' } })
|
||||
.returns(Promise.resolve({ access_token: 'foo', refresh_token: 'bar' }));
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'password', username: 'user', password: 'password' },
|
||||
})
|
||||
.resolves({ access_token: 'foo', refresh_token: 'bar' });
|
||||
|
||||
callWithRequest
|
||||
.withArgs(request, 'shield.authenticate')
|
||||
.returns(Promise.resolve(user));
|
||||
callWithRequest.withArgs(request, 'shield.authenticate').resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(user);
|
||||
expect(authenticationResult.state).to.be.eql({ accessToken: 'foo', refreshToken: 'bar' });
|
||||
expect(request.headers.authorization).to.be.eql(`Bearer foo`);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
expect(authenticationResult.state).toEqual({ accessToken: 'foo', refreshToken: 'bar' });
|
||||
expect(request.headers.authorization).toEqual(`Bearer foo`);
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
});
|
||||
|
||||
|
@ -90,12 +90,12 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization } }), 'shield.authenticate')
|
||||
.returns(Promise.resolve(user));
|
||||
.resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(user);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
});
|
||||
|
||||
|
@ -106,11 +106,13 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization } }), 'shield.authenticate')
|
||||
.returns(Promise.resolve(user));
|
||||
.resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(authenticationResult.state).not.to.eql({ authorization: request.headers.authorization });
|
||||
expect(authenticationResult.state).not.toEqual({
|
||||
authorization: request.headers.authorization,
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if only state is available.', async () => {
|
||||
|
@ -121,13 +123,13 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization } }), 'shield.authenticate')
|
||||
.returns(Promise.resolve(user));
|
||||
.resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken });
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(user);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
});
|
||||
|
||||
|
@ -140,23 +142,31 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.rejects({ statusCode: 401 });
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.returns(Promise.resolve({ access_token: 'newfoo', refresh_token: 'newbar' }));
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'refresh_token', refresh_token: 'bar' },
|
||||
})
|
||||
.resolves({ access_token: 'newfoo', refresh_token: 'newbar' });
|
||||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization: 'Bearer newfoo' } }), 'shield.authenticate')
|
||||
.withArgs(
|
||||
sinon.match({ headers: { authorization: 'Bearer newfoo' } }),
|
||||
'shield.authenticate'
|
||||
)
|
||||
.returns(user);
|
||||
|
||||
const accessToken = 'foo';
|
||||
const refreshToken = 'bar';
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.authenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(callWithRequest);
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(user);
|
||||
expect(authenticationResult.state).to.be.eql({ accessToken: 'newfoo', refreshToken: 'newbar' });
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' });
|
||||
});
|
||||
|
||||
it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => {
|
||||
|
@ -167,13 +177,13 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization } }), 'shield.authenticate')
|
||||
.returns(Promise.resolve(user));
|
||||
.resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken });
|
||||
|
||||
sinon.assert.notCalled(callWithRequest);
|
||||
expect(request.headers.authorization).to.be('Basic ***');
|
||||
expect(authenticationResult.notHandled()).to.be(true);
|
||||
expect(request.headers.authorization).toBe('Basic ***');
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
});
|
||||
|
||||
it('authenticates only via `authorization` header even if state is available.', async () => {
|
||||
|
@ -183,13 +193,13 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const user = { username: 'user' };
|
||||
|
||||
// GetUser will be called with request's `authorization` header.
|
||||
callWithRequest.withArgs(request, 'shield.authenticate').returns(Promise.resolve(user));
|
||||
callWithRequest.withArgs(request, 'shield.authenticate').resolves(user);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, { authorization });
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken });
|
||||
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(user);
|
||||
expect(authenticationResult.state).not.to.eql({ accessToken });
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual(user);
|
||||
expect(authenticationResult.state).not.toEqual({ accessToken });
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
});
|
||||
|
||||
|
@ -197,50 +207,52 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const request = requestFixture();
|
||||
const loginAttempt = new LoginAttempt();
|
||||
loginAttempt.setCredentials('user', 'password');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
const authenticationError = new Error('Invalid credentials');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'password', username: 'user', password: 'password' } })
|
||||
.returns(Promise.reject(authenticationError));
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'password', username: 'user', password: 'password' },
|
||||
})
|
||||
.rejects(authenticationError);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
sinon.assert.notCalled(callWithRequest);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
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 () => {
|
||||
const request = requestFixture();
|
||||
const loginAttempt = new LoginAttempt();
|
||||
loginAttempt.setCredentials('user', 'password');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'password', username: 'user', password: 'password' } })
|
||||
.returns(Promise.resolve({ access_token: 'foo', refresh_token: 'bar' }));
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'password', username: 'user', password: 'password' },
|
||||
})
|
||||
.resolves({ access_token: 'foo', refresh_token: 'bar' });
|
||||
|
||||
const authenticationError = new Error('Some error');
|
||||
callWithRequest
|
||||
.withArgs(request, 'shield.authenticate')
|
||||
.returns(Promise.reject(authenticationError));
|
||||
callWithRequest.withArgs(request, 'shield.authenticate').rejects(authenticationError);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
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 authentication with token from header fails with unknown error', async () => {
|
||||
|
@ -248,18 +260,16 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const request = requestFixture({ headers: { authorization } });
|
||||
|
||||
const authenticationError = new errors.InternalServerError('something went wrong');
|
||||
callWithRequest
|
||||
.withArgs(request, 'shield.authenticate')
|
||||
.rejects(authenticationError);
|
||||
callWithRequest.withArgs(request, 'shield.authenticate').rejects(authenticationError);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.error).toEqual(authenticationError);
|
||||
});
|
||||
|
||||
it('fails if authentication with token from state fails with unknown error.', async () => {
|
||||
|
@ -268,18 +278,21 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
const authenticationError = new errors.InternalServerError('something went wrong');
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization: `Bearer ${accessToken}` } }), 'shield.authenticate')
|
||||
.withArgs(
|
||||
sinon.match({ headers: { authorization: `Bearer ${accessToken}` } }),
|
||||
'shield.authenticate'
|
||||
)
|
||||
.rejects(authenticationError);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken });
|
||||
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
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 () => {
|
||||
|
@ -289,23 +302,28 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.withArgs(sinon.match({ headers: { authorization: 'Bearer foo' } }), 'shield.authenticate')
|
||||
.rejects({ statusCode: 401 });
|
||||
|
||||
const refreshError = new errors.InternalServerError('failed to refresh token');
|
||||
const refreshError = new errors.InternalServerError('failed to refresh token');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'refresh_token', refresh_token: 'bar' },
|
||||
})
|
||||
.rejects(refreshError);
|
||||
|
||||
const accessToken = 'foo';
|
||||
const refreshToken = 'bar';
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken, refreshToken });
|
||||
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.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(refreshError);
|
||||
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 document is missing', async () => {
|
||||
|
@ -319,22 +337,27 @@ describe('TokenAuthenticationProvider', () => {
|
|||
});
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.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 });
|
||||
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);
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe('/base-path/login?next=%2Fsome-path');
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual(null);
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('redirects non-AJAX requests to /login and clears session if token refresh fails with 400 error', async () => {
|
||||
|
@ -345,22 +368,27 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.rejects({ statusCode: 401 });
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.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 });
|
||||
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);
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe('/base-path/login?next=%2Fsome-path');
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual(null);
|
||||
expect(authenticationResult.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not redirect AJAX requests if token refresh fails with 400 error', async () => {
|
||||
|
@ -372,21 +400,26 @@ describe('TokenAuthenticationProvider', () => {
|
|||
|
||||
const authenticationError = new errors.BadRequest('failed to refresh token');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.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 });
|
||||
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);
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(authenticationError);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if new access token is rejected after successful refresh', async () => {
|
||||
|
@ -397,38 +430,48 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.rejects({ statusCode: 401 });
|
||||
|
||||
callWithInternalUser
|
||||
.withArgs('shield.getAccessToken', { body: { grant_type: 'refresh_token', refresh_token: 'bar' } })
|
||||
.returns(Promise.resolve({ access_token: 'newfoo', refresh_token: 'newbar' }));
|
||||
.withArgs('shield.getAccessToken', {
|
||||
body: { grant_type: 'refresh_token', refresh_token: 'bar' },
|
||||
})
|
||||
.resolves({ access_token: 'newfoo', refresh_token: 'newbar' });
|
||||
|
||||
const authenticationError = new errors.AuthenticationException('Some error');
|
||||
callWithRequest
|
||||
.withArgs(sinon.match({ headers: { authorization: 'Bearer newfoo' } }), 'shield.authenticate')
|
||||
.withArgs(
|
||||
sinon.match({ headers: { authorization: 'Bearer newfoo' } }),
|
||||
'shield.authenticate'
|
||||
)
|
||||
.rejects(authenticationError);
|
||||
|
||||
const accessToken = 'foo';
|
||||
const refreshToken = 'bar';
|
||||
const authenticationResult = await provider.authenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.authenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(callWithRequest);
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
|
||||
expect(request.headers).to.not.have.property('authorization');
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql(undefined);
|
||||
expect(authenticationResult.state).to.be.eql(undefined);
|
||||
expect(authenticationResult.error).to.be.eql(authenticationError);
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.user).toBeUndefined();
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
expect(authenticationResult.error).toEqual(authenticationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`deauthenticate` method', () => {
|
||||
let provider;
|
||||
let callWithInternalUser;
|
||||
let provider: TokenAuthenticationProvider;
|
||||
let callWithInternalUser: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithInternalUser = sinon.stub();
|
||||
provider = new TokenAuthenticationProvider({
|
||||
client: { callWithInternalUser },
|
||||
log() {},
|
||||
basePath: '/base-path'
|
||||
client: { callWithInternalUser } as any,
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -439,21 +482,24 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const refreshToken = 'bar';
|
||||
|
||||
let deauthenticateResult = await provider.deauthenticate(request);
|
||||
expect(deauthenticateResult.notHandled()).to.be(true);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
deauthenticateResult = await provider.deauthenticate(request, {});
|
||||
expect(deauthenticateResult.notHandled()).to.be(true);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
deauthenticateResult = await provider.deauthenticate(request, { accessToken });
|
||||
expect(deauthenticateResult.notHandled()).to.be(true);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
deauthenticateResult = await provider.deauthenticate(request, { refreshToken });
|
||||
expect(deauthenticateResult.notHandled()).to.be(true);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
sinon.assert.notCalled(callWithInternalUser);
|
||||
|
||||
deauthenticateResult = await provider.deauthenticate(request, { accessToken, refreshToken });
|
||||
expect(deauthenticateResult.notHandled()).to.be(false);
|
||||
deauthenticateResult = await provider.deauthenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
expect(deauthenticateResult.notHandled()).toBe(false);
|
||||
});
|
||||
|
||||
it('fails if call to delete access token responds with an error', async () => {
|
||||
|
@ -464,19 +510,20 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const failureReason = new Error('failed to delete token');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.deleteAccessToken', { body: { token: accessToken } })
|
||||
.returns(Promise.reject(failureReason));
|
||||
.rejects(failureReason);
|
||||
|
||||
const authenticationResult = await provider.deauthenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.deauthenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callWithInternalUser);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { token: accessToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { token: accessToken },
|
||||
});
|
||||
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.error).to.be(failureReason);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
|
||||
it('fails if call to delete refresh token responds with an error', async () => {
|
||||
|
@ -491,19 +538,20 @@ describe('TokenAuthenticationProvider', () => {
|
|||
const failureReason = new Error('failed to delete token');
|
||||
callWithInternalUser
|
||||
.withArgs('shield.deleteAccessToken', { body: { refresh_token: refreshToken } })
|
||||
.returns(Promise.reject(failureReason));
|
||||
.rejects(failureReason);
|
||||
|
||||
const authenticationResult = await provider.deauthenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.deauthenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(callWithInternalUser);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { refresh_token: refreshToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { refresh_token: refreshToken },
|
||||
});
|
||||
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.error).to.be(failureReason);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
|
||||
it('redirects to /login if tokens are deleted successfully', async () => {
|
||||
|
@ -519,22 +567,21 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.withArgs('shield.deleteAccessToken', { body: { refresh_token: refreshToken } })
|
||||
.returns({ invalidated_tokens: 1 });
|
||||
|
||||
const authenticationResult = await provider.deauthenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.deauthenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(callWithInternalUser);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { token: accessToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { refresh_token: refreshToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { token: accessToken },
|
||||
});
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { refresh_token: refreshToken },
|
||||
});
|
||||
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/base-path/login');
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe('/base-path/login');
|
||||
});
|
||||
|
||||
it('redirects to /login with optional search parameters if tokens are deleted successfully', async () => {
|
||||
|
@ -550,22 +597,21 @@ describe('TokenAuthenticationProvider', () => {
|
|||
.withArgs('shield.deleteAccessToken', { body: { refresh_token: refreshToken } })
|
||||
.returns({ created: true });
|
||||
|
||||
const authenticationResult = await provider.deauthenticate(request, { accessToken, refreshToken });
|
||||
const authenticationResult = await provider.deauthenticate(request, {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
sinon.assert.calledTwice(callWithInternalUser);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { token: accessToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(
|
||||
callWithInternalUser,
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { refresh_token: refreshToken } }
|
||||
);
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { token: accessToken },
|
||||
});
|
||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.deleteAccessToken', {
|
||||
body: { refresh_token: refreshToken },
|
||||
});
|
||||
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirectURL).to.be('/base-path/login?yep');
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe('/base-path/login?yep');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,37 +4,52 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
import { Request } from 'hapi';
|
||||
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { canRedirectRequest } from '../../can_redirect_request';
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
|
||||
/**
|
||||
* Object that represents available provider options.
|
||||
* @typedef {{
|
||||
* basePath: string,
|
||||
* client: Client,
|
||||
* log: Function
|
||||
* }} ProviderOptions
|
||||
* Represents available provider options.
|
||||
*/
|
||||
interface ProviderOptions {
|
||||
basePath: string;
|
||||
client: Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object that represents return value of internal header auth
|
||||
* @typedef {{
|
||||
* authenticationResult: AuthenticationResult,
|
||||
* headerNotRecognized?: boolean
|
||||
* }} HeaderAuthAttempt
|
||||
* The state supported by the provider.
|
||||
*/
|
||||
interface ProviderState {
|
||||
/**
|
||||
* Access token issued as the result of successful authentication and that should be provided with
|
||||
* every request to Elasticsearch on behalf of the authenticated user. This token will eventually expire.
|
||||
*/
|
||||
accessToken?: string;
|
||||
|
||||
/**
|
||||
* Once access token expires the refresh token is used to get a new pair of access/refresh tokens
|
||||
* without any user involvement. If not used this token will eventually expire as well.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
}
|
||||
|
||||
type RequestWithLoginAttempt = Request & {
|
||||
loginAttempt: () => LoginAttempt;
|
||||
};
|
||||
|
||||
/**
|
||||
* If request with access token fails with `401 Unauthorized` then this token is no
|
||||
* longer valid and we should try to refresh it. Another use case that we should
|
||||
* temporarily support (until elastic/elasticsearch#38866 is fixed) is when token
|
||||
* document has been removed and ES responds with `500 Internal Server Error`.
|
||||
* @param {Object} err Error returned from Elasticsearch.
|
||||
* @returns {boolean}
|
||||
* @param err Error returned from Elasticsearch.
|
||||
*/
|
||||
function isAccessTokenExpiredError(err) {
|
||||
function isAccessTokenExpiredError(err?: any) {
|
||||
const errorStatusCode = getErrorStatusCode(err);
|
||||
return (
|
||||
errorStatusCode === 401 ||
|
||||
|
@ -50,39 +65,29 @@ function isAccessTokenExpiredError(err) {
|
|||
* Provider that supports token-based request authentication.
|
||||
*/
|
||||
export class TokenAuthenticationProvider {
|
||||
/**
|
||||
* Server options that may be needed by authentication provider.
|
||||
* @type {?ProviderOptions}
|
||||
* @protected
|
||||
*/
|
||||
_options = null;
|
||||
|
||||
/**
|
||||
* Instantiates TokenAuthenticationProvider.
|
||||
* @param {ProviderOptions} options Provider options object.
|
||||
* @param options Options that may be needed by authentication provider.
|
||||
*/
|
||||
constructor(options) {
|
||||
this._options = options;
|
||||
}
|
||||
constructor(private readonly options: ProviderOptions) {}
|
||||
|
||||
/**
|
||||
* Performs token-based request authentication
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @param {Object} [state] Optional state object associated with the provider.
|
||||
* @returns {Promise.<AuthenticationResult>}
|
||||
* @param request HapiJS request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
async authenticate(request, state) {
|
||||
this._options.log(['debug', 'security', 'token'], `Trying to authenticate user request to ${request.url.path}.`);
|
||||
public async authenticate(request: RequestWithLoginAttempt, state?: ProviderState | null) {
|
||||
this.debug(`Trying to authenticate user request to ${request.url.path}.`);
|
||||
|
||||
// first try from login payload
|
||||
let authenticationResult = await this._authenticateViaLoginAttempt(request);
|
||||
let authenticationResult = await this.authenticateViaLoginAttempt(request);
|
||||
|
||||
// if there isn't a payload, try header-based token auth
|
||||
if (authenticationResult.notHandled()) {
|
||||
const {
|
||||
authenticationResult: headerAuthResult,
|
||||
headerNotRecognized,
|
||||
} = await this._authenticateViaHeader(request);
|
||||
} = await this.authenticateViaHeader(request);
|
||||
if (headerNotRecognized) {
|
||||
return headerAuthResult;
|
||||
}
|
||||
|
@ -91,16 +96,16 @@ export class TokenAuthenticationProvider {
|
|||
|
||||
// if we still can't attempt auth, try authenticating via state (session token)
|
||||
if (authenticationResult.notHandled() && state) {
|
||||
authenticationResult = await this._authenticateViaState(request, state);
|
||||
authenticationResult = await this.authenticateViaState(request, state);
|
||||
if (authenticationResult.failed() && isAccessTokenExpiredError(authenticationResult.error)) {
|
||||
authenticationResult = await this._authenticateViaRefreshToken(request, state);
|
||||
authenticationResult = await this.authenticateViaRefreshToken(request, state);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
authenticationResult = AuthenticationResult.redirectTo(this._getLoginPageURL(request));
|
||||
authenticationResult = AuthenticationResult.redirectTo(this.getLoginPageURL(request));
|
||||
}
|
||||
|
||||
return authenticationResult;
|
||||
|
@ -108,59 +113,59 @@ export class TokenAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Redirects user to the login page preserving query string parameters.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @param {Object} state State value previously stored by the provider.
|
||||
* @returns {Promise.<DeauthenticationResult>}
|
||||
* @param request HapiJS request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
async deauthenticate(request, state) {
|
||||
this._options.log(['debug', 'security', 'token'], `Trying to deauthenticate user via ${request.url.path}.`);
|
||||
public async deauthenticate(request: Request, state?: ProviderState | null) {
|
||||
this.debug(`Trying to deauthenticate user via ${request.url.path}.`);
|
||||
|
||||
if (!state || !state.accessToken || !state.refreshToken) {
|
||||
this._options.log(['debug', 'security', 'token'], 'There are no access and refresh tokens to invalidate.');
|
||||
this.debug('There are no access and refresh tokens to invalidate.');
|
||||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'Token-based logout has been initiated by the user.');
|
||||
|
||||
this.debug('Token-based logout has been initiated by the user.');
|
||||
|
||||
try {
|
||||
// First invalidate the access token.
|
||||
const { invalidated_tokens: invalidatedAccessTokensCount } = await this._options.client.callWithInternalUser(
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { token: state.accessToken } }
|
||||
);
|
||||
const {
|
||||
invalidated_tokens: invalidatedAccessTokensCount,
|
||||
} = await this.options.client.callWithInternalUser('shield.deleteAccessToken', {
|
||||
body: { token: state.accessToken },
|
||||
});
|
||||
|
||||
if (invalidatedAccessTokensCount === 0) {
|
||||
this._options.log(['debug', 'security', 'token'], 'User access token was already invalidated.');
|
||||
this.debug('User access token was already invalidated.');
|
||||
} else if (invalidatedAccessTokensCount === 1) {
|
||||
this._options.log(['debug', 'security', 'token'], 'User access token has been successfully invalidated.');
|
||||
this.debug('User access token has been successfully invalidated.');
|
||||
} else {
|
||||
this._options.log(['debug', 'security', 'token'],
|
||||
this.debug(
|
||||
`${invalidatedAccessTokensCount} user access tokens were invalidated, this is unexpected.`
|
||||
);
|
||||
}
|
||||
|
||||
// Then invalidate the refresh token.
|
||||
const { invalidated_tokens: invalidatedRefreshTokensCount } = await this._options.client.callWithInternalUser(
|
||||
'shield.deleteAccessToken',
|
||||
{ body: { refresh_token: state.refreshToken } }
|
||||
);
|
||||
const {
|
||||
invalidated_tokens: invalidatedRefreshTokensCount,
|
||||
} = await this.options.client.callWithInternalUser('shield.deleteAccessToken', {
|
||||
body: { refresh_token: state.refreshToken },
|
||||
});
|
||||
|
||||
if (invalidatedRefreshTokensCount === 0) {
|
||||
this._options.log(['debug', 'security', 'token'], 'User refresh token was already invalidated.');
|
||||
this.debug('User refresh token was already invalidated.');
|
||||
} else if (invalidatedRefreshTokensCount === 1) {
|
||||
this._options.log(['debug', 'security', 'token'], 'User refresh token has been successfully invalidated.');
|
||||
this.debug('User refresh token has been successfully invalidated.');
|
||||
} else {
|
||||
this._options.log(['debug', 'security', 'token'],
|
||||
this.debug(
|
||||
`${invalidatedRefreshTokensCount} user refresh tokens were invalidated, this is unexpected.`
|
||||
);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo(
|
||||
`${this._options.basePath}/login${request.url.search || ''}`
|
||||
`${this.options.basePath}/login${request.url.search || ''}`
|
||||
);
|
||||
} catch(err) {
|
||||
this._options.log(['debug', 'security', 'token'], `Failed invalidating user's access token: ${err.message}`);
|
||||
} catch (err) {
|
||||
this.debug(`Failed invalidating user's access token: ${err.message}`);
|
||||
return DeauthenticationResult.failed(err);
|
||||
}
|
||||
}
|
||||
|
@ -168,59 +173,48 @@ export class TokenAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains `Bearer ***` Authorization header and just passes it
|
||||
* forward to Elasticsearch backend.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<HeaderAuthAttempt>}
|
||||
* @private
|
||||
* @param request HapiJS request instance.
|
||||
*/
|
||||
async _authenticateViaHeader(request) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Trying to authenticate via header.');
|
||||
private async authenticateViaHeader(request: Request) {
|
||||
this.debug('Trying to authenticate via header.');
|
||||
|
||||
const authorization = request.headers.authorization;
|
||||
if (!authorization) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Authorization header is not presented.');
|
||||
return {
|
||||
authenticationResult: AuthenticationResult.notHandled()
|
||||
};
|
||||
this.debug('Authorization header is not presented.');
|
||||
return { authenticationResult: AuthenticationResult.notHandled() };
|
||||
}
|
||||
|
||||
const authenticationSchema = authorization.split(/\s+/)[0];
|
||||
if (authenticationSchema.toLowerCase() !== 'bearer') {
|
||||
this._options.log(['debug', 'security', 'token'], `Unsupported authentication schema: ${authenticationSchema}`);
|
||||
return {
|
||||
authenticationResult: AuthenticationResult.notHandled(),
|
||||
headerNotRecognized: true
|
||||
};
|
||||
this.debug(`Unsupported authentication schema: ${authenticationSchema}`);
|
||||
return { authenticationResult: AuthenticationResult.notHandled(), headerNotRecognized: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await this._options.client.callWithRequest(request, 'shield.authenticate');
|
||||
const user = await this.options.client.callWithRequest(request, 'shield.authenticate');
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'Request has been authenticated via header.');
|
||||
this.debug('Request has been authenticated via header.');
|
||||
|
||||
// We intentionally do not store anything in session state because token
|
||||
// header auth can only be used on a request by request basis.
|
||||
return {
|
||||
authenticationResult: AuthenticationResult.succeeded(user)
|
||||
};
|
||||
} catch(err) {
|
||||
this._options.log(['debug', 'security', 'token'], `Failed to authenticate request via header: ${err.message}`);
|
||||
return {
|
||||
authenticationResult: AuthenticationResult.failed(err)
|
||||
};
|
||||
return { authenticationResult: AuthenticationResult.succeeded(user) };
|
||||
} catch (err) {
|
||||
this.debug(`Failed to authenticate request via header: ${err.message}`);
|
||||
return { authenticationResult: AuthenticationResult.failed(err) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<AuthenticationResult>}
|
||||
* @private
|
||||
* Validates whether request contains a login payload and authenticates the
|
||||
* user if necessary.
|
||||
* @param request HapiJS request instance.
|
||||
*/
|
||||
async _authenticateViaLoginAttempt(request) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Trying to authenticate via login attempt.');
|
||||
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
|
||||
this.debug('Trying to authenticate via login attempt.');
|
||||
|
||||
const credentials = request.loginAttempt().getCredentials();
|
||||
if (!credentials) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Username and password not found in payload.');
|
||||
this.debug('Username and password not found in payload.');
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
|
@ -230,31 +224,31 @@ export class TokenAuthenticationProvider {
|
|||
const {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
} = await this._options.client.callWithInternalUser(
|
||||
'shield.getAccessToken',
|
||||
{ body: { grant_type: 'password', username, password } }
|
||||
);
|
||||
} = await this.options.client.callWithInternalUser('shield.getAccessToken', {
|
||||
body: { grant_type: 'password', username, password },
|
||||
});
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'Get token API request to Elasticsearch successful');
|
||||
this.debug('Get token API request to Elasticsearch successful');
|
||||
|
||||
// We validate that both access and refresh tokens exist in the response
|
||||
// so other private methods in this class can rely on them both existing.
|
||||
if (!accessToken) {
|
||||
throw new Error('Unexpected response from get token API - no access token present');
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('Unexpected response from get token API - no refresh token present');
|
||||
}
|
||||
|
||||
// Then attempt to query for the user details using the new token
|
||||
request.headers.authorization = `Bearer ${accessToken}`;
|
||||
const user = await this._options.client.callWithRequest(request, 'shield.authenticate');
|
||||
const user = await this.options.client.callWithRequest(request, 'shield.authenticate');
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'User has been authenticated with new access token');
|
||||
this.debug('User has been authenticated with new access token');
|
||||
|
||||
return AuthenticationResult.succeeded(user, { accessToken, refreshToken });
|
||||
} catch(err) {
|
||||
this._options.log(['debug', 'security', 'token'], `Failed to authenticate request via login attempt: ${err.message}`);
|
||||
} catch (err) {
|
||||
this.debug(`Failed to authenticate request via login attempt: ${err.message}`);
|
||||
|
||||
// Reset `Authorization` header we've just set. We know for sure that it hasn't been defined before,
|
||||
// otherwise it would have been used or completely rejected by the `authenticateViaHeader`.
|
||||
|
@ -270,28 +264,26 @@ export class TokenAuthenticationProvider {
|
|||
/**
|
||||
* Tries to extract authorization header from the state and adds it to the request before
|
||||
* it's forwarded to Elasticsearch backend.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @param {Object} state State value previously stored by the provider.
|
||||
* @returns {Promise.<AuthenticationResult>}
|
||||
* @private
|
||||
* @param request HapiJS request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
async _authenticateViaState(request, { accessToken }) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Trying to authenticate via state.');
|
||||
private async authenticateViaState(request: Request, { accessToken }: ProviderState) {
|
||||
this.debug('Trying to authenticate via state.');
|
||||
|
||||
if (!accessToken) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Access token is not found in state.');
|
||||
this.debug('Access token is not found in state.');
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
try {
|
||||
request.headers.authorization = `Bearer ${accessToken}`;
|
||||
const user = await this._options.client.callWithRequest(request, 'shield.authenticate');
|
||||
const user = await this.options.client.callWithRequest(request, 'shield.authenticate');
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'Request has been authenticated via state.');
|
||||
this.debug('Request has been authenticated via state.');
|
||||
|
||||
return AuthenticationResult.succeeded(user);
|
||||
} catch(err) {
|
||||
this._options.log(['debug', 'security', 'token'], `Failed to authenticate request via state: ${err.message}`);
|
||||
} catch (err) {
|
||||
this.debug(`Failed to authenticate request via state: ${err.message}`);
|
||||
|
||||
// Reset `Authorization` header we've just set. We know for sure that it hasn't been defined before,
|
||||
// otherwise it would have been used or completely rejected by the `authenticateViaHeader`.
|
||||
|
@ -308,16 +300,14 @@ export class TokenAuthenticationProvider {
|
|||
* This method is only called when authentication via access token stored in the state failed because of expired
|
||||
* token. So we should use refresh token, that is also stored in the state, to extend expired access token and
|
||||
* authenticate user with it.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @param {Object} state State value previously stored by the provider.
|
||||
* @returns {Promise.<AuthenticationResult>}
|
||||
* @private
|
||||
* @param request HapiJS request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
async _authenticateViaRefreshToken(request, { refreshToken }) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Trying to refresh access token.');
|
||||
private async authenticateViaRefreshToken(request: Request, { refreshToken }: ProviderState) {
|
||||
this.debug('Trying to refresh access token.');
|
||||
|
||||
if (!refreshToken) {
|
||||
this._options.log(['debug', 'security', 'token'], 'Refresh token is not found in state.');
|
||||
this.debug('Refresh token is not found in state.');
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
|
@ -326,34 +316,34 @@ export class TokenAuthenticationProvider {
|
|||
// kibana system user.
|
||||
const {
|
||||
access_token: newAccessToken,
|
||||
refresh_token: newRefreshToken
|
||||
} = await this._options.client.callWithInternalUser(
|
||||
'shield.getAccessToken',
|
||||
{ body: { grant_type: 'refresh_token', refresh_token: refreshToken } }
|
||||
);
|
||||
refresh_token: newRefreshToken,
|
||||
} = await this.options.client.callWithInternalUser('shield.getAccessToken', {
|
||||
body: { grant_type: 'refresh_token', refresh_token: refreshToken },
|
||||
});
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], `Request to refresh token via Elasticsearch's get token API successful`);
|
||||
this.debug(`Request to refresh token via Elasticsearch's get token API successful`);
|
||||
|
||||
// We validate that both access and refresh tokens exist in the response
|
||||
// so other private methods in this class can rely on them both existing.
|
||||
if (!newAccessToken) {
|
||||
throw new Error('Unexpected response from get token API - no access token present');
|
||||
}
|
||||
|
||||
if (!newRefreshToken) {
|
||||
throw new Error('Unexpected response from get token API - no refresh token present');
|
||||
}
|
||||
|
||||
request.headers.authorization = `Bearer ${newAccessToken}`;
|
||||
const user = await this._options.client.callWithRequest(request, 'shield.authenticate');
|
||||
const user = await this.options.client.callWithRequest(request, 'shield.authenticate');
|
||||
|
||||
this._options.log(['debug', 'security', 'token'], 'Request has been authenticated via refreshed token.');
|
||||
this.debug('Request has been authenticated via refreshed token.');
|
||||
|
||||
return AuthenticationResult.succeeded(
|
||||
user,
|
||||
{ accessToken: newAccessToken, refreshToken: newRefreshToken }
|
||||
);
|
||||
return AuthenticationResult.succeeded(user, {
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
});
|
||||
} catch (err) {
|
||||
this._options.log(['debug', 'security', 'token'], `Failed to refresh access token: ${err.message}`);
|
||||
this.debug(`Failed to refresh access token: ${err.message}`);
|
||||
|
||||
// Reset `Authorization` header we've just set. We know for sure that it hasn't been defined before,
|
||||
// otherwise it would have been used or completely rejected by the `authenticateViaHeader`.
|
||||
|
@ -365,13 +355,10 @@ export class TokenAuthenticationProvider {
|
|||
// 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.'
|
||||
);
|
||||
this.debug('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.redirectTo(this.getLoginPageURL(request), null);
|
||||
}
|
||||
|
||||
return AuthenticationResult.failed(err);
|
||||
|
@ -380,12 +367,18 @@ export class TokenAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Constructs login page URL using current url path as `next` query string parameter.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {string}
|
||||
* @private
|
||||
* @param request HapiJS request instance.
|
||||
*/
|
||||
_getLoginPageURL(request) {
|
||||
private getLoginPageURL(request: Request) {
|
||||
const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`);
|
||||
return `${this._options.basePath}/login?next=${nextURL}`;
|
||||
return `${this.options.basePath}/login?next=${nextURL}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs message with `debug` level and token/security related tags.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
private debug(message: string) {
|
||||
this.options.log(['debug', 'security', 'token'], message);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue