Skip to content

Commit a114e0b

Browse files
committed
Invalidate Token API enhancements - HLRC (elastic#36362)
* Adds Invalidate Token API enhancements to HLRC Relates: elastic#35388
1 parent adc961f commit a114e0b

File tree

6 files changed

+355
-56
lines changed

6 files changed

+355
-56
lines changed

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

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,36 @@ public final class InvalidateTokenRequest implements Validatable, ToXContentObje
3535

3636
private final String accessToken;
3737
private final String refreshToken;
38+
private final String realmName;
39+
private final String username;
3840

3941
InvalidateTokenRequest(@Nullable String accessToken, @Nullable String refreshToken) {
40-
if (Strings.isNullOrEmpty(accessToken)) {
41-
if (Strings.isNullOrEmpty(refreshToken)) {
42-
throw new IllegalArgumentException("Either access-token or refresh-token is required");
42+
this(accessToken, refreshToken, null, null);
43+
}
44+
45+
public InvalidateTokenRequest(@Nullable String accessToken, @Nullable String refreshToken,
46+
@Nullable String realmName, @Nullable String username) {
47+
if (Strings.hasText(realmName) || Strings.hasText(username)) {
48+
if (Strings.hasText(accessToken)) {
49+
throw new IllegalArgumentException("access token is not allowed when realm name or username are specified");
50+
}
51+
if (refreshToken != null) {
52+
throw new IllegalArgumentException("refresh token is not allowed when realm name or username are specified");
53+
}
54+
} else {
55+
if (Strings.isNullOrEmpty(accessToken)) {
56+
if (Strings.isNullOrEmpty(refreshToken)) {
57+
throw new IllegalArgumentException("Either access token or refresh token is required when neither realm name or " +
58+
"username are specified");
59+
}
60+
} else if (Strings.isNullOrEmpty(refreshToken) == false) {
61+
throw new IllegalArgumentException("Cannot supply both access token and refresh token");
4362
}
44-
} else if (Strings.isNullOrEmpty(refreshToken) == false) {
45-
throw new IllegalArgumentException("Cannot supply both access-token and refresh-token");
4663
}
4764
this.accessToken = accessToken;
4865
this.refreshToken = refreshToken;
66+
this.realmName = realmName;
67+
this.username = username;
4968
}
5069

5170
public static InvalidateTokenRequest accessToken(String accessToken) {
@@ -62,6 +81,20 @@ public static InvalidateTokenRequest refreshToken(String refreshToken) {
6281
return new InvalidateTokenRequest(null, refreshToken);
6382
}
6483

84+
public static InvalidateTokenRequest realmTokens(String realmName) {
85+
if (Strings.isNullOrEmpty(realmName)) {
86+
throw new IllegalArgumentException("realm name is required");
87+
}
88+
return new InvalidateTokenRequest(null, null, realmName, null);
89+
}
90+
91+
public static InvalidateTokenRequest userTokens(String username) {
92+
if (Strings.isNullOrEmpty(username)) {
93+
throw new IllegalArgumentException("username is required");
94+
}
95+
return new InvalidateTokenRequest(null, null, null, username);
96+
}
97+
6598
public String getAccessToken() {
6699
return accessToken;
67100
}
@@ -70,6 +103,14 @@ public String getRefreshToken() {
70103
return refreshToken;
71104
}
72105

106+
public String getRealmName() {
107+
return realmName;
108+
}
109+
110+
public String getUsername() {
111+
return username;
112+
}
113+
73114
@Override
74115
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
75116
builder.startObject();
@@ -79,24 +120,28 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
79120
if (refreshToken != null) {
80121
builder.field("refresh_token", refreshToken);
81122
}
123+
if (realmName != null) {
124+
builder.field("realm_name", realmName);
125+
}
126+
if (username != null) {
127+
builder.field("username", username);
128+
}
82129
return builder.endObject();
83130
}
84131

85132
@Override
86133
public boolean equals(Object o) {
87-
if (this == o) {
88-
return true;
89-
}
90-
if (o == null || getClass() != o.getClass()) {
91-
return false;
92-
}
93-
final InvalidateTokenRequest that = (InvalidateTokenRequest) o;
94-
return Objects.equals(this.accessToken, that.accessToken) &&
95-
Objects.equals(this.refreshToken, that.refreshToken);
134+
if (this == o) return true;
135+
if (o == null || getClass() != o.getClass()) return false;
136+
InvalidateTokenRequest that = (InvalidateTokenRequest) o;
137+
return Objects.equals(accessToken, that.accessToken) &&
138+
Objects.equals(refreshToken, that.refreshToken) &&
139+
Objects.equals(realmName, that.realmName) &&
140+
Objects.equals(username, that.username);
96141
}
97142

98143
@Override
99144
public int hashCode() {
100-
return Objects.hash(accessToken, refreshToken);
145+
return Objects.hash(accessToken, refreshToken, realmName, username);
101146
}
102147
}

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

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,107 @@
1919

2020
package org.elasticsearch.client.security;
2121

22+
import org.elasticsearch.ElasticsearchException;
23+
import org.elasticsearch.common.Nullable;
2224
import org.elasticsearch.common.ParseField;
2325
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
2426
import org.elasticsearch.common.xcontent.XContentParser;
27+
import org.elasticsearch.common.xcontent.XContentParserUtils;
2528

2629
import java.io.IOException;
30+
import java.util.Collections;
31+
import java.util.List;
2732
import java.util.Objects;
2833

2934
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
35+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
3036

3137
/**
32-
* Response when invalidating an OAuth2 token. Returns a
33-
* single boolean field for whether the invalidation record was created or updated.
38+
* Response when invalidating one or multiple OAuth2 access tokens and refresh tokens. Returns
39+
* information concerning how many tokens were invalidated, how many of the tokens that
40+
* were attempted to be invalidated were already invalid, and if there were any errors
41+
* encountered.
3442
*/
3543
public final class InvalidateTokenResponse {
3644

45+
public static final ParseField CREATED = new ParseField("created");
46+
public static final ParseField INVALIDATED_TOKENS = new ParseField("invalidated_tokens");
47+
public static final ParseField PREVIOUSLY_INVALIDATED_TOKENS = new ParseField("previously_invalidated_tokens");
48+
public static final ParseField ERROR_COUNT = new ParseField("error_count");
49+
public static final ParseField ERRORS = new ParseField("error_details");
50+
3751
private final boolean created;
52+
private final int invalidatedTokens;
53+
private final int previouslyInvalidatedTokens;
54+
private List<ElasticsearchException> errors;
55+
56+
@SuppressWarnings("unchecked")
57+
private static final ConstructingObjectParser<InvalidateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
58+
"tokens_invalidation_result", true,
59+
// we parse but do not use the count of errors as we implicitly have this in the size of the Exceptions list
60+
args -> new InvalidateTokenResponse((boolean) args[0], (int) args[1], (int) args[2], (List<ElasticsearchException>) args[4]));
61+
62+
static {
63+
PARSER.declareBoolean(constructorArg(), CREATED);
64+
PARSER.declareInt(constructorArg(), INVALIDATED_TOKENS);
65+
PARSER.declareInt(constructorArg(), PREVIOUSLY_INVALIDATED_TOKENS);
66+
PARSER.declareInt(constructorArg(), ERROR_COUNT);
67+
PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), ERRORS);
68+
}
3869

39-
public InvalidateTokenResponse(boolean created) {
70+
public InvalidateTokenResponse(boolean created, int invalidatedTokens, int previouslyInvalidatedTokens,
71+
@Nullable List<ElasticsearchException> errors) {
4072
this.created = created;
73+
this.invalidatedTokens = invalidatedTokens;
74+
this.previouslyInvalidatedTokens = previouslyInvalidatedTokens;
75+
if (null == errors) {
76+
this.errors = Collections.emptyList();
77+
} else {
78+
this.errors = Collections.unmodifiableList(errors);
79+
}
4180
}
4281

4382
public boolean isCreated() {
4483
return created;
4584
}
4685

86+
public int getInvalidatedTokens() {
87+
return invalidatedTokens;
88+
}
89+
90+
public int getPreviouslyInvalidatedTokens() {
91+
return previouslyInvalidatedTokens;
92+
}
93+
94+
public List<ElasticsearchException> getErrors() {
95+
return errors;
96+
}
97+
98+
public int getErrorsCount() {
99+
return errors == null ? 0 : errors.size();
100+
}
101+
47102
@Override
48103
public boolean equals(Object o) {
49-
if (this == o) {
50-
return true;
51-
}
52-
if (o == null || getClass() != o.getClass()) {
53-
return false;
54-
}
104+
if (this == o) return true;
105+
if (o == null || getClass() != o.getClass()) return false;
55106
InvalidateTokenResponse that = (InvalidateTokenResponse) o;
56-
return created == that.created;
107+
return created == that.created &&
108+
invalidatedTokens == that.invalidatedTokens &&
109+
previouslyInvalidatedTokens == that.previouslyInvalidatedTokens &&
110+
Objects.equals(errors, that.errors);
57111
}
58112

59113
@Override
60114
public int hashCode() {
61-
return Objects.hash(created);
62-
}
63-
64-
private static final ConstructingObjectParser<InvalidateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
65-
"invalidate_token_response", true, args -> new InvalidateTokenResponse((boolean) args[0]));
66-
67-
static {
68-
PARSER.declareBoolean(constructorArg(), new ParseField("created"));
115+
return Objects.hash(created, invalidatedTokens, previouslyInvalidatedTokens, errors);
69116
}
70117

71118
public static InvalidateTokenResponse fromXContent(XContentParser parser) throws IOException {
119+
if (parser.currentToken() == null) {
120+
parser.nextToken();
121+
}
122+
XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation);
72123
return PARSER.parse(parser, null);
73124
}
74125
}

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

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.client.documentation;
2121

22+
import org.elasticsearch.ElasticsearchException;
2223
import org.elasticsearch.ElasticsearchStatusException;
2324
import org.elasticsearch.action.ActionListener;
2425
import org.elasticsearch.action.LatchedActionListener;
@@ -1323,19 +1324,52 @@ public void testInvalidateToken() throws Exception {
13231324
String accessToken;
13241325
String refreshToken;
13251326
{
1326-
// Setup user
1327+
// Setup users
13271328
final char[] password = "password".toCharArray();
1328-
User invalidate_token_user = new User("invalidate_token", Collections.singletonList("kibana_user"));
1329-
PutUserRequest putUserRequest = new PutUserRequest(invalidate_token_user, password, true, RefreshPolicy.IMMEDIATE);
1329+
User user = new User("user", Collections.singletonList("kibana_user"));
1330+
PutUserRequest putUserRequest = new PutUserRequest(user, password, true, RefreshPolicy.IMMEDIATE);
13301331
PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
13311332
assertTrue(putUserResponse.isCreated());
13321333

1334+
User this_user = new User("this_user", Collections.singletonList("kibana_user"));
1335+
PutUserRequest putThisUserRequest = new PutUserRequest(this_user, password, true, RefreshPolicy.IMMEDIATE);
1336+
PutUserResponse putThisUserResponse = client.security().putUser(putThisUserRequest, RequestOptions.DEFAULT);
1337+
assertTrue(putThisUserResponse.isCreated());
1338+
1339+
User that_user = new User("that_user", Collections.singletonList("kibana_user"));
1340+
PutUserRequest putThatUserRequest = new PutUserRequest(that_user, password, true, RefreshPolicy.IMMEDIATE);
1341+
PutUserResponse putThatUserResponse = client.security().putUser(putThatUserRequest, RequestOptions.DEFAULT);
1342+
assertTrue(putThatUserResponse.isCreated());
1343+
1344+
User other_user = new User("other_user", Collections.singletonList("kibana_user"));
1345+
PutUserRequest putOtherUserRequest = new PutUserRequest(other_user, password, true, RefreshPolicy.IMMEDIATE);
1346+
PutUserResponse putOtherUserResponse = client.security().putUser(putOtherUserRequest, RequestOptions.DEFAULT);
1347+
assertTrue(putOtherUserResponse.isCreated());
1348+
1349+
User extra_user = new User("extra_user", Collections.singletonList("kibana_user"));
1350+
PutUserRequest putExtraUserRequest = new PutUserRequest(extra_user, password, true, RefreshPolicy.IMMEDIATE);
1351+
PutUserResponse putExtraUserResponse = client.security().putUser(putExtraUserRequest, RequestOptions.DEFAULT);
1352+
assertTrue(putExtraUserResponse.isCreated());
1353+
13331354
// Create tokens
1334-
final CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("invalidate_token", password);
1355+
final CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("user", password);
13351356
final CreateTokenResponse tokenResponse = client.security().createToken(createTokenRequest, RequestOptions.DEFAULT);
13361357
accessToken = tokenResponse.getAccessToken();
13371358
refreshToken = tokenResponse.getRefreshToken();
1359+
final CreateTokenRequest createThisTokenRequest = CreateTokenRequest.passwordGrant("this_user", password);
1360+
final CreateTokenResponse thisTokenResponse = client.security().createToken(createThisTokenRequest, RequestOptions.DEFAULT);
1361+
assertNotNull(thisTokenResponse);
1362+
final CreateTokenRequest createThatTokenRequest = CreateTokenRequest.passwordGrant("that_user", password);
1363+
final CreateTokenResponse thatTokenResponse = client.security().createToken(createThatTokenRequest, RequestOptions.DEFAULT);
1364+
assertNotNull(thatTokenResponse);
1365+
final CreateTokenRequest createOtherTokenRequest = CreateTokenRequest.passwordGrant("other_user", password);
1366+
final CreateTokenResponse otherTokenResponse = client.security().createToken(createOtherTokenRequest, RequestOptions.DEFAULT);
1367+
assertNotNull(otherTokenResponse);
1368+
final CreateTokenRequest createExtraTokenRequest = CreateTokenRequest.passwordGrant("extra_user", password);
1369+
final CreateTokenResponse extraTokenResponse = client.security().createToken(createExtraTokenRequest, RequestOptions.DEFAULT);
1370+
assertNotNull(extraTokenResponse);
13381371
}
1372+
13391373
{
13401374
// tag::invalidate-access-token-request
13411375
InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.accessToken(accessToken);
@@ -1347,15 +1381,54 @@ public void testInvalidateToken() throws Exception {
13471381
// end::invalidate-token-execute
13481382

13491383
// tag::invalidate-token-response
1350-
boolean isCreated = invalidateTokenResponse.isCreated();
1384+
final List<ElasticsearchException> errors = invalidateTokenResponse.getErrors();
1385+
final int invalidatedTokens = invalidateTokenResponse.getInvalidatedTokens();
1386+
final int previouslyInvalidatedTokens = invalidateTokenResponse.getPreviouslyInvalidatedTokens();
13511387
// end::invalidate-token-response
1352-
assertTrue(isCreated);
1388+
assertTrue(errors.isEmpty());
1389+
assertThat(invalidatedTokens, equalTo(1));
1390+
assertThat(previouslyInvalidatedTokens, equalTo(0));
13531391
}
13541392

13551393
{
13561394
// tag::invalidate-refresh-token-request
13571395
InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.refreshToken(refreshToken);
13581396
// end::invalidate-refresh-token-request
1397+
InvalidateTokenResponse invalidateTokenResponse =
1398+
client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
1399+
assertTrue(invalidateTokenResponse.getErrors().isEmpty());
1400+
assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(1));
1401+
assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
1402+
}
1403+
1404+
{
1405+
// tag::invalidate-user-tokens-request
1406+
InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.userTokens("other_user");
1407+
// end::invalidate-user-tokens-request
1408+
InvalidateTokenResponse invalidateTokenResponse =
1409+
client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
1410+
assertTrue(invalidateTokenResponse.getErrors().isEmpty());
1411+
// We have one refresh and one access token for that user
1412+
assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(2));
1413+
assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
1414+
}
1415+
1416+
{
1417+
// tag::invalidate-user-realm-tokens-request
1418+
InvalidateTokenRequest invalidateTokenRequest = new InvalidateTokenRequest(null, null, "default_native", "extra_user");
1419+
// end::invalidate-user-realm-tokens-request
1420+
InvalidateTokenResponse invalidateTokenResponse =
1421+
client.security().invalidateToken(invalidateTokenRequest, RequestOptions.DEFAULT);
1422+
assertTrue(invalidateTokenResponse.getErrors().isEmpty());
1423+
// We have one refresh and one access token for that user in this realm
1424+
assertThat(invalidateTokenResponse.getInvalidatedTokens(), equalTo(2));
1425+
assertThat(invalidateTokenResponse.getPreviouslyInvalidatedTokens(), equalTo(0));
1426+
}
1427+
1428+
{
1429+
// tag::invalidate-realm-tokens-request
1430+
InvalidateTokenRequest invalidateTokenRequest = InvalidateTokenRequest.realmTokens("default_native");
1431+
// end::invalidate-realm-tokens-request
13591432

13601433
ActionListener<InvalidateTokenResponse> listener;
13611434
//tag::invalidate-token-execute-listener
@@ -1385,8 +1458,10 @@ public void onFailure(Exception e) {
13851458

13861459
final InvalidateTokenResponse response = future.get(30, TimeUnit.SECONDS);
13871460
assertNotNull(response);
1388-
assertTrue(response.isCreated());// technically, this should be false, but the API is broken
1389-
// See https://github.com/elastic/elasticsearch/issues/35115
1461+
assertTrue(response.getErrors().isEmpty());
1462+
//We still have 4 tokens ( 2 access_tokens and 2 refresh_tokens ) for the default_native realm
1463+
assertThat(response.getInvalidatedTokens(), equalTo(4));
1464+
assertThat(response.getPreviouslyInvalidatedTokens(), equalTo(0));
13901465
}
13911466
}
13921467

0 commit comments

Comments
 (0)