Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Invalidate Token API (#35388) #36766

Merged
merged 4 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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