Browse Source

HLRC: Add InvalidateToken security API (#35114)

This change adds the Invalidate Token API
(DELETE /_xpack/security/oauth2/token) to the Elasticsearch
High Level Rest Client.

Relates: #29827
Tim Vernum 7 years ago
parent
commit
3776de5f20

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

@@ -38,6 +38,8 @@ import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRoleMappingsResponse;
 import org.elasticsearch.client.security.GetSslCertificatesRequest;
 import org.elasticsearch.client.security.GetSslCertificatesResponse;
+import org.elasticsearch.client.security.InvalidateTokenRequest;
+import org.elasticsearch.client.security.InvalidateTokenResponse;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleMappingResponse;
 import org.elasticsearch.client.security.PutUserRequest;
@@ -408,4 +410,35 @@ public final class SecurityClient {
         restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::createToken, options,
             CreateTokenResponse::fromXContent, listener, emptySet());
     }
+
+    /**
+     * Invalidates an OAuth2 token.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-token.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to invalidate the token
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response from the create token call
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public InvalidateTokenResponse invalidateToken(InvalidateTokenRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::invalidateToken, options,
+            InvalidateTokenResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously invalidates an OAuth2 token.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-token.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to invalidate the token
+     * @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 invalidateTokenAsync(InvalidateTokenRequest request, RequestOptions options,
+                                     ActionListener<InvalidateTokenResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateToken, options,
+            InvalidateTokenResponse::fromXContent, listener, emptySet());
+    }
+
 }

+ 7 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -27,6 +27,7 @@ import org.elasticsearch.client.security.ClearRolesCacheRequest;
 import org.elasticsearch.client.security.CreateTokenRequest;
 import org.elasticsearch.client.security.DeleteRoleMappingRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
+import org.elasticsearch.client.security.InvalidateTokenRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.DisableUserRequest;
 import org.elasticsearch.client.security.EnableUserRequest;
@@ -147,4 +148,10 @@ final class SecurityRequestConverters {
         request.setEntity(createEntity(createTokenRequest, REQUEST_BODY_CONTENT_TYPE));
         return request;
     }
+
+    static Request invalidateToken(InvalidateTokenRequest invalidateTokenRequest) throws IOException {
+        Request request = new Request(HttpDelete.METHOD_NAME, "/_xpack/security/oauth2/token");
+        request.setEntity(createEntity(invalidateTokenRequest, REQUEST_BODY_CONTENT_TYPE));
+        return request;
+    }
 }

+ 102 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateTokenRequest.java

@@ -0,0 +1,102 @@
+/*
+ * 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.Validatable;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Request to invalidate a OAuth2 token within the Elasticsearch cluster.
+ */
+public final class InvalidateTokenRequest implements Validatable, ToXContentObject {
+
+    private final String accessToken;
+    private final String refreshToken;
+
+    InvalidateTokenRequest(@Nullable String accessToken, @Nullable String refreshToken) {
+        if (Strings.isNullOrEmpty(accessToken)) {
+            if (Strings.isNullOrEmpty(refreshToken)) {
+                throw new IllegalArgumentException("Either access-token or refresh-token is required");
+            }
+        } else if (Strings.isNullOrEmpty(refreshToken) == false) {
+            throw new IllegalArgumentException("Cannot supply both access-token and refresh-token");
+        }
+        this.accessToken = accessToken;
+        this.refreshToken = refreshToken;
+    }
+
+    public static InvalidateTokenRequest accessToken(String accessToken) {
+        if (Strings.isNullOrEmpty(accessToken)) {
+            throw new IllegalArgumentException("token is required");
+        }
+        return new InvalidateTokenRequest(accessToken, null);
+    }
+
+    public static InvalidateTokenRequest refreshToken(String refreshToken) {
+        if (Strings.isNullOrEmpty(refreshToken)) {
+            throw new IllegalArgumentException("refresh_token is required");
+        }
+        return new InvalidateTokenRequest(null, refreshToken);
+    }
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        if (accessToken != null) {
+            builder.field("token", accessToken);
+        }
+        if (refreshToken != null) {
+            builder.field("refresh_token", refreshToken);
+        }
+        return builder.endObject();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final InvalidateTokenRequest that = (InvalidateTokenRequest) o;
+        return Objects.equals(this.accessToken, that.accessToken) &&
+            Objects.equals(this.refreshToken, that.refreshToken);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(accessToken, refreshToken);
+    }
+}

+ 74 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateTokenResponse.java

@@ -0,0 +1,74 @@
+/*
+ * 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.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+
+/**
+ * Response when invalidating an OAuth2 token. Returns a
+ * single boolean field for whether the invalidation record was created or updated.
+ */
+public final class InvalidateTokenResponse {
+
+    private final boolean created;
+
+    public InvalidateTokenResponse(boolean created) {
+        this.created = created;
+    }
+
+    public boolean isCreated() {
+        return created;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        InvalidateTokenResponse that = (InvalidateTokenResponse) o;
+        return created == that.created;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(created);
+    }
+
+    private static final ConstructingObjectParser<InvalidateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "invalidate_token_response", true, args -> new InvalidateTokenResponse((boolean) args[0]));
+
+    static {
+        PARSER.declareBoolean(constructorArg(), new ParseField("created"));
+    }
+
+    public static InvalidateTokenResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+}

+ 91 - 17
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -46,6 +46,8 @@ import org.elasticsearch.client.security.ExpressionRoleMapping;
 import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRoleMappingsResponse;
 import org.elasticsearch.client.security.GetSslCertificatesResponse;
+import org.elasticsearch.client.security.InvalidateTokenRequest;
+import org.elasticsearch.client.security.InvalidateTokenResponse;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleMappingResponse;
 import org.elasticsearch.client.security.PutUserRequest;
@@ -134,11 +136,11 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         {
             // tag::put-role-mapping-execute
             final RoleMapperExpression rules = AnyRoleMapperExpression.builder()
-                    .addExpression(FieldRoleMapperExpression.ofUsername("*"))
-                    .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com"))
-                    .build();
+                .addExpression(FieldRoleMapperExpression.ofUsername("*"))
+                .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com"))
+                .build();
             final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"),
-                    rules, null, RefreshPolicy.NONE);
+                rules, null, RefreshPolicy.NONE);
             final PutRoleMappingResponse response = client.security().putRoleMapping(request, RequestOptions.DEFAULT);
             // end::put-role-mapping-execute
             // tag::put-role-mapping-response
@@ -149,11 +151,11 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
         {
             final RoleMapperExpression rules = AnyRoleMapperExpression.builder()
-                    .addExpression(FieldRoleMapperExpression.ofUsername("*"))
-                    .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com"))
-                    .build();
+                .addExpression(FieldRoleMapperExpression.ofUsername("*"))
+                .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com"))
+                .build();
             final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"),
-                    rules, null, RefreshPolicy.NONE);
+                rules, null, RefreshPolicy.NONE);
             // tag::put-role-mapping-execute-listener
             ActionListener<PutRoleMappingResponse> listener = new ActionListener<PutRoleMappingResponse>() {
                 @Override
@@ -184,21 +186,21 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         final RestHighLevelClient client = highLevelClient();
 
         final RoleMapperExpression rules1 = AnyRoleMapperExpression.builder().addExpression(FieldRoleMapperExpression.ofUsername("*"))
-                .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com")).build();
+            .addExpression(FieldRoleMapperExpression.ofGroups("cn=admins,dc=example,dc=com")).build();
         final PutRoleMappingRequest putRoleMappingRequest1 = new PutRoleMappingRequest("mapping-example-1", true, Collections.singletonList(
-                "superuser"), rules1, null, RefreshPolicy.NONE);
+            "superuser"), rules1, null, RefreshPolicy.NONE);
         final PutRoleMappingResponse putRoleMappingResponse1 = client.security().putRoleMapping(putRoleMappingRequest1,
-                RequestOptions.DEFAULT);
+            RequestOptions.DEFAULT);
         boolean isCreated1 = putRoleMappingResponse1.isCreated();
         assertTrue(isCreated1);
         final RoleMapperExpression rules2 = AnyRoleMapperExpression.builder().addExpression(FieldRoleMapperExpression.ofGroups(
-                "cn=admins,dc=example,dc=com")).build();
+            "cn=admins,dc=example,dc=com")).build();
         final Map<String, Object> metadata2 = new HashMap<>();
         metadata2.put("k1", "v1");
         final PutRoleMappingRequest putRoleMappingRequest2 = new PutRoleMappingRequest("mapping-example-2", true, Collections.singletonList(
-                "monitoring"), rules2, metadata2, RefreshPolicy.NONE);
+            "monitoring"), rules2, metadata2, RefreshPolicy.NONE);
         final PutRoleMappingResponse putRoleMappingResponse2 = client.security().putRoleMapping(putRoleMappingRequest2,
-                RequestOptions.DEFAULT);
+            RequestOptions.DEFAULT);
         boolean isCreated2 = putRoleMappingResponse2.isCreated();
         assertTrue(isCreated2);
 
@@ -229,7 +231,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             assertThat(mappings.size(), is(2));
             for (ExpressionRoleMapping roleMapping : mappings) {
                 assertThat(roleMapping.isEnabled(), is(true));
-                assertThat(roleMapping.getName(), isIn(new String[] { "mapping-example-1", "mapping-example-2" }));
+                assertThat(roleMapping.getName(), isIn(new String[]{"mapping-example-1", "mapping-example-2"}));
                 if (roleMapping.getName().equals("mapping-example-1")) {
                     assertThat(roleMapping.getMetadata(), equalTo(Collections.emptyMap()));
                     assertThat(roleMapping.getExpression(), equalTo(rules1));
@@ -252,7 +254,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             assertThat(mappings.size(), is(2));
             for (ExpressionRoleMapping roleMapping : mappings) {
                 assertThat(roleMapping.isEnabled(), is(true));
-                assertThat(roleMapping.getName(), isIn(new String[] { "mapping-example-1", "mapping-example-2" }));
+                assertThat(roleMapping.getName(), isIn(new String[]{"mapping-example-1", "mapping-example-2"}));
                 if (roleMapping.getName().equals("mapping-example-1")) {
                     assertThat(roleMapping.getMetadata(), equalTo(Collections.emptyMap()));
                     assertThat(roleMapping.getExpression(), equalTo(rules1));
@@ -605,7 +607,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // Create role mappings
             final RoleMapperExpression rules = FieldRoleMapperExpression.ofUsername("*");
             final PutRoleMappingRequest request = new PutRoleMappingRequest("mapping-example", true, Collections.singletonList("superuser"),
-                    rules, null, RefreshPolicy.NONE);
+                rules, null, RefreshPolicy.NONE);
             final PutRoleMappingResponse response = client.security().putRoleMapping(request, RequestOptions.DEFAULT);
             boolean isCreated = response.isCreated();
             assertTrue(isCreated);
@@ -792,6 +794,78 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // "client-credentials" grants aren't refreshable
             assertNull(future.get().getRefreshToken());
         }
+    }
+
+    public void testInvalidateToken() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        String accessToken;
+        String refreshToken;
+        {
+            // Setup user
+            final char[] password = "password".toCharArray();
+            PutUserRequest putUserRequest = new PutUserRequest("invalidate_token", password,
+                Collections.singletonList("kibana_user"), null, null, true, null, RefreshPolicy.IMMEDIATE);
+            PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
+            assertTrue(putUserResponse.isCreated());
 
+            // Create tokens
+            final CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("invalidate_token", password);
+            final CreateTokenResponse tokenResponse = client.security().createToken(createTokenRequest, RequestOptions.DEFAULT);
+            accessToken = tokenResponse.getAccessToken();
+            refreshToken = tokenResponse.getRefreshToken();
+        }
+        {
+            // tag::invalidate-access-token-request
+            InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.accessToken(accessToken);
+            // end::invalidate-access-token-request
+
+            // tag::invalidate-token-execute
+            InvalidateTokenResponse invalidateTokenResponse =
+                client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
+            // end::invalidate-token-execute
+
+            // tag::invalidate-token-response
+            boolean isCreated = invalidateTokenResponse.isCreated();
+            // end::invalidate-token-response
+            assertTrue(isCreated);
+        }
+
+        {
+            // tag::invalidate-refresh-token-request
+            InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.refreshToken(refreshToken);
+            // end::invalidate-refresh-token-request
+
+            ActionListener<InvalidateTokenResponse> listener;
+            //tag::invalidate-token-execute-listener
+            listener = new ActionListener<InvalidateTokenResponse>() {
+                @Override
+                public void onResponse(InvalidateTokenResponse invalidateTokenResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            //end::invalidate-token-execute-listener
+
+            // Avoid unused variable warning
+            assertNotNull(listener);
+
+            // Replace the empty listener by a blocking listener in test
+            final PlainActionFuture<InvalidateTokenResponse> future = new PlainActionFuture<>();
+            listener = future;
+
+            //tag::invalidate-token-execute-async
+            client.security().invalidateTokenAsync(invalidateTokenRequest, RequestOptions.DEFAULT, listener); // <1>
+            //end::invalidate-token-execute-async
+
+            final InvalidateTokenResponse response = future.get(30, TimeUnit.SECONDS);
+            assertNotNull(response);
+            assertTrue(response.isCreated());// technically, this should be false, but the API is broken
+            // See https://github.com/elastic/elasticsearch/issues/35115
+        }
     }
 }

+ 68 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateTokenRequestTests.java

@@ -0,0 +1,68 @@
+/*
+ * 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.common.Strings;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+
+public class InvalidateTokenRequestTests extends ESTestCase {
+
+    public void testInvalidateAccessToken() {
+        String token = "Tf01rrAymdUjxMY4VlG3gV3gsFFUWxVVPrztX+4uhe0=";
+        final InvalidateTokenRequest request = InvalidateTokenRequest.accessToken(token);
+        assertThat(request.getAccessToken(), equalTo(token));
+        assertThat(request.getRefreshToken(), nullValue());
+        assertThat(Strings.toString(request), equalTo("{" +
+            "\"token\":\"Tf01rrAymdUjxMY4VlG3gV3gsFFUWxVVPrztX+4uhe0=\"" +
+            "}"
+        ));
+    }
+
+    public void testInvalidateRefreshToken() {
+        String token = "4rE0YPT/oHODS83TbTtYmuh8";
+        final InvalidateTokenRequest request = InvalidateTokenRequest.refreshToken(token);
+        assertThat(request.getAccessToken(), nullValue());
+        assertThat(request.getRefreshToken(), equalTo(token));
+        assertThat(Strings.toString(request), equalTo("{" +
+            "\"refresh_token\":\"4rE0YPT/oHODS83TbTtYmuh8\"" +
+            "}"
+        ));
+    }
+
+    public void testEqualsAndHashCode() {
+        final String token = randomAlphaOfLength(8);
+        final boolean accessToken = randomBoolean();
+        final InvalidateTokenRequest request = accessToken ? InvalidateTokenRequest.accessToken(token)
+            : InvalidateTokenRequest.refreshToken(token);
+        final EqualsHashCodeTestUtils.MutateFunction<InvalidateTokenRequest> mutate = r -> {
+            if (randomBoolean()) {
+                return accessToken ? InvalidateTokenRequest.refreshToken(token) : InvalidateTokenRequest.accessToken(token);
+            } else {
+                return accessToken ? InvalidateTokenRequest.accessToken(randomAlphaOfLength(10))
+                    : InvalidateTokenRequest.refreshToken(randomAlphaOfLength(10));
+            }
+        };
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(request,
+            r -> new InvalidateTokenRequest(r.getAccessToken(), r.getRefreshToken()), mutate);
+    }
+}

+ 50 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateTokenResponseTests.java

@@ -0,0 +1,50 @@
+/*
+ * 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.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+
+public class InvalidateTokenResponseTests extends ESTestCase {
+
+    public void testFromXContent() throws IOException {
+        final boolean created = randomBoolean();
+
+        final XContentType xContentType = randomFrom(XContentType.values());
+        final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
+        builder.startObject()
+            .field("created", created)
+            .endObject();
+        BytesReference xContent = BytesReference.bytes(builder);
+
+        try (XContentParser parser = createParser(xContentType.xContent(), xContent)) {
+            final InvalidateTokenResponse response = InvalidateTokenResponse.fromXContent(parser);
+            assertThat(response.isCreated(), Matchers.equalTo(created));
+        }
+
+    }
+
+}

+ 39 - 0
docs/java-rest/high-level/security/invalidate-token.asciidoc

@@ -0,0 +1,39 @@
+--
+:api: invalidate-token
+:request: InvalidateTokenRequest
+:response: InvalidateTokenResponse
+--
+
+[id="{upid}-{api}"]
+=== Invalidate Token API
+
+[id="{upid}-{api}-request"]
+==== Invalidate Token Request
+The +{request}+ supports invalidating either an _access token_ or a _refresh token_
+
+===== Access Token
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[invalidate-access-token-request]
+--------------------------------------------------
+
+===== Refresh Token
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[invalidate-refresh-token-request]
+--------------------------------------------------
+
+include::../execution.asciidoc[]
+
+[id="{upid}-{api}-response"]
+==== Invalidate Token Response
+
+The returned +{response}+ contains a single property:
+
+`created`:: Whether the invalidation record was newly created (`true`),
+   or if the token had already been invalidated (`false`).
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -337,6 +337,7 @@ The Java High Level REST Client supports the following Security APIs:
 * <<java-rest-high-security-get-role-mappings>>
 * <<java-rest-high-security-delete-role-mapping>>
 * <<java-rest-high-security-create-token>>
+* <<{upid}-invalidate-token>>
 
 include::security/put-user.asciidoc[]
 include::security/enable-user.asciidoc[]
@@ -350,6 +351,7 @@ include::security/put-role-mapping.asciidoc[]
 include::security/get-role-mappings.asciidoc[]
 include::security/delete-role-mapping.asciidoc[]
 include::security/create-token.asciidoc[]
+include::security/invalidate-token.asciidoc[]
 
 == Watcher APIs