Browse Source

Invalidate Token API enhancements - HLRC (#36362)

* Adds Invalidate Token API enhancements to HLRC

Relates: #35388
Ioannis Kakavas 6 years ago
parent
commit
78f9af19c6

+ 60 - 15
client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateTokenRequest.java

@@ -35,17 +35,36 @@ public final class InvalidateTokenRequest implements Validatable, ToXContentObje
 
     private final String accessToken;
     private final String refreshToken;
+    private final String realmName;
+    private final String username;
 
     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");
+        this(accessToken, refreshToken, null, null);
+    }
+
+    public InvalidateTokenRequest(@Nullable String accessToken, @Nullable String refreshToken,
+                                  @Nullable String realmName, @Nullable String username) {
+        if (Strings.hasText(realmName) || Strings.hasText(username)) {
+            if (Strings.hasText(accessToken)) {
+                throw new IllegalArgumentException("access token is not allowed when realm name or username are specified");
+            }
+            if (refreshToken != null) {
+                throw new IllegalArgumentException("refresh token is not allowed when realm name or username are specified");
+            }
+        } else {
+            if (Strings.isNullOrEmpty(accessToken)) {
+                if (Strings.isNullOrEmpty(refreshToken)) {
+                    throw new IllegalArgumentException("Either access token or refresh token is required when neither realm name or " +
+                        "username are specified");
+                }
+            } else if (Strings.isNullOrEmpty(refreshToken) == false) {
+                throw new IllegalArgumentException("Cannot supply both access token and refresh token");
             }
-        } else if (Strings.isNullOrEmpty(refreshToken) == false) {
-            throw new IllegalArgumentException("Cannot supply both access-token and refresh-token");
         }
         this.accessToken = accessToken;
         this.refreshToken = refreshToken;
+        this.realmName = realmName;
+        this.username = username;
     }
 
     public static InvalidateTokenRequest accessToken(String accessToken) {
@@ -62,6 +81,20 @@ public final class InvalidateTokenRequest implements Validatable, ToXContentObje
         return new InvalidateTokenRequest(null, refreshToken);
     }
 
+    public static InvalidateTokenRequest realmTokens(String realmName) {
+        if (Strings.isNullOrEmpty(realmName)) {
+            throw new IllegalArgumentException("realm name is required");
+        }
+        return new InvalidateTokenRequest(null, null, realmName, null);
+    }
+
+    public static InvalidateTokenRequest userTokens(String username) {
+        if (Strings.isNullOrEmpty(username)) {
+            throw new IllegalArgumentException("username is required");
+        }
+        return new InvalidateTokenRequest(null, null, null, username);
+    }
+
     public String getAccessToken() {
         return accessToken;
     }
@@ -70,6 +103,14 @@ public final class InvalidateTokenRequest implements Validatable, ToXContentObje
         return refreshToken;
     }
 
+    public String getRealmName() {
+        return realmName;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
@@ -79,24 +120,28 @@ public final class InvalidateTokenRequest implements Validatable, ToXContentObje
         if (refreshToken != null) {
             builder.field("refresh_token", refreshToken);
         }
+        if (realmName != null) {
+            builder.field("realm_name", realmName);
+        }
+        if (username != null) {
+            builder.field("username", username);
+        }
         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);
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        InvalidateTokenRequest that = (InvalidateTokenRequest) o;
+        return Objects.equals(accessToken, that.accessToken) &&
+            Objects.equals(refreshToken, that.refreshToken) &&
+            Objects.equals(realmName, that.realmName) &&
+            Objects.equals(username, that.username);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(accessToken, refreshToken);
+        return Objects.hash(accessToken, refreshToken, realmName, username);
     }
 }

+ 69 - 18
client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateTokenResponse.java

@@ -19,56 +19,107 @@
 
 package org.elasticsearch.client.security;
 
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentParserUtils;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
 /**
- * Response when invalidating an OAuth2 token. Returns a
- * single boolean field for whether the invalidation record was created or updated.
+ * Response when invalidating one or multiple OAuth2 access tokens and refresh tokens. Returns
+ * information concerning how many tokens were invalidated, how many of the tokens that
+ * were attempted to be invalidated were already invalid, and if there were any errors
+ * encountered.
  */
 public final class InvalidateTokenResponse {
 
+    public static final ParseField CREATED = new ParseField("created");
+    public static final ParseField INVALIDATED_TOKENS = new ParseField("invalidated_tokens");
+    public static final ParseField PREVIOUSLY_INVALIDATED_TOKENS = new ParseField("previously_invalidated_tokens");
+    public static final ParseField ERROR_COUNT = new ParseField("error_count");
+    public static final ParseField ERRORS = new ParseField("error_details");
+
     private final boolean created;
+    private final int invalidatedTokens;
+    private final int previouslyInvalidatedTokens;
+    private List<ElasticsearchException> errors;
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<InvalidateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "tokens_invalidation_result", true,
+        // we parse but do not use the count of errors as we implicitly have this in the size of the Exceptions list
+        args -> new InvalidateTokenResponse((boolean) args[0], (int) args[1], (int) args[2], (List<ElasticsearchException>) args[4]));
+
+    static {
+        PARSER.declareBoolean(constructorArg(), CREATED);
+        PARSER.declareInt(constructorArg(), INVALIDATED_TOKENS);
+        PARSER.declareInt(constructorArg(), PREVIOUSLY_INVALIDATED_TOKENS);
+        PARSER.declareInt(constructorArg(), ERROR_COUNT);
+        PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), ERRORS);
+    }
 
-    public InvalidateTokenResponse(boolean created) {
+    public InvalidateTokenResponse(boolean created, int invalidatedTokens, int previouslyInvalidatedTokens,
+                                   @Nullable List<ElasticsearchException> errors) {
         this.created = created;
+        this.invalidatedTokens = invalidatedTokens;
+        this.previouslyInvalidatedTokens = previouslyInvalidatedTokens;
+        if (null == errors) {
+            this.errors = Collections.emptyList();
+        } else {
+            this.errors = Collections.unmodifiableList(errors);
+        }
     }
 
     public boolean isCreated() {
         return created;
     }
 
+    public int getInvalidatedTokens() {
+        return invalidatedTokens;
+    }
+
+    public int getPreviouslyInvalidatedTokens() {
+        return previouslyInvalidatedTokens;
+    }
+
+    public List<ElasticsearchException> getErrors() {
+        return errors;
+    }
+
+    public int getErrorsCount() {
+        return errors == null ? 0 : errors.size();
+    }
+
     @Override
     public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
         InvalidateTokenResponse that = (InvalidateTokenResponse) o;
-        return created == that.created;
+        return created == that.created &&
+            invalidatedTokens == that.invalidatedTokens &&
+            previouslyInvalidatedTokens == that.previouslyInvalidatedTokens &&
+            Objects.equals(errors, that.errors);
     }
 
     @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"));
+        return Objects.hash(created, invalidatedTokens, previouslyInvalidatedTokens, errors);
     }
 
     public static InvalidateTokenResponse fromXContent(XContentParser parser) throws IOException {
+        if (parser.currentToken() == null) {
+            parser.nextToken();
+        }
+        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation);
         return PARSER.parse(parser, null);
     }
 }

+ 83 - 8
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.client.documentation;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.LatchedActionListener;
@@ -1324,19 +1325,52 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         String accessToken;
         String refreshToken;
         {
-            // Setup user
+            // Setup users
             final char[] password = "password".toCharArray();
-            User invalidate_token_user = new User("invalidate_token", Collections.singletonList("kibana_user"));
-            PutUserRequest putUserRequest = new PutUserRequest(invalidate_token_user, password, true, RefreshPolicy.IMMEDIATE);
+            User user = new User("user", Collections.singletonList("kibana_user"));
+            PutUserRequest putUserRequest = new PutUserRequest(user, password, true, RefreshPolicy.IMMEDIATE);
             PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
             assertTrue(putUserResponse.isCreated());
 
+            User this_user = new User("this_user", Collections.singletonList("kibana_user"));
+            PutUserRequest putThisUserRequest = new PutUserRequest(this_user, password, true, RefreshPolicy.IMMEDIATE);
+            PutUserResponse putThisUserResponse = client.security().putUser(putThisUserRequest, RequestOptions.DEFAULT);
+            assertTrue(putThisUserResponse.isCreated());
+
+            User that_user = new User("that_user", Collections.singletonList("kibana_user"));
+            PutUserRequest putThatUserRequest = new PutUserRequest(that_user, password, true, RefreshPolicy.IMMEDIATE);
+            PutUserResponse putThatUserResponse = client.security().putUser(putThatUserRequest, RequestOptions.DEFAULT);
+            assertTrue(putThatUserResponse.isCreated());
+
+            User other_user = new User("other_user", Collections.singletonList("kibana_user"));
+            PutUserRequest putOtherUserRequest = new PutUserRequest(other_user, password, true, RefreshPolicy.IMMEDIATE);
+            PutUserResponse putOtherUserResponse = client.security().putUser(putOtherUserRequest, RequestOptions.DEFAULT);
+            assertTrue(putOtherUserResponse.isCreated());
+
+            User extra_user = new User("extra_user", Collections.singletonList("kibana_user"));
+            PutUserRequest putExtraUserRequest = new PutUserRequest(extra_user, password, true, RefreshPolicy.IMMEDIATE);
+            PutUserResponse putExtraUserResponse = client.security().putUser(putExtraUserRequest, RequestOptions.DEFAULT);
+            assertTrue(putExtraUserResponse.isCreated());
+
             // Create tokens
-            final CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("invalidate_token", password);
+            final CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("user", password);
             final CreateTokenResponse tokenResponse = client.security().createToken(createTokenRequest, RequestOptions.DEFAULT);
             accessToken = tokenResponse.getAccessToken();
             refreshToken = tokenResponse.getRefreshToken();
+            final CreateTokenRequest createThisTokenRequest = CreateTokenRequest.passwordGrant("this_user", password);
+            final CreateTokenResponse thisTokenResponse = client.security().createToken(createThisTokenRequest, RequestOptions.DEFAULT);
+            assertNotNull(thisTokenResponse);
+            final CreateTokenRequest createThatTokenRequest = CreateTokenRequest.passwordGrant("that_user", password);
+            final CreateTokenResponse thatTokenResponse = client.security().createToken(createThatTokenRequest, RequestOptions.DEFAULT);
+            assertNotNull(thatTokenResponse);
+            final CreateTokenRequest createOtherTokenRequest = CreateTokenRequest.passwordGrant("other_user", password);
+            final CreateTokenResponse otherTokenResponse = client.security().createToken(createOtherTokenRequest, RequestOptions.DEFAULT);
+            assertNotNull(otherTokenResponse);
+            final CreateTokenRequest createExtraTokenRequest = CreateTokenRequest.passwordGrant("extra_user", password);
+            final CreateTokenResponse extraTokenResponse = client.security().createToken(createExtraTokenRequest, RequestOptions.DEFAULT);
+            assertNotNull(extraTokenResponse);
         }
+
         {
             // tag::invalidate-access-token-request
             InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.accessToken(accessToken);
@@ -1348,15 +1382,54 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // end::invalidate-token-execute
 
             // tag::invalidate-token-response
-            boolean isCreated = invalidateTokenResponse.isCreated();
+            final List<ElasticsearchException> errors = invalidateTokenResponse.getErrors();
+            final int invalidatedTokens = invalidateTokenResponse.getInvalidatedTokens();
+            final int previouslyInvalidatedTokens = invalidateTokenResponse.getPreviouslyInvalidatedTokens();
             // end::invalidate-token-response
-            assertTrue(isCreated);
+            assertTrue(errors.isEmpty());
+            assertThat(invalidatedTokens, equalTo(1));
+            assertThat(previouslyInvalidatedTokens, equalTo(0));
         }
 
         {
             // tag::invalidate-refresh-token-request
             InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.refreshToken(refreshToken);
             // end::invalidate-refresh-token-request
+            InvalidateTokenResponse invalidateTokenResponse =
+                client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
+            assertTrue(invalidateTokenResponse.getErrors().isEmpty());
+            assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(1));
+            assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
+        }
+
+        {
+            // tag::invalidate-user-tokens-request
+            InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.userTokens("other_user");
+            // end::invalidate-user-tokens-request
+            InvalidateTokenResponse invalidateTokenResponse =
+                client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
+            assertTrue(invalidateTokenResponse.getErrors().isEmpty());
+            // We have one refresh and one access token for that user
+            assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(2));
+            assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
+        }
+
+        {
+            // tag::invalidate-user-realm-tokens-request
+            InvalidateTokenRequest invalidateTokenRequest = new InvalidateTokenRequest(null, null, "default_native", "extra_user");
+            // end::invalidate-user-realm-tokens-request
+            InvalidateTokenResponse invalidateTokenResponse =
+                client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
+            assertTrue(invalidateTokenResponse.getErrors().isEmpty());
+            // We have one refresh and one access token for that user in this realm
+            assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(2));
+            assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
+        }
+
+        {
+            // tag::invalidate-realm-tokens-request
+            InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.realmTokens("default_native");
+            // end::invalidate-realm-tokens-request
 
             ActionListener<InvalidateTokenResponse> listener;
             //tag::invalidate-token-execute-listener
@@ -1386,8 +1459,10 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
             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
+            assertTrue(response.getErrors().isEmpty());
+            //We still have 4 tokens ( 2 access_tokens and 2 refresh_tokens ) for the default_native realm
+            assertThat(response.getInvalidatedTokens(), equalTo(4));
+            assertThat(response.getPreviouslyInvalidatedTokens(), equalTo(0));
         }
     }
 

+ 54 - 5
client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateTokenRequestTests.java

@@ -49,17 +49,66 @@ public class InvalidateTokenRequestTests extends ESTestCase {
         ));
     }
 
+    public void testInvalidateRealmTokens() {
+        String realmName = "native";
+        final InvalidateTokenRequest request = InvalidateTokenRequest.realmTokens(realmName);
+        assertThat(request.getAccessToken(), nullValue());
+        assertThat(request.getRefreshToken(), nullValue());
+        assertThat(request.getRealmName(), equalTo(realmName));
+        assertThat(request.getUsername(), nullValue());
+        assertThat(Strings.toString(request), equalTo("{" +
+            "\"realm_name\":\"native\"" +
+            "}"
+        ));
+    }
+
+    public void testInvalidateUserTokens() {
+        String username = "user";
+        final InvalidateTokenRequest request = InvalidateTokenRequest.userTokens(username);
+        assertThat(request.getAccessToken(), nullValue());
+        assertThat(request.getRefreshToken(), nullValue());
+        assertThat(request.getRealmName(), nullValue());
+        assertThat(request.getUsername(), equalTo(username));
+        assertThat(Strings.toString(request), equalTo("{" +
+            "\"username\":\"user\"" +
+            "}"
+        ));
+    }
+
+    public void testInvalidateUserTokensInRealm() {
+        String username = "user";
+        String realmName = "native";
+        final InvalidateTokenRequest request = new InvalidateTokenRequest(null, null, realmName, username);
+        assertThat(request.getAccessToken(), nullValue());
+        assertThat(request.getRefreshToken(), nullValue());
+        assertThat(request.getRealmName(), equalTo(realmName));
+        assertThat(request.getUsername(), equalTo(username));
+        assertThat(Strings.toString(request), equalTo("{" +
+            "\"realm_name\":\"native\"," +
+            "\"username\":\"user\"" +
+
+            "}"
+        ));
+    }
+
     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));
+            int randomCase = randomIntBetween(1, 4);
+            switch (randomCase) {
+                case 1:
+                    return InvalidateTokenRequest.refreshToken(randomAlphaOfLength(5));
+                case 2:
+                    return InvalidateTokenRequest.accessToken(randomAlphaOfLength(5));
+                case 3:
+                    return InvalidateTokenRequest.realmTokens(randomAlphaOfLength(5));
+                case 4:
+                    return InvalidateTokenRequest.userTokens(randomAlphaOfLength(5));
+                default:
+                    return new InvalidateTokenRequest(null, null, randomAlphaOfLength(5), randomAlphaOfLength(5));
             }
         };
         EqualsHashCodeTestUtils.checkEqualsAndHashCode(request,

+ 49 - 4
client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateTokenResponseTests.java

@@ -18,7 +18,9 @@
  */
 package org.elasticsearch.client.security;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentParser;
@@ -28,23 +30,66 @@ import org.hamcrest.Matchers;
 
 import java.io.IOException;
 
+import static org.hamcrest.Matchers.containsString;
+
 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);
+        final int invalidatedTokens = randomInt(32);
+        final int previouslyInvalidatedTokens = randomInt(32);
         builder.startObject()
-            .field("created", created)
+            .field("created", false)
+            .field("invalidated_tokens", invalidatedTokens)
+            .field("previously_invalidated_tokens", previouslyInvalidatedTokens)
+            .field("error_count", 0)
             .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));
+            assertThat(response.isCreated(), Matchers.equalTo(false));
+            assertThat(response.getInvalidatedTokens(), Matchers.equalTo(invalidatedTokens));
+            assertThat(response.getPreviouslyInvalidatedTokens(), Matchers.equalTo(previouslyInvalidatedTokens));
+            assertThat(response.getErrorsCount(), Matchers.equalTo(0));
         }
-
     }
 
+    public void testFromXContentWithErrors() throws IOException {
+
+        final XContentType xContentType = randomFrom(XContentType.values());
+        final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
+        final int invalidatedTokens = randomInt(32);
+        final int previouslyInvalidatedTokens = randomInt(32);
+        builder.startObject()
+            .field("created", false)
+            .field("invalidated_tokens", invalidatedTokens)
+            .field("previously_invalidated_tokens", previouslyInvalidatedTokens)
+            .field("error_count", 0)
+            .startArray("error_details")
+            .startObject();
+        ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, new ElasticsearchException("foo",
+            new IllegalArgumentException("bar")));
+        builder.endObject().startObject();
+        ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, new ElasticsearchException("boo",
+            new IllegalArgumentException("far")));
+        builder.endObject()
+            .endArray()
+            .endObject();
+        BytesReference xContent = BytesReference.bytes(builder);
+
+        try (XContentParser parser = createParser(xContentType.xContent(), xContent)) {
+            final InvalidateTokenResponse response = InvalidateTokenResponse.fromXContent(parser);
+            assertThat(response.isCreated(), Matchers.equalTo(false));
+            assertThat(response.getInvalidatedTokens(), Matchers.equalTo(invalidatedTokens));
+            assertThat(response.getPreviouslyInvalidatedTokens(), Matchers.equalTo(previouslyInvalidatedTokens));
+            assertThat(response.getErrorsCount(), Matchers.equalTo(2));
+            assertThat(response.getErrors().get(0).toString(), containsString("type=exception, reason=foo"));
+            assertThat(response.getErrors().get(0).toString(), containsString("type=illegal_argument_exception, reason=bar"));
+            assertThat(response.getErrors().get(1).toString(), containsString("type=exception, reason=boo"));
+            assertThat(response.getErrors().get(1).toString(), containsString("type=illegal_argument_exception, reason=far"));
+        }
+    }
 }

+ 40 - 6
docs/java-rest/high-level/security/invalidate-token.asciidoc

@@ -9,29 +9,63 @@
 
 [id="{upid}-{api}-request"]
 ==== Invalidate Token Request
-The +{request}+ supports invalidating either an _access token_ or a _refresh token_
+The +{request}+ supports invalidating
 
-===== Access Token
+. A specific token, that can be either an _access token_ or a _refresh token_
+
+. All tokens (both _access tokens_ and _refresh tokens_) for a specific realm
+
+. All tokens (both _access tokens_ and _refresh tokens_) for a specific user
+
+. All tokens (both _access tokens_ and _refresh tokens_) for a specific user in a specific realm
+
+===== Specific access token
 ["source","java",subs="attributes,callouts,macros"]
 --------------------------------------------------
 include-tagged::{doc-tests-file}[invalidate-access-token-request]
 --------------------------------------------------
 
-===== Refresh Token
+===== Specific refresh token
 ["source","java",subs="attributes,callouts,macros"]
 --------------------------------------------------
 include-tagged::{doc-tests-file}[invalidate-refresh-token-request]
 --------------------------------------------------
 
+===== All tokens for realm
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[invalidate-realm-tokens-request]
+--------------------------------------------------
+
+===== All tokens for user
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[invalidate-user-tokens-request]
+--------------------------------------------------
+
+===== All tokens for user in realm
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[invalidate-user-realm-tokens-request]
+--------------------------------------------------
+
 include::../execution.asciidoc[]
 
 [id="{upid}-{api}-response"]
 ==== Invalidate Token Response
 
-The returned +{response}+ contains a single property:
+The returned +{response}+ contains the information regarding the tokens that the request
+invalidated.
+
+`invalidatedTokens`:: Available using `getInvalidatedTokens` denotes the number of tokens
+                      that this request invalidated.
+
+`previouslyInvalidatedTokens`:: Available using `getPreviouslyInvalidatedTokens` denotes
+                                the number of tokens that this request attempted to invalidate
+                                but were already invalid.
 
-`created`:: Whether the invalidation record was newly created (`true`),
-   or if the token had already been invalidated (`false`).
+`errors`:: Available using `getErrors` contains possible errors that were encountered while
+           attempting to invalidate specific tokens.
 
 ["source","java",subs="attributes,callouts,macros"]
 --------------------------------------------------