Skip to content

Commit

Permalink
Enhance Invalidate Token API (#36766)
Browse files Browse the repository at this point in the history
This change:

- Adds functionality to invalidate all (refresh+access) tokens for all users of a realm
- Adds functionality to invalidate all (refresh+access)tokens for a user in all realms
- Adds functionality to invalidate all (refresh+access) tokens for a user in a specific realm
- Changes the response format for the invalidate token API to contain information about the
   number of the invalidated tokens and possible errors that were encountered.
- Updates the API Documentation

Relates: #35338
  • Loading branch information
jkakavas authored Dec 18, 2018
1 parent 24ab17a commit 20883d1
Show file tree
Hide file tree
Showing 24 changed files with 1,422 additions and 302 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,7 @@ public void onFailure(Exception e) {
}
}

@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/36362")
public void testInvalidateToken() throws Exception {
RestHighLevelClient client = highLevelClient();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ The returned +{response}+ contains a single property:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-response]
--------------------------------------------------
--------------------------------------------------
90 changes: 80 additions & 10 deletions x-pack/docs/en/rest-api/security/invalidate-tokens.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[[security-api-invalidate-token]]
=== Invalidate token API

Invalidates an access token or a refresh token.
Invalidates one or more access tokens or refresh tokens.

==== Request

Expand All @@ -19,21 +19,31 @@ can no longer be used. That time period is defined by the
The refresh tokens returned by the <<security-api-get-token,get token API>> are
only valid for 24 hours. They can also be used exactly once.

If you want to invalidate an access or refresh token immediately, use this invalidate token API.
If you want to invalidate one or more access or refresh tokens immediately, use this invalidate token API.


==== Request Body

The following parameters can be specified in the body of a DELETE request and
pertain to invalidating a token:
pertain to invalidating tokens:

`token` (optional)::
(string) An access token. This parameter cannot be used when `refresh_token` is used.
(string) An access token. This parameter cannot be used any of `refresh_token`, `realm_name` or
`username` are used.

`refresh_token` (optional)::
(string) A refresh token. This parameter cannot be used when `token` is used.
(string) A refresh token. This parameter cannot be used any of `refresh_token`, `realm_name` or
`username` are used.

NOTE: One of `token` or `refresh_token` parameters is required.
`realm_name` (optional)::
(string) The name of an authentication realm. This parameter cannot be used with either `refresh_token` or `token`.

`username` (optional)::
(string) The username of a user. This parameter cannot be used with either `refresh_token` or `token`

NOTE: While all parameters are optional, at least one of them is required. More specifically, either one of `token`
or `refresh_token` parameters is required. If none of these two are specified, then `realm_name` and/or `username`
need to be specified.

==== Examples

Expand All @@ -59,15 +69,75 @@ DELETE /_xpack/security/oauth2/token
--------------------------------------------------
// NOTCONSOLE

A successful call returns a JSON structure that indicates whether the token
has already been invalidated.
The following example invalidates all access tokens and refresh tokens for the `saml1` realm immediately:

[source,js]
--------------------------------------------------
DELETE /_xpack/security/oauth2/token
{
"realm_name" : "saml1"
}
--------------------------------------------------
// NOTCONSOLE

The following example invalidates all access tokens and refresh tokens for the user `myuser` in all realms immediately:

[source,js]
--------------------------------------------------
DELETE /_xpack/security/oauth2/token
{
"username" : "myuser"
}
--------------------------------------------------
// NOTCONSOLE

Finally, the following example invalidates all access tokens and refresh tokens for the user `myuser` in
the `saml1` realm immediately:

[source,js]
--------------------------------------------------
DELETE /_xpack/security/oauth2/token
{
"username" : "myuser",
"realm_name" : "saml1"
}
--------------------------------------------------
// NOTCONSOLE

A successful call returns a JSON structure that contains the number of tokens that were invalidated, the number
of tokens that had already been invalidated, and potentially a list of errors encountered while invalidating
specific tokens.

[source,js]
--------------------------------------------------
{
"created" : true <1>
"invalidated_tokens":9, <1>
"previously_invalidated_tokens":15, <2>
"error_count":2, <3>
"error_details":[ <4>
{
"type":"exception",
"reason":"Elasticsearch exception [type=exception, reason=foo]",
"caused_by":{
"type":"exception",
"reason":"Elasticsearch exception [type=illegal_argument_exception, reason=bar]"
}
},
{
"type":"exception",
"reason":"Elasticsearch exception [type=exception, reason=boo]",
"caused_by":{
"type":"exception",
"reason":"Elasticsearch exception [type=illegal_argument_exception, reason=far]"
}
}
]
}
--------------------------------------------------
// NOTCONSOLE

<1> When a token has already been invalidated, `created` is set to false.
<1> The number of the tokens that were invalidated as part of this request.
<2> The number of tokens that were already invalidated.
<3> The number of errors that were encountered when invalidating the tokens.
<4> Details about these errors. This field is not present in the response when
`error_count` is 0.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.elasticsearch.client.ElasticsearchClient;

/**
* Action for invalidating a given token
* Action for invalidating one or more tokens
*/
public final class InvalidateTokenAction extends Action<InvalidateTokenRequest, InvalidateTokenResponse, InvalidateTokenRequestBuilder> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
Expand All @@ -22,31 +23,81 @@
public final class InvalidateTokenRequest extends ActionRequest {

public enum Type {
ACCESS_TOKEN,
REFRESH_TOKEN
ACCESS_TOKEN("token"),
REFRESH_TOKEN("refresh_token");

private final String value;

Type(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static Type fromString(String tokenType) {
if (tokenType != null) {
for (Type type : values()) {
if (type.getValue().equals(tokenType)) {
return type;
}
}
}
return null;
}
}

private String tokenString;
private Type tokenType;
private String realmName;
private String userName;

public InvalidateTokenRequest() {}

/**
* @param tokenString the string representation of the token
* @param tokenString the string representation of the token to be invalidated
* @param tokenType the type of the token to be invalidated
* @param realmName the name of the realm for which all tokens will be invalidated
* @param userName the principal of the user for which all tokens will be invalidated
*/
public InvalidateTokenRequest(String tokenString, Type type) {
public InvalidateTokenRequest(@Nullable String tokenString, @Nullable String tokenType,
@Nullable String realmName, @Nullable String userName) {
this.tokenString = tokenString;
this.tokenType = type;
this.tokenType = Type.fromString(tokenType);
this.realmName = realmName;
this.userName = userName;
}

/**
* @param tokenString the string representation of the token to be invalidated
* @param tokenType the type of the token to be invalidated
*/
public InvalidateTokenRequest(String tokenString, String tokenType) {
this.tokenString = tokenString;
this.tokenType = Type.fromString(tokenType);
this.realmName = null;
this.userName = null;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(tokenString)) {
validationException = addValidationError("token string must be provided", null);
}
if (tokenType == null) {
validationException = addValidationError("token type must be provided", validationException);
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
if (Strings.hasText(tokenString)) {
validationException =
addValidationError("token string must not be provided when realm name or username is specified", null);
}
if (tokenType != null) {
validationException =
addValidationError("token type must not be provided when realm name or username is specified", validationException);
}
} else if (Strings.isNullOrEmpty(tokenString)) {
validationException =
addValidationError("token string must be provided when not specifying a realm name or a username", null);
} else if (tokenType == null) {
validationException =
addValidationError("token type must be provided when a token string is specified", null);
}
return validationException;
}
Expand All @@ -67,26 +118,76 @@ void setTokenType(Type tokenType) {
this.tokenType = tokenType;
}

public String getRealmName() {
return realmName;
}

public void setRealmName(String realmName) {
this.realmName = realmName;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(tokenString);
if (out.getVersion().before(Version.V_6_6_0)) {
if (Strings.isNullOrEmpty(tokenString)) {
throw new IllegalArgumentException("token is required for versions < v6.6.0");
}
out.writeString(tokenString);
} else {
out.writeOptionalString(tokenString);
}
if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
out.writeVInt(tokenType.ordinal());
if (out.getVersion().before(Version.V_6_6_0)) {
if (tokenType == null) {
throw new IllegalArgumentException("token type is not optional for versions > v6.2.0 and < v6.6.0");
}
out.writeVInt(tokenType.ordinal());
} else {
out.writeOptionalVInt(tokenType == null ? null : tokenType.ordinal());
}
} else if (tokenType == Type.REFRESH_TOKEN) {
throw new IllegalArgumentException("refresh token invalidation cannot be serialized with version [" + out.getVersion() +
"]");
throw new IllegalArgumentException("refresh token invalidation cannot be serialized with version [" + out.getVersion() + "]");
}
if (out.getVersion().onOrAfter(Version.V_6_6_0)) {
out.writeOptionalString(realmName);
out.writeOptionalString(userName);
} else if (realmName != null || userName != null) {
throw new IllegalArgumentException(
"realm or user token invalidation cannot be serialized with version [" + out.getVersion() + "]");
}
}

@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
tokenString = in.readString();
if (in.getVersion().before(Version.V_6_6_0)) {
tokenString = in.readString();
} else {
tokenString = in.readOptionalString();
}
if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
tokenType = Type.values()[in.readVInt()];
if (in.getVersion().before(Version.V_6_6_0)) {
int type = in.readVInt();
tokenType = Type.values()[type];
} else {
Integer type = in.readOptionalVInt();
tokenType = type == null ? null : Type.values()[type];
}
} else {
tokenType = Type.ACCESS_TOKEN;
}
if (in.getVersion().onOrAfter(Version.V_6_6_0)) {
realmName = in.readOptionalString();
userName = in.readOptionalString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,20 @@ public InvalidateTokenRequestBuilder setType(InvalidateTokenRequest.Type type) {
request.setTokenType(type);
return this;
}

/**
* Sets the name of the realm for which all tokens should be invalidated
*/
public InvalidateTokenRequestBuilder setRealmName(String realmName) {
request.setRealmName(realmName);
return this;
}

/**
* Sets the username for which all tokens should be invalidated
*/
public InvalidateTokenRequestBuilder setUserName(String username) {
request.setUserName(username);
return this;
}
}
Loading

0 comments on commit 20883d1

Please sign in to comment.