Skip to content

Commit 2f17340

Browse files
authored
Add kerberos grant_type to get token in exchange for Kerberos ticket (elastic#42847) (elastic#43355)
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 elastic#41943
1 parent 42cc27e commit 2f17340

File tree

15 files changed

+493
-78
lines changed

15 files changed

+493
-78
lines changed

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ public final class CreateTokenResponse {
4141
private final TimeValue expiresIn;
4242
private final String scope;
4343
private final String refreshToken;
44+
private final String kerberosAuthenticationResponseToken;
4445

45-
public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken) {
46+
public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken,
47+
String kerberosAuthenticationResponseToken) {
4648
this.accessToken = accessToken;
4749
this.type = type;
4850
this.expiresIn = expiresIn;
4951
this.scope = scope;
5052
this.refreshToken = refreshToken;
53+
this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
5154
}
5255

5356
public String getAccessToken() {
@@ -70,6 +73,10 @@ public String getRefreshToken() {
7073
return refreshToken;
7174
}
7275

76+
public String getKerberosAuthenticationResponseToken() {
77+
return kerberosAuthenticationResponseToken;
78+
}
79+
7380
@Override
7481
public boolean equals(Object o) {
7582
if (this == o) {
@@ -83,24 +90,26 @@ public boolean equals(Object o) {
8390
Objects.equals(type, that.type) &&
8491
Objects.equals(expiresIn, that.expiresIn) &&
8592
Objects.equals(scope, that.scope) &&
86-
Objects.equals(refreshToken, that.refreshToken);
93+
Objects.equals(refreshToken, that.refreshToken) &&
94+
Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken);
8795
}
8896

8997
@Override
9098
public int hashCode() {
91-
return Objects.hash(accessToken, type, expiresIn, scope, refreshToken);
99+
return Objects.hash(accessToken, type, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
92100
}
93101

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

98106
static {
99107
PARSER.declareString(constructorArg(), new ParseField("access_token"));
100108
PARSER.declareString(constructorArg(), new ParseField("type"));
101109
PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
102110
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("scope"));
103111
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("refresh_token"));
112+
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("kerberos_authentication_response_token"));
104113
}
105114

106115
public static CreateTokenResponse fromXContent(XContentParser parser) throws IOException {

client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateTokenResponseTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public void testFromXContent() throws IOException {
3737
final String refreshToken = randomBoolean() ? null : randomAlphaOfLengthBetween(12, 24);
3838
final String scope = randomBoolean() ? null : randomAlphaOfLength(4);
3939
final String type = randomAlphaOfLength(6);
40+
final String kerberosAuthenticationResponseToken = randomBoolean() ? null : randomAlphaOfLength(7);
4041

4142
final XContentType xContentType = randomFrom(XContentType.values());
4243
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
@@ -50,6 +51,9 @@ public void testFromXContent() throws IOException {
5051
if (scope != null || randomBoolean()) {
5152
builder.field("scope", scope);
5253
}
54+
if (kerberosAuthenticationResponseToken != null) {
55+
builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
56+
}
5357
builder.endObject();
5458
BytesReference xContent = BytesReference.bytes(builder);
5559

@@ -59,5 +63,6 @@ public void testFromXContent() throws IOException {
5963
assertThat(response.getScope(), equalTo(scope));
6064
assertThat(response.getType(), equalTo(type));
6165
assertThat(response.getExpiresIn(), equalTo(expiresIn));
66+
assertThat(response.getKerberosAuthenticationResponseToken(), equalTo(kerberosAuthenticationResponseToken));
6267
}
6368
}

x-pack/docs/en/rest-api/security/get-tokens.asciidoc

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,21 @@ The following parameters can be specified in the body of a POST request and
4141
pertain to creating a token:
4242

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

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

54+
`kerberos_ticket`::
55+
(string) base64 encoded kerberos ticket. If you specify the `_kerberos` grant type,
56+
this parameter is required. This parameter is not valid with any other supported
57+
grant type.
58+
5259
`refresh_token`::
5360
(string) If you specify the `refresh_token` grant type, this parameter is
5461
required. It contains the string that was returned when you created the token
@@ -160,4 +167,34 @@ be used one time.
160167
}
161168
--------------------------------------------------
162169
// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
163-
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
170+
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
171+
172+
The following example obtains a access token and refresh token using the `kerberos` grant type,
173+
which simply creates a token in exchange for the base64 encoded kerberos ticket:
174+
175+
[source,js]
176+
--------------------------------------------------
177+
POST /_security/oauth2/token
178+
{
179+
"grant_type" : "_kerberos",
180+
"kerberos_ticket" : "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAgEFoQMCAQ6iBtaDcp4cdMODwOsIvmvdX//sye8NDJZ8Gstabor3MOGryBWyaJ1VxI4WBVZaSn1WnzE06Xy2"
181+
}
182+
--------------------------------------------------
183+
// NOTCONSOLE
184+
185+
The API will return a new token and refresh token if kerberos authentication is successful.
186+
Each refresh token may only be used one time. When the mutual authentication is requested in the Spnego GSS context,
187+
a base64 encoded token will be returned by the server in the `kerberos_authentication_response_token`
188+
for clients to consume and finalize the authentication.
189+
190+
[source,js]
191+
--------------------------------------------------
192+
{
193+
"access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
194+
"type" : "Bearer",
195+
"expires_in" : 1200,
196+
"refresh_token": "vLBPvmAB6KvwvJZr27cS"
197+
"kerberos_authentication_response_token": "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAg"
198+
}
199+
--------------------------------------------------
200+
// NOTCONSOLE

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@
88
import org.elasticsearch.Version;
99
import org.elasticsearch.action.ActionRequest;
1010
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.common.CharArrays;
1112
import org.elasticsearch.common.Nullable;
1213
import org.elasticsearch.common.Strings;
1314
import org.elasticsearch.common.bytes.BytesArray;
1415
import org.elasticsearch.common.bytes.BytesReference;
1516
import org.elasticsearch.common.io.stream.StreamInput;
1617
import org.elasticsearch.common.io.stream.StreamOutput;
1718
import org.elasticsearch.common.settings.SecureString;
18-
import org.elasticsearch.common.CharArrays;
1919

2020
import java.io.IOException;
2121
import java.util.Arrays;
2222
import java.util.Collections;
2323
import java.util.EnumSet;
24+
import java.util.Locale;
2425
import java.util.Set;
2526
import java.util.stream.Collectors;
2627

@@ -35,6 +36,7 @@ public final class CreateTokenRequest extends ActionRequest {
3536

3637
public enum GrantType {
3738
PASSWORD("password"),
39+
KERBEROS("_kerberos"),
3840
REFRESH_TOKEN("refresh_token"),
3941
AUTHORIZATION_CODE("authorization_code"),
4042
CLIENT_CREDENTIALS("client_credentials");
@@ -62,21 +64,23 @@ public static GrantType fromString(String grantType) {
6264
}
6365

6466
private static final Set<GrantType> SUPPORTED_GRANT_TYPES = Collections.unmodifiableSet(
65-
EnumSet.of(GrantType.PASSWORD, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS));
67+
EnumSet.of(GrantType.PASSWORD, GrantType.KERBEROS, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS));
6668

6769
private String grantType;
6870
private String username;
6971
private SecureString password;
72+
private SecureString kerberosTicket;
7073
private String scope;
7174
private String refreshToken;
7275

7376
public CreateTokenRequest() {}
7477

75-
public CreateTokenRequest(String grantType, @Nullable String username, @Nullable SecureString password, @Nullable String scope,
76-
@Nullable String refreshToken) {
78+
public CreateTokenRequest(String grantType, @Nullable String username, @Nullable SecureString password,
79+
@Nullable SecureString kerberosTicket, @Nullable String scope, @Nullable String refreshToken) {
7780
this.grantType = grantType;
7881
this.username = username;
7982
this.password = password;
83+
this.kerberosTicket = kerberosTicket;
8084
this.scope = scope;
8185
this.refreshToken = refreshToken;
8286
}
@@ -88,43 +92,28 @@ public ActionRequestValidationException validate() {
8892
if (type != null) {
8993
switch (type) {
9094
case PASSWORD:
91-
if (Strings.isNullOrEmpty(username)) {
92-
validationException = addValidationError("username is missing", validationException);
93-
}
94-
if (password == null || password.getChars() == null || password.getChars().length == 0) {
95-
validationException = addValidationError("password is missing", validationException);
96-
}
97-
if (refreshToken != null) {
98-
validationException =
99-
addValidationError("refresh_token is not supported with the password grant_type", validationException);
100-
}
95+
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
96+
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
97+
validationException = validateRequiredField("username", username, validationException);
98+
validationException = validateRequiredField("password", password, validationException);
99+
break;
100+
case KERBEROS:
101+
validationException = validateUnsupportedField(type, "username", username, validationException);
102+
validationException = validateUnsupportedField(type, "password", password, validationException);
103+
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
104+
validationException = validateRequiredField("kerberos_ticket", kerberosTicket, validationException);
101105
break;
102106
case REFRESH_TOKEN:
103-
if (username != null) {
104-
validationException =
105-
addValidationError("username is not supported with the refresh_token grant_type", validationException);
106-
}
107-
if (password != null) {
108-
validationException =
109-
addValidationError("password is not supported with the refresh_token grant_type", validationException);
110-
}
111-
if (refreshToken == null) {
112-
validationException = addValidationError("refresh_token is missing", validationException);
113-
}
107+
validationException = validateUnsupportedField(type, "username", username, validationException);
108+
validationException = validateUnsupportedField(type, "password", password, validationException);
109+
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
110+
validationException = validateRequiredField("refresh_token", refreshToken, validationException);
114111
break;
115112
case CLIENT_CREDENTIALS:
116-
if (username != null) {
117-
validationException =
118-
addValidationError("username is not supported with the client_credentials grant_type", validationException);
119-
}
120-
if (password != null) {
121-
validationException =
122-
addValidationError("password is not supported with the client_credentials grant_type", validationException);
123-
}
124-
if (refreshToken != null) {
125-
validationException = addValidationError("refresh_token is not supported with the client_credentials grant_type",
126-
validationException);
127-
}
113+
validationException = validateUnsupportedField(type, "username", username, validationException);
114+
validationException = validateUnsupportedField(type, "password", password, validationException);
115+
validationException = validateUnsupportedField(type, "kerberos_ticket", kerberosTicket, validationException);
116+
validationException = validateUnsupportedField(type, "refresh_token", refreshToken, validationException);
128117
break;
129118
default:
130119
validationException = addValidationError("grant_type only supports the values: [" +
@@ -139,6 +128,32 @@ public ActionRequestValidationException validate() {
139128
return validationException;
140129
}
141130

131+
private static ActionRequestValidationException validateRequiredField(String field, String fieldValue,
132+
ActionRequestValidationException validationException) {
133+
if (Strings.isNullOrEmpty(fieldValue)) {
134+
validationException = addValidationError(String.format(Locale.ROOT, "%s is missing", field), validationException);
135+
}
136+
return validationException;
137+
}
138+
139+
private static ActionRequestValidationException validateRequiredField(String field, SecureString fieldValue,
140+
ActionRequestValidationException validationException) {
141+
if (fieldValue == null || fieldValue.getChars() == null || fieldValue.length() == 0) {
142+
validationException = addValidationError(String.format(Locale.ROOT, "%s is missing", field), validationException);
143+
}
144+
return validationException;
145+
}
146+
147+
private static ActionRequestValidationException validateUnsupportedField(GrantType grantType, String field, Object fieldValue,
148+
ActionRequestValidationException validationException) {
149+
if (fieldValue != null) {
150+
validationException = addValidationError(
151+
String.format(Locale.ROOT, "%s is not supported with the %s grant_type", field, grantType.getValue()),
152+
validationException);
153+
}
154+
return validationException;
155+
}
156+
142157
public void setGrantType(String grantType) {
143158
this.grantType = grantType;
144159
}
@@ -151,6 +166,10 @@ public void setPassword(@Nullable SecureString password) {
151166
this.password = password;
152167
}
153168

169+
public void setKerberosTicket(@Nullable SecureString kerberosTicket) {
170+
this.kerberosTicket = kerberosTicket;
171+
}
172+
154173
public void setScope(@Nullable String scope) {
155174
this.scope = scope;
156175
}
@@ -173,6 +192,11 @@ public SecureString getPassword() {
173192
return password;
174193
}
175194

195+
@Nullable
196+
public SecureString getKerberosTicket() {
197+
return kerberosTicket;
198+
}
199+
176200
@Nullable
177201
public String getScope() {
178202
return scope;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
2727
private TimeValue expiresIn;
2828
private String scope;
2929
private String refreshToken;
30+
private String kerberosAuthenticationResponseToken;
3031

3132
CreateTokenResponse() {}
3233

33-
public CreateTokenResponse(String tokenString, TimeValue expiresIn, String scope, String refreshToken) {
34+
public CreateTokenResponse(String tokenString, TimeValue expiresIn, String scope, String refreshToken,
35+
String kerberosAuthenticationResponseToken) {
3436
this.tokenString = Objects.requireNonNull(tokenString);
3537
this.expiresIn = Objects.requireNonNull(expiresIn);
3638
this.scope = scope;
3739
this.refreshToken = refreshToken;
40+
this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
3841
}
3942

4043
public String getTokenString() {
@@ -53,6 +56,10 @@ public String getRefreshToken() {
5356
return refreshToken;
5457
}
5558

59+
public String getKerberosAuthenticationResponseToken() {
60+
return kerberosAuthenticationResponseToken;
61+
}
62+
5663
@Override
5764
public void writeTo(StreamOutput out) throws IOException {
5865
super.writeTo(out);
@@ -68,6 +75,7 @@ public void writeTo(StreamOutput out) throws IOException {
6875
out.writeString(refreshToken);
6976
}
7077
}
78+
out.writeOptionalString(kerberosAuthenticationResponseToken);
7179
}
7280

7381
@Override
@@ -81,6 +89,7 @@ public void readFrom(StreamInput in) throws IOException {
8189
} else if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
8290
refreshToken = in.readString();
8391
}
92+
kerberosAuthenticationResponseToken = in.readOptionalString();
8493
}
8594

8695
@Override
@@ -96,6 +105,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
96105
if (scope != null) {
97106
builder.field("scope", scope);
98107
}
108+
if (kerberosAuthenticationResponseToken != null) {
109+
builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
110+
}
99111
return builder.endObject();
100112
}
101113

@@ -107,11 +119,12 @@ public boolean equals(Object o) {
107119
return Objects.equals(tokenString, that.tokenString) &&
108120
Objects.equals(expiresIn, that.expiresIn) &&
109121
Objects.equals(scope, that.scope) &&
110-
Objects.equals(refreshToken, that.refreshToken);
122+
Objects.equals(refreshToken, that.refreshToken) &&
123+
Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken);
111124
}
112125

113126
@Override
114127
public int hashCode() {
115-
return Objects.hash(tokenString, expiresIn, scope, refreshToken);
128+
return Objects.hash(tokenString, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
116129
}
117130
}

0 commit comments

Comments
 (0)