Skip to content

Commit

Permalink
Add kerberos grant_type to get token in exchange for Kerberos ticket (#…
Browse files Browse the repository at this point in the history
…42847)

Kibana wants to create access_token/refresh_token pair using Token
management APIs in exchange for kerberos tickets. `client_credentials`
grant_type requires every user to have `cluster:admin/xpack/security/token/create`
cluster privilege.

This commit introduces `_kerberos` grant_type for generating `access_token`
and `refresh_token` in exchange for a valid base64 encoded kerberos ticket.
In addition, `kibana_user` role now has cluster privilege to create tokens.
This allows Kibana to create access_token/refresh_token pair in exchange for
kerberos tickets.

Note:
The lifetime from the kerberos ticket is not used in ES and so even after it expires
the access_token/refresh_token pair will be valid. Care must be taken to invalidate
such tokens using token management APIs if required.

Closes #41943
  • Loading branch information
bizybot authored Jun 18, 2019
1 parent 3b022bb commit 4422c0e
Show file tree
Hide file tree
Showing 15 changed files with 490 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ public final class CreateTokenResponse {
private final TimeValue expiresIn;
private final String scope;
private final String refreshToken;
private final String kerberosAuthenticationResponseToken;

public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken) {
public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken,
String kerberosAuthenticationResponseToken) {
this.accessToken = accessToken;
this.type = type;
this.expiresIn = expiresIn;
this.scope = scope;
this.refreshToken = refreshToken;
this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
}

public String getAccessToken() {
Expand All @@ -70,6 +73,10 @@ public String getRefreshToken() {
return refreshToken;
}

public String getKerberosAuthenticationResponseToken() {
return kerberosAuthenticationResponseToken;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -83,24 +90,26 @@ public boolean equals(Object o) {
Objects.equals(type, that.type) &&
Objects.equals(expiresIn, that.expiresIn) &&
Objects.equals(scope, that.scope) &&
Objects.equals(refreshToken, that.refreshToken);
Objects.equals(refreshToken, that.refreshToken) &&
Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken);
}

@Override
public int hashCode() {
return Objects.hash(accessToken, type, expiresIn, scope, refreshToken);
return Objects.hash(accessToken, type, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
}

private static final ConstructingObjectParser<CreateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
"create_token_response", true, args -> new CreateTokenResponse(
(String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2]), (String) args[3], (String) args[4]));
"create_token_response", true, args -> new CreateTokenResponse((String) args[0], (String) args[1],
TimeValue.timeValueSeconds((Long) args[2]), (String) args[3], (String) args[4], (String) args[5]));

static {
PARSER.declareString(constructorArg(), new ParseField("access_token"));
PARSER.declareString(constructorArg(), new ParseField("type"));
PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("scope"));
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("refresh_token"));
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("kerberos_authentication_response_token"));
}

public static CreateTokenResponse fromXContent(XContentParser parser) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public void testFromXContent() throws IOException {
final String refreshToken = randomBoolean() ? null : randomAlphaOfLengthBetween(12, 24);
final String scope = randomBoolean() ? null : randomAlphaOfLength(4);
final String type = randomAlphaOfLength(6);
final String kerberosAuthenticationResponseToken = randomBoolean() ? null : randomAlphaOfLength(7);

final XContentType xContentType = randomFrom(XContentType.values());
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
Expand All @@ -50,6 +51,9 @@ public void testFromXContent() throws IOException {
if (scope != null || randomBoolean()) {
builder.field("scope", scope);
}
if (kerberosAuthenticationResponseToken != null) {
builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
}
builder.endObject();
BytesReference xContent = BytesReference.bytes(builder);

Expand All @@ -59,5 +63,6 @@ public void testFromXContent() throws IOException {
assertThat(response.getScope(), equalTo(scope));
assertThat(response.getType(), equalTo(type));
assertThat(response.getExpiresIn(), equalTo(expiresIn));
assertThat(response.getKerberosAuthenticationResponseToken(), equalTo(kerberosAuthenticationResponseToken));
}
}
43 changes: 40 additions & 3 deletions x-pack/docs/en/rest-api/security/get-tokens.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,21 @@ The following parameters can be specified in the body of a POST request and
pertain to creating a token:

`grant_type`::
(string) The type of grant. Supported grant types are: `password`,
`client_credentials` and `refresh_token`.
(string) The type of grant. Supported grant types are: `password`, `_kerberos`,
`client_credentials` and `refresh_token`. The `_kerberos` grant type
is supported internally and implements SPNEGO based Kerberos support. The `_kerberos`
grant type may change from version to version.

`password`::
(string) The user's password. If you specify the `password` grant type, this
parameter is required. This parameter is not valid with any other supported
grant type.

`kerberos_ticket`::
(string) base64 encoded kerberos ticket. If you specify the `_kerberos` grant type,
this parameter is required. This parameter is not valid with any other supported
grant type.

`refresh_token`::
(string) If you specify the `refresh_token` grant type, this parameter is
required. It contains the string that was returned when you created the token
Expand Down Expand Up @@ -160,4 +167,34 @@ be used one time.
}
--------------------------------------------------
// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]

The following example obtains a access token and refresh token using the `kerberos` grant type,
which simply creates a token in exchange for the base64 encoded kerberos ticket:

[source,js]
--------------------------------------------------
POST /_security/oauth2/token
{
"grant_type" : "_kerberos",
"kerberos_ticket" : "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAgEFoQMCAQ6iBtaDcp4cdMODwOsIvmvdX//sye8NDJZ8Gstabor3MOGryBWyaJ1VxI4WBVZaSn1WnzE06Xy2"
}
--------------------------------------------------
// NOTCONSOLE

The API will return a new token and refresh token if kerberos authentication is successful.
Each refresh token may only be used one time. When the mutual authentication is requested in the Spnego GSS context,
a base64 encoded token will be returned by the server in the `kerberos_authentication_response_token`
for clients to consume and finalize the authentication.

[source,js]
--------------------------------------------------
{
"access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
"type" : "Bearer",
"expires_in" : 1200,
"refresh_token": "vLBPvmAB6KvwvJZr27cS"
"kerberos_authentication_response_token": "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAg"
}
--------------------------------------------------
// NOTCONSOLE
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.CharArrays;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.CharArrays;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -34,6 +35,7 @@ public final class CreateTokenRequest extends ActionRequest {

public enum GrantType {
PASSWORD("password"),
KERBEROS("_kerberos"),
REFRESH_TOKEN("refresh_token"),
AUTHORIZATION_CODE("authorization_code"),
CLIENT_CREDENTIALS("client_credentials");
Expand Down Expand Up @@ -61,21 +63,23 @@ public static GrantType fromString(String grantType) {
}

private static final Set<GrantType> SUPPORTED_GRANT_TYPES = Collections.unmodifiableSet(
EnumSet.of(GrantType.PASSWORD, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS));
EnumSet.of(GrantType.PASSWORD, GrantType.KERBEROS, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS));

private String grantType;
private String username;
private SecureString password;
private SecureString kerberosTicket;
private String scope;
private String refreshToken;

public CreateTokenRequest() {}

public CreateTokenRequest(String grantType, @Nullable String username, @Nullable SecureString password, @Nullable String scope,
@Nullable String refreshToken) {
public CreateTokenRequest(String grantType, @Nullable String username, @Nullable SecureString password,
@Nullable SecureString kerberosTicket, @Nullable String scope, @Nullable String refreshToken) {
this.grantType = grantType;
this.username = username;
this.password = password;
this.kerberosTicket = kerberosTicket;
this.scope = scope;
this.refreshToken = refreshToken;
}
Expand All @@ -87,43 +91,28 @@ public ActionRequestValidationException validate() {
if (type != null) {
switch (type) {
case PASSWORD:
if (Strings.isNullOrEmpty(username)) {
validationException = addValidationError("username is missing", validationException);
}
if (password == null || password.getChars() == null || password.getChars().length == 0) {
validationException = addValidationError("password is missing", validationException);
}
if (refreshToken != null) {
validationException =
addValidationError("refresh_token is not supported with the password grant_type", validationException);
}
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
validationException = validateRequiredField("username", username, validationException);
validationException = validateRequiredField("password", password, validationException);
break;
case KERBEROS:
validationException = validateUnsupportedField(type, "username", username, validationException);
validationException = validateUnsupportedField(type, "password", password, validationException);
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
validationException = validateRequiredField("kerberos_ticket", kerberosTicket, validationException);
break;
case REFRESH_TOKEN:
if (username != null) {
validationException =
addValidationError("username is not supported with the refresh_token grant_type", validationException);
}
if (password != null) {
validationException =
addValidationError("password is not supported with the refresh_token grant_type", validationException);
}
if (refreshToken == null) {
validationException = addValidationError("refresh_token is missing", validationException);
}
validationException = validateUnsupportedField(type, "username", username, validationException);
validationException = validateUnsupportedField(type, "password", password, validationException);
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
validationException = validateRequiredField("refresh_token", refreshToken, validationException);
break;
case CLIENT_CREDENTIALS:
if (username != null) {
validationException =
addValidationError("username is not supported with the client_credentials grant_type", validationException);
}
if (password != null) {
validationException =
addValidationError("password is not supported with the client_credentials grant_type", validationException);
}
if (refreshToken != null) {
validationException = addValidationError("refresh_token is not supported with the client_credentials grant_type",
validationException);
}
validationException = validateUnsupportedField(type, "username", username, validationException);
validationException = validateUnsupportedField(type, "password", password, validationException);
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
break;
default:
validationException = addValidationError("grant_type only supports the values: [" +
Expand All @@ -138,6 +127,32 @@ public ActionRequestValidationException validate() {
return validationException;
}

private static ActionRequestValidationException validateRequiredField(String field, String fieldValue,
ActionRequestValidationException validationException) {
if (Strings.isNullOrEmpty(fieldValue)) {
validationException = addValidationError(String.format(Locale.ROOT, "%s is missing", field), validationException);
}
return validationException;
}

private static ActionRequestValidationException validateRequiredField(String field, SecureString fieldValue,
ActionRequestValidationException validationException) {
if (fieldValue == null || fieldValue.getChars() == null || fieldValue.length() == 0) {
validationException = addValidationError(String.format(Locale.ROOT, "%s is missing", field), validationException);
}
return validationException;
}

private static ActionRequestValidationException validateUnsupportedField(GrantType grantType, String field, Object fieldValue,
ActionRequestValidationException validationException) {
if (fieldValue != null) {
validationException = addValidationError(
String.format(Locale.ROOT, "%s is not supported with the %s grant_type", field, grantType.getValue()),
validationException);
}
return validationException;
}

public void setGrantType(String grantType) {
this.grantType = grantType;
}
Expand All @@ -150,6 +165,10 @@ public void setPassword(@Nullable SecureString password) {
this.password = password;
}

public void setKerberosTicket(@Nullable SecureString kerberosTicket) {
this.kerberosTicket = kerberosTicket;
}

public void setScope(@Nullable String scope) {
this.scope = scope;
}
Expand All @@ -172,6 +191,11 @@ public SecureString getPassword() {
return password;
}

@Nullable
public SecureString getKerberosTicket() {
return kerberosTicket;
}

@Nullable
public String getScope() {
return scope;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
private TimeValue expiresIn;
private String scope;
private String refreshToken;
private String kerberosAuthenticationResponseToken;

CreateTokenResponse() {}

public CreateTokenResponse(String tokenString, TimeValue expiresIn, String scope, String refreshToken) {
public CreateTokenResponse(String tokenString, TimeValue expiresIn, String scope, String refreshToken,
String kerberosAuthenticationResponseToken) {
this.tokenString = Objects.requireNonNull(tokenString);
this.expiresIn = Objects.requireNonNull(expiresIn);
this.scope = scope;
this.refreshToken = refreshToken;
this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
}

public String getTokenString() {
Expand All @@ -52,13 +55,18 @@ public String getRefreshToken() {
return refreshToken;
}

public String getKerberosAuthenticationResponseToken() {
return kerberosAuthenticationResponseToken;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(tokenString);
out.writeTimeValue(expiresIn);
out.writeOptionalString(scope);
out.writeOptionalString(refreshToken);
out.writeOptionalString(kerberosAuthenticationResponseToken);
}

@Override
Expand All @@ -68,6 +76,7 @@ public void readFrom(StreamInput in) throws IOException {
expiresIn = in.readTimeValue();
scope = in.readOptionalString();
refreshToken = in.readOptionalString();
kerberosAuthenticationResponseToken = in.readOptionalString();
}

@Override
Expand All @@ -83,6 +92,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (scope != null) {
builder.field("scope", scope);
}
if (kerberosAuthenticationResponseToken != null) {
builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
}
return builder.endObject();
}

Expand All @@ -94,11 +106,12 @@ public boolean equals(Object o) {
return Objects.equals(tokenString, that.tokenString) &&
Objects.equals(expiresIn, that.expiresIn) &&
Objects.equals(scope, that.scope) &&
Objects.equals(refreshToken, that.refreshToken);
Objects.equals(refreshToken, that.refreshToken) &&
Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken);
}

@Override
public int hashCode() {
return Objects.hash(tokenString, expiresIn, scope, refreshToken);
return Objects.hash(tokenString, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
}
}
Loading

0 comments on commit 4422c0e

Please sign in to comment.