Browse Source

HLRest: add security authenticate API (#33552)

This adds the security `_authenticate` API to the HLREST client.
It is unlike some of the other APIs because the request does not
have a body.
The commit also creates the `User` entity. It is important
to note that the `User` entity does not have the `enabled`
flag. The `enabled` flag is part of the response, alongside
the `User` entity.
Moreover this adds the `SecurityIT` test class 
(extending `ESRestHighLevelClientTestCase`).

Relates #29827
Albert Zaharovits 7 years ago
parent
commit
bdd8460906

+ 28 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -20,6 +20,8 @@
 package org.elasticsearch.client;
 
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.security.AuthenticateRequest;
+import org.elasticsearch.client.security.AuthenticateResponse;
 import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.ClearRolesCacheRequest;
 import org.elasticsearch.client.security.ClearRolesCacheResponse;
@@ -210,6 +212,32 @@ public final class SecurityClient {
             EmptyResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Authenticate the current user and return all the information about the authenticated user.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html">
+     * the docs</a> for more.
+     *
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized 
+     * @return the responsee from the authenticate user call
+     */
+    public AuthenticateResponse authenticate(RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(AuthenticateRequest.INSTANCE, AuthenticateRequest::getRequest, options,
+                AuthenticateResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Authenticate the current user asynchronously and return all the information about the authenticated user.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html">
+     * the docs</a> for more.
+     *
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized 
+     * @param listener the listener to be notified upon request completion 
+     */
+    public void authenticateAsync(RequestOptions options, ActionListener<AuthenticateResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(AuthenticateRequest.INSTANCE, AuthenticateRequest::getRequest, options,
+                AuthenticateResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Clears the native roles cache for a set of roles.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-role-cache.html">

+ 41 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateRequest.java

@@ -0,0 +1,41 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.apache.http.client.methods.HttpGet;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Validatable;
+
+/**
+ * Empty request object required to make the authenticate call. The authenticate call
+ * retrieves metadata about the authenticated user.
+ */
+public final class AuthenticateRequest implements Validatable {
+
+    public static final AuthenticateRequest INSTANCE = new AuthenticateRequest();
+
+    private AuthenticateRequest() {
+    }
+
+    public Request getRequest() {
+        return new Request(HttpGet.METHOD_NAME, "/_xpack/security/_authenticate");
+    }
+
+}

+ 109 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java

@@ -0,0 +1,109 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.security.user.User;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * The response for the authenticate call. The response contains two fields: a
+ * user field and a boolean flag signaling if the user is enabled or not. The
+ * user object contains all user metadata which Elasticsearch uses to map roles,
+ * etc.
+ */
+public final class AuthenticateResponse {
+
+    static final ParseField USERNAME = new ParseField("username");
+    static final ParseField ROLES = new ParseField("roles");
+    static final ParseField METADATA = new ParseField("metadata");
+    static final ParseField FULL_NAME = new ParseField("full_name");
+    static final ParseField EMAIL = new ParseField("email");
+    static final ParseField ENABLED = new ParseField("enabled");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<AuthenticateResponse, Void> PARSER = new ConstructingObjectParser<>(
+            "client_security_authenticate_response",
+            a -> new AuthenticateResponse(new User((String) a[0], ((List<String>) a[1]), (Map<String, Object>) a[2],
+                    (String) a[3], (String) a[4]), (Boolean) a[5]));
+    static {
+        PARSER.declareString(constructorArg(), USERNAME);
+        PARSER.declareStringArray(constructorArg(), ROLES);
+        PARSER.<Map<String, Object>>declareObject(constructorArg(), (parser, c) -> parser.map(), METADATA);
+        PARSER.declareStringOrNull(optionalConstructorArg(), FULL_NAME);
+        PARSER.declareStringOrNull(optionalConstructorArg(), EMAIL);
+        PARSER.declareBoolean(constructorArg(), ENABLED);
+    }
+
+    private final User user;
+    private final boolean enabled;
+
+    public AuthenticateResponse(User user, boolean enabled) {
+        this.user = user;
+        this.enabled = enabled;
+    }
+
+    /**
+     * @return The effective user. This is the authenticated user, or, when
+     *         submitting requests on behalf of other users, it is the
+     *         impersonated user.
+     */
+    public User getUser() {
+        return user;
+    }
+
+    /**
+     * @return whether the user is enabled or not
+     */
+    public boolean enabled() {
+        return enabled;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final AuthenticateResponse that = (AuthenticateResponse) o;
+        return user.equals(that.user) && enabled == that.enabled;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(user, enabled);
+    }
+
+    public static AuthenticateResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+}

+ 135 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/User.java

@@ -0,0 +1,135 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security.user;
+
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * An authenticated user
+ */
+public final class User {
+
+    private final String username;
+    private final Collection<String> roles;
+    private final Map<String, Object> metadata;
+    @Nullable private final String fullName;
+    @Nullable private final String email;
+
+    public User(String username, Collection<String> roles, Map<String, Object> metadata, @Nullable String fullName,
+            @Nullable String email) {
+        Objects.requireNonNull(username, "`username` cannot be null");
+        Objects.requireNonNull(roles, "`roles` cannot be null. Pass an empty collection instead.");
+        Objects.requireNonNull(roles, "`metadata` cannot be null. Pass an empty map instead.");
+        this.username = username;
+        this.roles = roles;
+        this.metadata = Collections.unmodifiableMap(metadata);
+        this.fullName = fullName;
+        this.email = email;
+    }
+
+    /**
+     * @return  The principal of this user - effectively serving as the
+     *          unique identity of the user. Can never be {@code null}.
+     */
+    public String username() {
+        return this.username;
+    }
+
+    /**
+     * @return  The roles this user is associated with. The roles are
+     *          identified by their unique names and each represents as
+     *          set of permissions. Can never be {@code null}.
+     */
+    public Collection<String> roles() {
+        return this.roles;
+    }
+
+    /**
+     * @return  The metadata that is associated with this user. Can never be {@code null}.
+     */
+    public Map<String, Object> metadata() {
+        return metadata;
+    }
+
+    /**
+     * @return  The full name of this user. May be {@code null}.
+     */
+    public @Nullable String fullName() {
+        return fullName;
+    }
+
+    /**
+     * @return  The email of this user. May be {@code null}.
+     */
+    public @Nullable String email() {
+        return email;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("User[username=").append(username);
+        sb.append(",roles=[").append(Strings.collectionToCommaDelimitedString(roles)).append("]");
+        sb.append(",metadata=").append(metadata);
+        sb.append(",fullName=").append(fullName);
+        sb.append(",email=").append(email);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o instanceof User == false) {
+            return false;
+        }
+
+        final User user = (User) o;
+
+        if (!username.equals(user.username)) {
+            return false;
+        }
+        if (!roles.equals(user.roles)) {
+            return false;
+        }
+        if (!metadata.equals(user.metadata)) {
+            return false;
+        }
+        if (fullName != null ? !fullName.equals(user.fullName) : user.fullName != null) {
+            return false;
+        }
+        return !(email != null ? !email.equals(user.email) : user.email != null);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(username, roles, metadata, fullName, email);
+    }
+
+}

+ 26 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java

@@ -84,16 +84,42 @@ public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase {
         }
     }
 
+    /**
+     * Executes the provided request using either the sync method or its async
+     * variant, both provided as functions. This variant is used when the call does
+     * not have a request object (only headers and the request path).
+     */
+    protected static <Resp> Resp execute(SyncMethodNoRequest<Resp> syncMethodNoRequest, AsyncMethodNoRequest<Resp> asyncMethodNoRequest,
+            RequestOptions requestOptions) throws IOException {
+        if (randomBoolean()) {
+            return syncMethodNoRequest.execute(requestOptions);
+        } else {
+            PlainActionFuture<Resp> future = PlainActionFuture.newFuture();
+            asyncMethodNoRequest.execute(requestOptions, future);
+            return future.actionGet();
+        }
+    }
+
     @FunctionalInterface
     protected interface SyncMethod<Request, Response> {
         Response execute(Request request, RequestOptions options) throws IOException;
     }
 
+    @FunctionalInterface
+    protected interface SyncMethodNoRequest<Response> {
+        Response execute(RequestOptions options) throws IOException;
+    }
+
     @FunctionalInterface
     protected interface AsyncMethod<Request, Response> {
         void execute(Request request, RequestOptions options, ActionListener<Response> listener);
     }
 
+    @FunctionalInterface
+    protected interface AsyncMethodNoRequest<Response> {
+        void execute(RequestOptions options, ActionListener<Response> listener);
+    }
+
     private static class HighLevelClient extends RestHighLevelClient {
         private HighLevelClient(RestClient restClient) {
             super(restClient, (client) -> {}, Collections.emptyList());

+ 3 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

@@ -719,7 +719,7 @@ public class RestHighLevelClientTests extends ESTestCase {
                         methods.containsKey(apiName.substring(0, apiName.length() - 6)));
                 assertThat("async method [" + method + "] should return void", method.getReturnType(), equalTo(Void.TYPE));
                 assertEquals("async method [" + method + "] should not throw any exceptions", 0, method.getExceptionTypes().length);
-                if (apiName.equals("security.get_ssl_certificates_async")) {
+                if (apiName.equals("security.authenticate_async") || apiName.equals("security.get_ssl_certificates_async")) {
                     assertEquals(2, method.getParameterTypes().length);
                     assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class));
                     assertThat(method.getParameterTypes()[1], equalTo(ActionListener.class));
@@ -744,7 +744,8 @@ public class RestHighLevelClientTests extends ESTestCase {
 
                 assertEquals("incorrect number of exceptions for method [" + method + "]", 1, method.getExceptionTypes().length);
                 //a few methods don't accept a request object as argument
-                if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates")) {
+                if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates")
+                    || apiName.equals("security.authenticate")) {
                     assertEquals("incorrect number of arguments for method [" + method + "]", 1, method.getParameterTypes().length);
                     assertThat("the parameter to method [" + method + "] is the wrong type",
                         method.getParameterTypes()[0], equalTo(RequestOptions.class));

+ 108 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java

@@ -0,0 +1,108 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.apache.http.client.methods.HttpDelete;
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.client.security.AuthenticateResponse;
+import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.PutUserResponse;
+import org.elasticsearch.client.security.RefreshPolicy;
+import org.elasticsearch.common.CharArrays;
+
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+
+public class SecurityIT extends ESRestHighLevelClientTestCase {
+
+    public void testAuthenticate() throws Exception {
+        final SecurityClient securityClient = highLevelClient().security();
+        // test fixture: put enabled user
+        final PutUserRequest putUserRequest = randomPutUserRequest(true);
+        final PutUserResponse putUserResponse = execute(putUserRequest, securityClient::putUser, securityClient::putUserAsync);
+        assertThat(putUserResponse.isCreated(), is(true));
+
+        // authenticate correctly
+        final String basicAuthHeader = basicAuthHeader(putUserRequest.getUsername(), putUserRequest.getPassword());
+        final AuthenticateResponse authenticateResponse = execute(securityClient::authenticate, securityClient::authenticateAsync,
+                authorizationRequestOptions(basicAuthHeader));
+
+        assertThat(authenticateResponse.getUser().username(), is(putUserRequest.getUsername()));
+        if (putUserRequest.getRoles().isEmpty()) {
+            assertThat(authenticateResponse.getUser().roles(), is(empty()));
+        } else {
+            assertThat(authenticateResponse.getUser().roles(), contains(putUserRequest.getRoles().toArray()));
+        }
+        assertThat(authenticateResponse.getUser().metadata(), is(putUserRequest.getMetadata()));
+        assertThat(authenticateResponse.getUser().fullName(), is(putUserRequest.getFullName()));
+        assertThat(authenticateResponse.getUser().email(), is(putUserRequest.getEmail()));
+        assertThat(authenticateResponse.enabled(), is(true));
+
+        // delete user
+        final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, "/_xpack/security/user/" + putUserRequest.getUsername());
+        highLevelClient().getLowLevelClient().performRequest(deleteUserRequest);
+
+        // authentication no longer works
+        ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> execute(securityClient::authenticate,
+                securityClient::authenticateAsync, authorizationRequestOptions(basicAuthHeader)));
+        assertThat(e.getMessage(), containsString("unable to authenticate user [" + putUserRequest.getUsername() + "]"));
+    }
+
+    private static PutUserRequest randomPutUserRequest(boolean enabled) {
+        final String username = randomAlphaOfLengthBetween(1, 4);
+        final char[] password = randomAlphaOfLengthBetween(6, 10).toCharArray();
+        final List<String> roles = Arrays.asList(generateRandomStringArray(3, 3, false, true));
+        final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 3));
+        final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 3));
+        final Map<String, Object> metadata;
+        metadata = new HashMap<>();
+        if (randomBoolean()) {
+            metadata.put("string", null);
+        } else {
+            metadata.put("string", randomAlphaOfLengthBetween(0, 4));
+        }
+        if (randomBoolean()) {
+            metadata.put("string_list", null);
+        } else {
+            metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true)));
+        }
+        return new PutUserRequest(username, password, roles, fullName, email, enabled, metadata, RefreshPolicy.IMMEDIATE);
+    }
+    
+    private static String basicAuthHeader(String username, char[] password) {
+        final String concat = new StringBuilder().append(username).append(':').append(password).toString();
+        final byte[] concatBytes = CharArrays.toUtf8Bytes(concat.toCharArray());
+        return "Basic " + Base64.getEncoder().encodeToString(concatBytes);
+    }
+    
+    private static RequestOptions authorizationRequestOptions(String authorizationHeader) {
+        final RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
+        builder.addHeader("Authorization", authorizationHeader);
+        return builder.build();
+    }
+}

+ 52 - 4
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -29,6 +29,7 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.security.AuthenticateResponse;
 import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.ClearRolesCacheRequest;
 import org.elasticsearch.client.security.ClearRolesCacheResponse;
@@ -50,10 +51,11 @@ import org.elasticsearch.client.security.PutRoleMappingResponse;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.support.CertificateInfo;
 import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression;
-import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
+import org.elasticsearch.client.security.user.User;
+import org.elasticsearch.client.security.support.CertificateInfo;
+import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.hamcrest.Matchers;
@@ -67,13 +69,14 @@ import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.not;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isIn;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 
 public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
@@ -379,6 +382,51 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testAuthenticate() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+        {
+            //tag::authenticate-execute
+            AuthenticateResponse response = client.security().authenticate(RequestOptions.DEFAULT);
+            //end::authenticate-execute
+
+            //tag::authenticate-response
+            User user = response.getUser(); // <1>
+            boolean enabled = response.enabled(); // <2>
+            //end::authenticate-response
+
+            assertThat(user.username(), is("test_user"));
+            assertThat(user.roles(), contains(new String[] {"superuser"}));
+            assertThat(user.fullName(), nullValue());
+            assertThat(user.email(), nullValue());
+            assertThat(user.metadata().isEmpty(), is(true));
+            assertThat(enabled, is(true));
+        }
+
+        {
+            // tag::authenticate-execute-listener
+            ActionListener<AuthenticateResponse> listener = new ActionListener<AuthenticateResponse>() {
+                @Override
+                public void onResponse(AuthenticateResponse response) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::authenticate-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+            // tag::authenticate-execute-async
+            client.security().authenticateAsync(RequestOptions.DEFAULT, listener); // <1>
+            // end::authenticate-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
 
     public void testClearRolesCache() throws Exception {
         RestHighLevelClient client = highLevelClient();

+ 128 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java

@@ -0,0 +1,128 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.security.user.User;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester;
+
+public class AuthenticateResponseTests extends ESTestCase {
+
+    public void testFromXContent() throws IOException {
+        xContentTester(
+                this::createParser,
+                this::createTestInstance,
+                this::toXContent,
+                AuthenticateResponse::fromXContent)
+                .supportsUnknownFields(false)
+                .test();
+    }
+
+    public void testEqualsAndHashCode() {
+        final AuthenticateResponse reponse = createTestInstance();
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(reponse, this::copy,
+            this::mutate);
+    }
+
+    protected AuthenticateResponse createTestInstance() {
+        final String username = randomAlphaOfLengthBetween(1, 4);
+        final List<String> roles = Arrays.asList(generateRandomStringArray(4, 4, false, true));
+        final Map<String, Object> metadata;
+        metadata = new HashMap<>();
+        if (randomBoolean()) {
+            metadata.put("string", null);
+        } else {
+            metadata.put("string", randomAlphaOfLengthBetween(0, 4));
+        }
+        if (randomBoolean()) {
+            metadata.put("string_list", null);
+        } else {
+            metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true)));
+        }
+        final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final boolean enabled = randomBoolean();
+        return new AuthenticateResponse(new User(username, roles, metadata, fullName, email), enabled);
+    }
+
+    private void toXContent(AuthenticateResponse response, XContentBuilder builder) throws IOException {
+        final User user = response.getUser();
+        final boolean enabled = response.enabled();
+        builder.startObject();
+        builder.field(AuthenticateResponse.USERNAME.getPreferredName(), user.username());
+        builder.field(AuthenticateResponse.ROLES.getPreferredName(), user.roles());
+        builder.field(AuthenticateResponse.METADATA.getPreferredName(), user.metadata());
+        if (user.fullName() != null) {
+            builder.field(AuthenticateResponse.FULL_NAME.getPreferredName(), user.fullName());
+        }
+        if (user.email() != null) {
+            builder.field(AuthenticateResponse.EMAIL.getPreferredName(), user.email());
+        }
+        builder.field(AuthenticateResponse.ENABLED.getPreferredName(), enabled);
+        builder.endObject();
+    }
+
+    private AuthenticateResponse copy(AuthenticateResponse response) {
+        final User originalUser = response.getUser();
+        final User copyUser = new User(originalUser.username(), originalUser.roles(), originalUser.metadata(), originalUser.fullName(),
+                originalUser.email());
+        return new AuthenticateResponse(copyUser, response.enabled());
+    }
+
+    private AuthenticateResponse mutate(AuthenticateResponse response) {
+        final User originalUser = response.getUser();
+        switch (randomIntBetween(1, 6)) {
+            case 1:
+            return new AuthenticateResponse(new User(originalUser.username() + "wrong", originalUser.roles(), originalUser.metadata(),
+                    originalUser.fullName(), originalUser.email()), response.enabled());
+            case 2:
+                final Collection<String> wrongRoles = new ArrayList<>(originalUser.roles());
+                wrongRoles.add(randomAlphaOfLengthBetween(1, 4));
+                return new AuthenticateResponse(new User(originalUser.username(), wrongRoles, originalUser.metadata(),
+                        originalUser.fullName(), originalUser.email()), response.enabled());
+            case 3:
+                final Map<String, Object> wrongMetadata = new HashMap<>(originalUser.metadata());
+                wrongMetadata.put("wrong_string", randomAlphaOfLengthBetween(0, 4));
+                return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), wrongMetadata,
+                        originalUser.fullName(), originalUser.email()), response.enabled());
+            case 4:
+                return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(),
+                        originalUser.fullName() + "wrong", originalUser.email()), response.enabled());
+            case 5:
+                return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(),
+                        originalUser.fullName(), originalUser.email() + "wrong"), response.enabled());
+            case 6:
+                return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(),
+                        originalUser.fullName(), originalUser.email()), !response.enabled());
+        }
+        throw new IllegalStateException("Bad random number");
+    }
+}

+ 66 - 0
docs/java-rest/high-level/security/authenticate.asciidoc

@@ -0,0 +1,66 @@
+
+--
+:api: authenticate
+:response: AuthenticateResponse
+--
+
+[id="{upid}-{api}"]
+=== Authenticate API
+
+[id="{upid}-{api}-sync"]
+==== Execution
+
+Authenticating and retrieving information about a user can be performed
+using the `security().authenticate()` method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-execute]
+--------------------------------------------------
+
+This method does not require a request object. The client waits for the
++{response}+ to be returned before continuing with code execution.
+
+[id="{upid}-{api}-response"]
+==== Response
+
+The returned +{response}+ contains two fields. Firstly, the `user` field
+, accessed with `getUser`, contains all the information about this
+authenticated user. The other field, `enabled`, tells if this user is actually
+usable or has been temporalily deactivated.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> `getUser` retrieves the `User` instance containing the information,
+see {javadoc-client}/security/user/User.html.
+<2> `enabled` tells if this user is usable or is deactivated.
+
+[id="{upid}-{api}-async"]
+==== Asynchronous Execution
+
+This request can also be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-execute-async]
+--------------------------------------------------
+<1> The `ActionListener` to use when the execution completes. This method does
+not require a request object.
+
+The asynchronous method does not block and returns immediately. Once the request
+has completed the `ActionListener` is called back using the `onResponse` method
+if the execution completed successfully or using the `onFailure` method if
+it failed.
+
+A typical listener for a +{response}+ looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-execute-listener]
+--------------------------------------------------
+<1> Called when the execution completed successfully. The response is
+provided as an argument.
+<2> Called in case of a failure. The exception is provided as an argument.
+

+ 3 - 1
docs/java-rest/high-level/supported-apis.asciidoc

@@ -327,6 +327,7 @@ The Java High Level REST Client supports the following Security APIs:
 * <<java-rest-high-security-change-password>>
 * <<java-rest-high-security-delete-role>>
 * <<{upid}-clear-roles-cache>>
+* <<{upid}-authenticate>>
 * <<java-rest-high-security-get-certificates>>
 * <<java-rest-high-security-put-role-mapping>>
 * <<java-rest-high-security-get-role-mappings>>
@@ -339,6 +340,7 @@ include::security/disable-user.asciidoc[]
 include::security/change-password.asciidoc[]
 include::security/delete-role.asciidoc[]
 include::security/clear-roles-cache.asciidoc[]
+include::security/authenticate.asciidoc[]
 include::security/get-certificates.asciidoc[]
 include::security/put-role-mapping.asciidoc[]
 include::security/get-role-mappings.asciidoc[]
@@ -386,4 +388,4 @@ don't leak into the rest of the documentation.
 :response!:
 :doc-tests-file!:
 :upid!:
---
+--