Adding authc.grantAPIKeyAsInternalUser (#60423)

* Parsing the Authorization HTTP header to grant API keys

* Using HTTPAuthorizationHeader and BasicHTTPAuthorizationHeaderCredentials

* Adding tests for grantAPIKey

* Adding http_authentication/ folder

* Removing test route

* Using new classes to create the headers we pass to ES

* No longer .toLowerCase() when parsing the scheme from the request

* Updating snapshots

* Update x-pack/plugins/security/server/authentication/http_authentication/http_authorization_header.ts

Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com>

* Updating another inline snapshot

* Adding JSDoc

* Renaming `grant` to `grantAsInternalUser`

* Adding forgotten test. Fixing snapshot

* Fixing mock

* Apply suggestions from code review

Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-Authored-By: Mike Côté <mikecote@users.noreply.github.com>

* Using new classes for changing password

* Removing unneeded asScoped call

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>
This commit is contained in:
Brandon Kobel 2020-03-23 09:03:13 -07:00 committed by GitHub
parent 05c995a939
commit cca23c26fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 534 additions and 118 deletions

View file

@ -15,6 +15,8 @@ import {
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
const encodeToBase64 = (str: string) => Buffer.from(str).toString('base64');
describe('API Keys', () => {
let apiKeys: APIKeys;
let mockClusterClient: jest.Mocked<IClusterClient>;
@ -81,6 +83,87 @@ describe('API Keys', () => {
});
});
describe('grantAsInternalUser()', () => {
it('returns null when security feature is disabled', async () => {
mockLicense.isEnabled.mockReturnValue(false);
const result = await apiKeys.grantAsInternalUser(httpServerMock.createKibanaRequest());
expect(result).toBeNull();
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
});
it('calls callAsInternalUser with proper parameters for the Basic scheme', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
api_key: 'abc123',
});
const result = await apiKeys.grantAsInternalUser(
httpServerMock.createKibanaRequest({
headers: {
authorization: `Basic ${encodeToBase64('foo:bar')}`,
},
})
);
expect(result).toEqual({
api_key: 'abc123',
id: '123',
name: 'key-name',
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.grantAPIKey', {
body: {
grant_type: 'password',
username: 'foo',
password: 'bar',
},
});
});
it('calls callAsInternalUser with proper parameters for the Bearer scheme', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
api_key: 'abc123',
});
const result = await apiKeys.grantAsInternalUser(
httpServerMock.createKibanaRequest({
headers: {
authorization: `Bearer foo-access-token`,
},
})
);
expect(result).toEqual({
api_key: 'abc123',
id: '123',
name: 'key-name',
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.grantAPIKey', {
body: {
grant_type: 'access_token',
access_token: 'foo-access-token',
},
});
});
it('throw error for other schemes', async () => {
mockLicense.isEnabled.mockReturnValue(true);
await expect(
apiKeys.grantAsInternalUser(
httpServerMock.createKibanaRequest({
headers: {
authorization: `Digest username="foo"`,
},
})
)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unsupported scheme \\"Digest\\" for granting API Key"`
);
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
});
});
describe('invalidate()', () => {
it('returns null when security feature is disabled', async () => {
mockLicense.isEnabled.mockReturnValue(false);

View file

@ -6,6 +6,8 @@
import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { HTTPAuthorizationHeader } from './http_authentication';
import { BasicHTTPAuthorizationHeaderCredentials } from './http_authentication';
/**
* Represents the options to create an APIKey class instance that will be
@ -26,6 +28,13 @@ export interface CreateAPIKeyParams {
expiration?: string;
}
interface GrantAPIKeyParams {
grant_type: 'password' | 'access_token';
username?: string;
password?: string;
access_token?: string;
}
/**
* Represents the params for invalidating an API key
*/
@ -58,6 +67,21 @@ export interface CreateAPIKeyResult {
api_key: string;
}
export interface GrantAPIKeyResult {
/**
* Unique id for this API key
*/
id: string;
/**
* Name for this API key
*/
name: string;
/**
* Generated API key
*/
api_key: string;
}
/**
* The return value when invalidating an API key in Elasticsearch.
*/
@ -131,6 +155,39 @@ export class APIKeys {
return result;
}
/**
* Tries to grant an API key for the current user.
* @param request Request instance.
*/
async grantAsInternalUser(request: KibanaRequest) {
if (!this.license.isEnabled()) {
return null;
}
this.logger.debug('Trying to grant an API key');
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader == null) {
throw new Error(
`Unable to grant an API Key, request does not contain an authorization header`
);
}
const params = this.getGrantParams(authorizationHeader);
// User needs `manage_api_key` or `grant_api_key` privilege to use this API
let result: GrantAPIKeyResult;
try {
result = (await this.clusterClient.callAsInternalUser('shield.grantAPIKey', {
body: params,
})) as GrantAPIKeyResult;
this.logger.debug('API key was granted successfully');
} catch (e) {
this.logger.error(`Failed to grant API key: ${e.message}`);
throw e;
}
return result;
}
/**
* Tries to invalidate an API key.
* @param request Request instance.
@ -164,4 +221,26 @@ export class APIKeys {
return result;
}
private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams {
if (authorizationHeader.scheme.toLowerCase() === 'bearer') {
return {
grant_type: 'access_token',
access_token: authorizationHeader.credentials,
};
}
if (authorizationHeader.scheme.toLowerCase() === 'basic') {
const basicCredentials = BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(
authorizationHeader.credentials
);
return {
grant_type: 'password',
username: basicCredentials.username,
password: basicCredentials.password,
};
}
throw new Error(`Unsupported scheme "${authorizationHeader.scheme}" for granting API Key`);
}
}

View file

@ -1,58 +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 { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks';
import { getHTTPAuthenticationScheme } from './get_http_authentication_scheme';
describe('getHTTPAuthenticationScheme', () => {
it('returns `null` if request does not have authorization header', () => {
expect(getHTTPAuthenticationScheme(httpServerMock.createKibanaRequest())).toBeNull();
});
it('returns `null` if authorization header value isn not a string', () => {
expect(
getHTTPAuthenticationScheme(
httpServerMock.createKibanaRequest({
headers: { authorization: ['Basic xxx', 'Bearer xxx'] as any },
})
)
).toBeNull();
});
it('returns `null` if authorization header value is an empty string', () => {
expect(
getHTTPAuthenticationScheme(
httpServerMock.createKibanaRequest({ headers: { authorization: '' } })
)
).toBeNull();
});
it('returns only scheme portion of the authorization header value in lower case', () => {
const headerValueAndSchemeMap = [
['Basic xxx', 'basic'],
['Basic xxx yyy', 'basic'],
['basic xxx', 'basic'],
['basic', 'basic'],
// We don't trim leading whitespaces in scheme.
[' Basic xxx', ''],
['Negotiate xxx', 'negotiate'],
['negotiate xxx', 'negotiate'],
['negotiate', 'negotiate'],
['ApiKey xxx', 'apikey'],
['apikey xxx', 'apikey'],
['Api Key xxx', 'api'],
];
for (const [authorization, scheme] of headerValueAndSchemeMap) {
expect(
getHTTPAuthenticationScheme(
httpServerMock.createKibanaRequest({ headers: { authorization } })
)
).toBe(scheme);
}
});
});

View file

@ -1,21 +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 { KibanaRequest } from '../../../../../src/core/server';
/**
* Parses request's `Authorization` HTTP header if present and extracts authentication scheme.
* https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
* @param request Request instance to extract authentication scheme for.
*/
export function getHTTPAuthenticationScheme(request: KibanaRequest) {
const authorizationHeaderValue = request.headers.authorization;
if (!authorizationHeaderValue || typeof authorizationHeaderValue !== 'string') {
return null;
}
return authorizationHeaderValue.split(/\s+/)[0].toLowerCase();
}

View file

@ -0,0 +1,56 @@
/*
* 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 { BasicHTTPAuthorizationHeaderCredentials } from './basic_http_authorization_header_credentials';
const encodeToBase64 = (str: string) => Buffer.from(str).toString('base64');
describe('BasicHTTPAuthorizationHeaderCredentials.parseFromRequest()', () => {
it('parses username from the left-side of the single colon', () => {
const basicCredentials = BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(
encodeToBase64('fOo:bAr')
);
expect(basicCredentials.username).toBe('fOo');
});
it('parses username from the left-side of the first colon', () => {
const basicCredentials = BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(
encodeToBase64('fOo:bAr:bAz')
);
expect(basicCredentials.username).toBe('fOo');
});
it('parses password from the right-side of the single colon', () => {
const basicCredentials = BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(
encodeToBase64('fOo:bAr')
);
expect(basicCredentials.password).toBe('bAr');
});
it('parses password from the right-side of the first colon', () => {
const basicCredentials = BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(
encodeToBase64('fOo:bAr:bAz')
);
expect(basicCredentials.password).toBe('bAr:bAz');
});
it('throws error if there is no colon', () => {
expect(() => {
BasicHTTPAuthorizationHeaderCredentials.parseFromCredentials(encodeToBase64('fOobArbAz'));
}).toThrowErrorMatchingInlineSnapshot(
`"Unable to parse basic authentication credentials without a colon"`
);
});
});
describe(`toString()`, () => {
it('concatenates username and password using a colon and then base64 encodes the string', () => {
const basicCredentials = new BasicHTTPAuthorizationHeaderCredentials('elastic', 'changeme');
expect(basicCredentials.toString()).toEqual(Buffer.from(`elastic:changeme`).toString('base64')); // I don't like that this so closely mirror the actual implementation
expect(basicCredentials.toString()).toEqual('ZWxhc3RpYzpjaGFuZ2VtZQ=='); // and I don't like that this is so opaque. Both together seem reasonable...
});
});

View file

@ -0,0 +1,44 @@
/*
* 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.
*/
export class BasicHTTPAuthorizationHeaderCredentials {
/**
* Username, referred to as the `user-id` in https://tools.ietf.org/html/rfc7617.
*/
readonly username: string;
/**
* Password used to authenticate
*/
readonly password: string;
constructor(username: string, password: string) {
this.username = username;
this.password = password;
}
/**
* Parses the username and password from the credentials included in a HTTP Authorization header
* for the Basic scheme https://tools.ietf.org/html/rfc7617
* @param credentials The credentials extracted from the HTTP Authorization header
*/
static parseFromCredentials(credentials: string) {
const decoded = Buffer.from(credentials, 'base64').toString();
if (decoded.indexOf(':') === -1) {
throw new Error('Unable to parse basic authentication credentials without a colon');
}
const [username] = decoded.split(':');
// according to https://tools.ietf.org/html/rfc7617, everything
// after the first colon is considered to be part of the password
const password = decoded.substring(username.length + 1);
return new BasicHTTPAuthorizationHeaderCredentials(username, password);
}
toString() {
return Buffer.from(`${this.username}:${this.password}`).toString('base64');
}
}

View file

@ -0,0 +1,85 @@
/*
* 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 { httpServerMock } from '../../../../../../src/core/server/mocks';
import { HTTPAuthorizationHeader } from './http_authorization_header';
describe('HTTPAuthorizationHeader.parseFromRequest()', () => {
it('returns `null` if request does not have authorization header', () => {
expect(
HTTPAuthorizationHeader.parseFromRequest(httpServerMock.createKibanaRequest())
).toBeNull();
});
it('returns `null` if authorization header value is not a string', () => {
expect(
HTTPAuthorizationHeader.parseFromRequest(
httpServerMock.createKibanaRequest({
headers: { authorization: ['Basic xxx', 'Bearer xxx'] as any },
})
)
).toBeNull();
});
it('returns `null` if authorization header value is an empty string', () => {
expect(
HTTPAuthorizationHeader.parseFromRequest(
httpServerMock.createKibanaRequest({ headers: { authorization: '' } })
)
).toBeNull();
});
it('parses scheme portion of the authorization header value', () => {
const headerValueAndSchemeMap = [
['Basic xxx', 'Basic'],
['Basic xxx yyy', 'Basic'],
['basic xxx', 'basic'],
['basic', 'basic'],
// We don't trim leading whitespaces in scheme.
[' Basic xxx', ''],
['Negotiate xxx', 'Negotiate'],
['negotiate xxx', 'negotiate'],
['negotiate', 'negotiate'],
['ApiKey xxx', 'ApiKey'],
['apikey xxx', 'apikey'],
['Api Key xxx', 'Api'],
];
for (const [authorization, scheme] of headerValueAndSchemeMap) {
const header = HTTPAuthorizationHeader.parseFromRequest(
httpServerMock.createKibanaRequest({ headers: { authorization } })
);
expect(header).not.toBeNull();
expect(header!.scheme).toBe(scheme);
}
});
it('parses credentials portion of the authorization header value', () => {
const headerValueAndCredentialsMap = [
['xxx fOo', 'fOo'],
['xxx fOo bAr', 'fOo bAr'],
// We don't trim leading whitespaces in scheme.
[' xxx fOo', 'xxx fOo'],
];
for (const [authorization, credentials] of headerValueAndCredentialsMap) {
const header = HTTPAuthorizationHeader.parseFromRequest(
httpServerMock.createKibanaRequest({ headers: { authorization } })
);
expect(header).not.toBeNull();
expect(header!.credentials).toBe(credentials);
}
});
});
describe('toString()', () => {
it('concatenates scheme and credentials using a space', () => {
const header = new HTTPAuthorizationHeader('Bearer', 'some-access-token');
expect(header.toString()).toEqual('Bearer some-access-token');
});
});

View file

@ -0,0 +1,45 @@
/*
* 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 { KibanaRequest } from '../../../../../../src/core/server';
export class HTTPAuthorizationHeader {
/**
* The authentication scheme. Should be consumed in a case-insensitive manner.
* https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
*/
readonly scheme: string;
/**
* The authentication credentials for the scheme.
*/
readonly credentials: string;
constructor(scheme: string, credentials: string) {
this.scheme = scheme;
this.credentials = credentials;
}
/**
* Parses request's `Authorization` HTTP header if present.
* @param request Request instance to extract the authorization header from.
*/
static parseFromRequest(request: KibanaRequest) {
const authorizationHeaderValue = request.headers.authorization;
if (!authorizationHeaderValue || typeof authorizationHeaderValue !== 'string') {
return null;
}
const [scheme] = authorizationHeaderValue.split(/\s+/);
const credentials = authorizationHeaderValue.substring(scheme.length + 1);
return new HTTPAuthorizationHeader(scheme, credentials);
}
toString() {
return `${this.scheme} ${this.credentials}`;
}
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { BasicHTTPAuthorizationHeaderCredentials } from './basic_http_authorization_header_credentials';
export { HTTPAuthorizationHeader } from './http_authorization_header';

View file

@ -13,6 +13,7 @@ export const authenticationMock = {
isProviderEnabled: jest.fn(),
createAPIKey: jest.fn(),
getCurrentUser: jest.fn(),
grantAPIKeyAsInternalUser: jest.fn(),
invalidateAPIKey: jest.fn(),
isAuthenticated: jest.fn(),
getSessionInfo: jest.fn(),

View file

@ -369,6 +369,24 @@ describe('setupAuthentication()', () => {
});
});
describe('grantAPIKeyAsInternalUser()', () => {
let grantAPIKeyAsInternalUser: (request: KibanaRequest) => Promise<CreateAPIKeyResult | null>;
beforeEach(async () => {
grantAPIKeyAsInternalUser = (await setupAuthentication(mockSetupAuthenticationParams))
.grantAPIKeyAsInternalUser;
});
it('calls grantAsInternalUser', async () => {
const request = httpServerMock.createKibanaRequest();
const apiKeysInstance = jest.requireMock('./api_keys').APIKeys.mock.instances[0];
apiKeysInstance.grantAsInternalUser.mockResolvedValueOnce({ api_key: 'foo' });
await expect(grantAPIKeyAsInternalUser(request)).resolves.toEqual({
api_key: 'foo',
});
expect(apiKeysInstance.grantAsInternalUser).toHaveBeenCalledWith(request);
});
});
describe('invalidateAPIKey()', () => {
let invalidateAPIKey: (
request: KibanaRequest,

View file

@ -28,6 +28,10 @@ export {
CreateAPIKeyParams,
InvalidateAPIKeyParams,
} from './api_keys';
export {
BasicHTTPAuthorizationHeaderCredentials,
HTTPAuthorizationHeader,
} from './http_authentication';
interface SetupAuthenticationParams {
http: CoreSetup['http'];
@ -169,6 +173,7 @@ export async function setupAuthentication({
getCurrentUser,
createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) =>
apiKeys.create(request, params),
grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request),
invalidateAPIKey: (request: KibanaRequest, params: InvalidateAPIKeyParams) =>
apiKeys.invalidate(request, params),
isAuthenticated: (request: KibanaRequest) => http.auth.isAuthenticated(request),

View file

@ -8,7 +8,10 @@ import { KibanaRequest } from '../../../../../../src/core/server';
import { canRedirectRequest } from '../can_redirect_request';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import {
HTTPAuthorizationHeader,
BasicHTTPAuthorizationHeaderCredentials,
} from '../http_authentication';
import { BaseAuthenticationProvider } from './base';
/**
@ -54,7 +57,10 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Trying to perform a login.');
const authHeaders = {
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
authorization: new HTTPAuthorizationHeader(
'Basic',
new BasicHTTPAuthorizationHeaderCredentials(username, password).toString()
).toString(),
};
try {
@ -76,7 +82,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
if (getHTTPAuthenticationScheme(request) != null) {
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
return AuthenticationResult.notHandled();
}

View file

@ -7,7 +7,7 @@
import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base';
interface HTTPAuthenticationProviderOptions {
@ -38,7 +38,9 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
if ((httpOptions?.supportedSchemes?.size ?? 0) === 0) {
throw new Error('Supported schemes should be specified');
}
this.supportedSchemes = httpOptions.supportedSchemes;
this.supportedSchemes = new Set(
[...httpOptions.supportedSchemes].map(scheme => scheme.toLowerCase())
);
}
/**
@ -56,26 +58,26 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
const authenticationScheme = getHTTPAuthenticationScheme(request);
if (authenticationScheme == null) {
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader == null) {
this.logger.debug('Authorization header is not presented.');
return AuthenticationResult.notHandled();
}
if (!this.supportedSchemes.has(authenticationScheme)) {
this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`);
if (!this.supportedSchemes.has(authorizationHeader.scheme.toLowerCase())) {
this.logger.debug(`Unsupported authentication scheme: ${authorizationHeader.scheme}`);
return AuthenticationResult.notHandled();
}
try {
const user = await this.getUser(request);
this.logger.debug(
`Request to ${request.url.path} has been authenticated via authorization header with "${authenticationScheme}" scheme.`
`Request to ${request.url.path} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.`
);
return AuthenticationResult.succeeded(user);
} catch (err) {
this.logger.debug(
`Failed to authenticate request to ${request.url.path} via authorization header with "${authenticationScheme}" scheme: ${err.message}`
`Failed to authenticate request to ${request.url.path} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}`
);
return AuthenticationResult.failed(err);
}

View file

@ -12,7 +12,7 @@ import {
} from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { Tokens, TokenPair } from '../tokens';
import { BaseAuthenticationProvider } from './base';
@ -44,13 +44,13 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
const authenticationScheme = getHTTPAuthenticationScheme(request);
if (authenticationScheme && authenticationScheme !== 'negotiate') {
this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`);
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader && authorizationHeader.scheme.toLowerCase() !== 'negotiate') {
this.logger.debug(`Unsupported authentication scheme: ${authorizationHeader.scheme}`);
return AuthenticationResult.notHandled();
}
let authenticationResult = authenticationScheme
let authenticationResult = authorizationHeader
? await this.authenticateWithNegotiateScheme(request)
: AuthenticationResult.notHandled();
@ -175,7 +175,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
try {
// Then attempt to query for the user details using the new token
const authHeaders = { authorization: `Bearer ${tokens.access_token}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', tokens.access_token).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('User has been authenticated with new access token');
@ -205,7 +207,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
@ -242,7 +246,12 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${refreshedTokenPair.accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader(
'Bearer',
refreshedTokenPair.accessToken
).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via refreshed token.');

View file

@ -10,7 +10,7 @@ import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { canRedirectRequest } from '../can_redirect_request';
import { DeauthenticationResult } from '../deauthentication_result';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { Tokens, TokenPair } from '../tokens';
import {
AuthenticationProviderOptions,
@ -131,7 +131,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
if (getHTTPAuthenticationScheme(request) != null) {
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
return AuthenticationResult.notHandled();
}
@ -289,7 +289,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
@ -345,7 +347,12 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${refreshedTokenPair.accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader(
'Bearer',
refreshedTokenPair.accessToken
).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via refreshed token.');

View file

@ -9,7 +9,7 @@ import { DetailedPeerCertificate } from 'tls';
import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { Tokens } from '../tokens';
import { BaseAuthenticationProvider } from './base';
@ -45,7 +45,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
if (getHTTPAuthenticationScheme(request) != null) {
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
return AuthenticationResult.notHandled();
}
@ -156,7 +156,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
@ -207,7 +209,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
try {
// Then attempt to query for the user details using the new token
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('User has been authenticated with new access token');

View file

@ -10,7 +10,7 @@ import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { canRedirectRequest } from '../can_redirect_request';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { Tokens, TokenPair } from '../tokens';
import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base';
@ -181,7 +181,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
if (getHTTPAuthenticationScheme(request) != null) {
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
return AuthenticationResult.notHandled();
}
@ -390,7 +390,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
@ -445,7 +447,12 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${refreshedTokenPair.accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader(
'Bearer',
refreshedTokenPair.accessToken
).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via refreshed token.');

View file

@ -9,7 +9,7 @@ import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { canRedirectRequest } from '../can_redirect_request';
import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { Tokens, TokenPair } from '../tokens';
import { BaseAuthenticationProvider } from './base';
@ -60,7 +60,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Get token API request to Elasticsearch successful');
// Then attempt to query for the user details using the new token
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Login has been successfully performed.');
@ -82,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
if (getHTTPAuthenticationScheme(request) != null) {
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
return AuthenticationResult.notHandled();
}
@ -152,7 +154,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Trying to authenticate via state.');
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
@ -199,7 +203,12 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
}
try {
const authHeaders = { authorization: `Bearer ${refreshedTokenPair.accessToken}` };
const authHeaders = {
authorization: new HTTPAuthorizationHeader(
'Bearer',
refreshedTokenPair.accessToken
).toString(),
};
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via refreshed token.');

View file

@ -538,6 +538,24 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen
},
});
/**
* Grants an API key in Elasticsearch for the current user.
*
* @param {string} type The type of grant, either "password" or "access_token"
* @param {string} username Required when using the "password" type
* @param {string} password Required when using the "password" type
* @param {string} access_token Required when using the "access_token" type
*
* @returns {{api_key: string}}
*/
shield.grantAPIKey = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/api_key/grant',
},
});
/**
* Invalidates an API key in Elasticsearch.
*

View file

@ -74,6 +74,7 @@ describe('Security Plugin', () => {
"createAPIKey": [Function],
"getCurrentUser": [Function],
"getSessionInfo": [Function],
"grantAPIKeyAsInternalUser": [Function],
"invalidateAPIKey": [Function],
"isAuthenticated": [Function],
"isProviderEnabled": [Function],

View file

@ -8,6 +8,10 @@ import { schema } from '@kbn/config-schema';
import { canUserChangePassword } from '../../../common/model';
import { getErrorStatusCode, wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
import {
HTTPAuthorizationHeader,
BasicHTTPAuthorizationHeaderCredentials,
} from '../../authentication';
import { RouteDefinitionParams } from '..';
export function defineChangeUserPasswordRoutes({
@ -43,9 +47,13 @@ export function defineChangeUserPasswordRoutes({
? {
headers: {
...request.headers,
authorization: `Basic ${Buffer.from(`${username}:${currentPassword}`).toString(
'base64'
)}`,
authorization: new HTTPAuthorizationHeader(
'Basic',
new BasicHTTPAuthorizationHeaderCredentials(
username,
currentPassword || ''
).toString()
).toString(),
},
}
: request