Skip to content

Improve authentication handling #908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions docs/authentication.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,105 @@

This document contains code snippets to show you how to connect to various Elasticsearch providers.

=== Basic Auth

You can provide your credentials in the node(s) URL.
=== Elastic Cloud

If you are using https://www.elastic.co/cloud[Elastic Cloud], the client offers a easy way to connect to it via the `cloud` option. +
You must pass the Cloud ID that you can find in the cloud console, then your username and password inside the `auth` option.

NOTE: When connecting to Elastic Cloud, the client will automatically enable both request and response compression by default, since it yields significant throughput improvements. +
Moreover, the client will also set the ssl option `secureProtocol` to `TLSv1_2_method` unless specified otherwise.
You can still override this option by configuring them.

IMPORTANT: Do not enable sniffing when using Elastic Cloud, since the nodes are behind a load balancer, Elastic Cloud will take care of everything for you.

[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://username:password@localhost:9200'
cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
},
auth: {
username: 'elastic',
password: 'changeme'
}
})
----

Or you can use the full node declaration.
=== Basic authentication

You can provide your credentials by passing the `username` and `password` parameters via the `auth` option.

[source,js]
----
const { URL } = require('url')
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: {
url: new URL('https://username:password@localhost:9200'),
id: 'node-1',
...
node: 'https://localhost:9200',
auth: {
username: 'elastic',
password: 'changeme'
}
})
----

=== SSL configuration
Otherwise, you can provide your credentials in the node(s) URL.

Without any additional configuration you can specify `https://` node urls, but the certificates used to sign these requests will not verified (`rejectUnauthorized: false`). To turn on certificate verification you must specify an `ssl` object either in the top level config or in each host config object and set `rejectUnauthorized: true`. The ssl config object can contain many of the same configuration options that https://nodejs.org/api/tls.html#tls_tls_connect_options_callback[tls.connect()] accepts.
[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://username:password@localhost:9200'
})
----

=== ApiKey authentication

You can use the https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey] authentication by passing the `apiKey` parameter via the `auth` option. +
The `apiKey` parameter can be either a base64 encoded string or an object with the values that you can obtain from the https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[create api key endpoint].

[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'http://username:password@localhost:9200',
ssl: {
ca: fs.readFileSync('./cacert.pem'),
rejectUnauthorized: true
node: 'https://localhost:9200',
auth: {
apiKey: 'base64EncodedKey'
}
})
----

=== Elastic Cloud
[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
node: 'https://localhost:9200',
auth: {
apiKey: {
id: 'foo',
api_key: 'bar'
}
}
})
----

If you are using https://www.elastic.co/cloud[Elastic Cloud], the client offers a easy way to connect to it via the `cloud` option. +
You must pass the Cloud ID that you can find in the cloud console, then your username and password.

NOTE: When connecting to Elastic Cloud, the client will automatically enable both request and response compression by default, since it yields significant throughput improvements. +
Moreover, the client will also set the ssl option `secureProtocol` to `TLSv1_2_method` unless specified otherwise.
You can still override this option by configuring them.
=== SSL configuration

IMPORTANT: Do not enable sniffing when using Elastic Cloud, since the nodes are behind a load balancer, Elastic Cloud will take care of everything for you.
Without any additional configuration you can specify `https://` node urls, but the certificates used to sign these requests will not verified (`rejectUnauthorized: false`). To turn on certificate verification you must specify an `ssl` object either in the top level config or in each host config object and set `rejectUnauthorized: true`. The ssl config object can contain many of the same configuration options that https://nodejs.org/api/tls.html#tls_tls_connect_options_callback[tls.connect()] accepts.

[source,js]
----
const { Client } = require('@elastic/elasticsearch')
const client = new Client({
cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
node: 'http://localhost:9200',
auth: {
username: 'elastic',
password: 'changeme'
},
ssl: {
ca: fs.readFileSync('./cacert.pem'),
rejectUnauthorized: true
}
})
----
26 changes: 25 additions & 1 deletion docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,28 @@ node: {
}
----

|`auth`
a|Your authentication data. You can use both Basic authentication and https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey]. +
See https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/auth-reference.html[Authentication] for more details. +
_Default:_ `null`

Basic authentication:
[source,js]
----
auth: {
username: 'elastic',
password: 'changeme'
}
----
https://www.elastic.co/guide/en/elasticsearch/reference/7.x/security-api-create-api-key.html[ApiKey] authentication:
[source,js]
----
auth: {
apiKey: 'base64EncodedKey'
}
----


|`maxRetries`
|`number` - Max number of retries for each request. +
_Default:_ `3`
Expand Down Expand Up @@ -163,7 +185,9 @@ _Cloud configuration example:_
----
const client = new Client({
cloud: {
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA=='
},
auth: {
username: 'elastic',
password: 'changeme'
}
Expand Down
4 changes: 3 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import Transport, {
} from './lib/Transport';
import { URL } from 'url';
import Connection, { AgentOptions, agentFn } from './lib/Connection';
import ConnectionPool, { ResurrectEvent } from './lib/ConnectionPool';
import ConnectionPool, { ResurrectEvent, BasicAuth, ApiKeyAuth } from './lib/ConnectionPool';
import Serializer from './lib/Serializer';
import * as RequestParams from './api/requestParams';
import * as errors from './lib/errors';
Expand Down Expand Up @@ -111,8 +111,10 @@ interface ClientOptions {
headers?: anyObject;
generateRequestId?: generateRequestIdFn;
name?: string;
auth?: BasicAuth | ApiKeyAuth;
cloud?: {
id: string;
// TODO: remove username and password here in 8
username: string;
password: string;
}
Expand Down
52 changes: 50 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'use strict'

const { EventEmitter } = require('events')
const { URL } = require('url')
const debug = require('debug')('elasticsearch')
const Transport = require('./lib/Transport')
const Connection = require('./lib/Connection')
Expand All @@ -43,7 +44,12 @@ class Client extends EventEmitter {
// the url is a string divided by two '$', the first is the cloud url
// the second the elasticsearch instance, the third the kibana instance
const cloudUrls = Buffer.from(id.split(':')[1], 'base64').toString().split('$')
opts.node = `https://${username}:${password}@${cloudUrls[1]}.${cloudUrls[0]}`

// TODO: remove username and password here in 8
if (username && password) {
opts.auth = Object.assign({}, opts.auth, { username, password })
}
opts.node = `https://${cloudUrls[1]}.${cloudUrls[0]}`

// Cloud has better performances with compression enabled
// see https://github.com/elastic/elasticsearch-py/pull/704.
Expand All @@ -61,6 +67,11 @@ class Client extends EventEmitter {
throw new ConfigurationError('Missing node(s) option')
}

const checkAuth = getAuth(opts.node || opts.nodes)
if (checkAuth && checkAuth.username && checkAuth.password) {
opts.auth = Object.assign({}, opts.auth, { username: checkAuth.username, password: checkAuth.password })
}

const options = Object.assign({}, {
Connection,
ConnectionPool,
Expand All @@ -82,7 +93,8 @@ class Client extends EventEmitter {
nodeFilter: null,
nodeSelector: 'round-robin',
generateRequestId: null,
name: 'elasticsearch-js'
name: 'elasticsearch-js',
auth: null
}, opts)

this[kInitialOptions] = options
Expand All @@ -96,6 +108,7 @@ class Client extends EventEmitter {
ssl: options.ssl,
agent: options.agent,
Connection: options.Connection,
auth: options.auth,
emit: this.emit.bind(this),
sniffEnabled: options.sniffInterval !== false ||
options.sniffOnStart !== false ||
Expand Down Expand Up @@ -209,6 +222,41 @@ class Client extends EventEmitter {
}
}

function getAuth (node) {
if (Array.isArray(node)) {
for (const url of node) {
const auth = getUsernameAndPassword(url)
if (auth.username !== '' && auth.password !== '') {
return auth
}
}

return null
}

const auth = getUsernameAndPassword(node)
if (auth.username !== '' && auth.password !== '') {
return auth
}

return null

function getUsernameAndPassword (node) {
if (typeof node === 'string') {
const { username, password } = new URL(node)
return {
username: decodeURIComponent(username),
password: decodeURIComponent(password)
}
} else if (node.url instanceof URL) {
return {
username: decodeURIComponent(node.url.username),
password: decodeURIComponent(node.url.password)
}
}
}
}

const events = {
RESPONSE: 'response',
REQUEST: 'request',
Expand Down
2 changes: 2 additions & 0 deletions lib/Connection.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import { URL } from 'url';
import { inspect, InspectOptions } from 'util';
import { ApiKeyAuth, BasicAuth } from './ConnectionPool'
import * as http from 'http';
import { ConnectionOptions as TlsConnectionOptions } from 'tls';

Expand All @@ -34,6 +35,7 @@ interface ConnectionOptions {
agent?: AgentOptions | agentFn;
status?: string;
roles?: any;
auth?: BasicAuth | ApiKeyAuth;
}

interface RequestOptions extends http.ClientRequestArgs {
Expand Down
38 changes: 30 additions & 8 deletions lib/Connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ class Connection {
this.url = opts.url
this.ssl = opts.ssl || null
this.id = opts.id || stripAuth(opts.url.href)
this.headers = opts.headers || null
this.auth = opts.auth || { username: null, password: null }
this.headers = prepareHeaders(opts.headers, opts.auth)
this.deadCount = 0
this.resurrectTimeout = 0

Expand Down Expand Up @@ -181,7 +180,6 @@ class Connection {

buildRequestObject (params) {
const url = this.url
const { username, password } = this.auth
const request = {
protocol: url.protocol,
hostname: url.hostname[0] === '['
Expand All @@ -196,9 +194,6 @@ class Connection {
// https://github.com/elastic/elasticsearch-js/issues/843
port: url.port !== '' ? url.port : undefined,
headers: this.headers,
auth: username != null && password != null
? `${username}:${password}`
: undefined,
agent: this.agent
}

Expand Down Expand Up @@ -230,10 +225,15 @@ class Connection {
// the logs very hard to read. The user can still
// access them with `instance.agent` and `instance.ssl`.
[inspect.custom] (depth, options) {
const {
authorization,
...headers
} = this.headers

return {
url: stripAuth(this.url.toString()),
id: this.id,
headers: this.headers,
headers,
deadCount: this.deadCount,
resurrectTimeout: this.resurrectTimeout,
_openRequests: this._openRequests,
Expand All @@ -243,10 +243,15 @@ class Connection {
}

toJSON () {
const {
authorization,
...headers
} = this.headers

return {
url: stripAuth(this.url.toString()),
id: this.id,
headers: this.headers,
headers,
deadCount: this.deadCount,
resurrectTimeout: this.resurrectTimeout,
_openRequests: this._openRequests,
Expand Down Expand Up @@ -302,4 +307,21 @@ function resolve (host, path) {
}
}

function prepareHeaders (headers = {}, auth) {
if (auth != null && headers.authorization == null) {
if (auth.username && auth.password) {
headers.authorization = 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64')
}

if (auth.apiKey) {
if (typeof auth.apiKey === 'object') {
headers.authorization = 'ApiKey ' + Buffer.from(`${auth.apiKey.id}:${auth.apiKey.api_key}`).toString('base64')
} else {
headers.authorization = `ApiKey ${auth.apiKey}`
}
}
}
return headers
}

module.exports = Connection
Loading