Skip to content

Commit

Permalink
Invalidate Token API enhancements - HLRC (elastic#36362)
Browse files Browse the repository at this point in the history
* Adds Invalidate Token API enhancements to HLRC

Relates: elastic#35388
  • Loading branch information
jkakavas committed Dec 18, 2018
1 parent adc961f commit a114e0b
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -62,6 +81,20 @@ public static InvalidateTokenRequest refreshToken(String refreshToken) {
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;
}
Expand All @@ -70,6 +103,14 @@ public String getRefreshToken() {
return refreshToken;
}

public String getRealmName() {
return realmName;
}

public String getUsername() {
return username;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand All @@ -79,24 +120,28 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1323,19 +1324,52 @@ public void testInvalidateToken() throws Exception {
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);
Expand All @@ -1347,15 +1381,54 @@ public void testInvalidateToken() throws Exception {
// 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
Expand Down Expand Up @@ -1385,8 +1458,10 @@ public void onFailure(Exception e) {

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));
}
}

Expand Down
Loading

0 comments on commit a114e0b

Please sign in to comment.