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

Add kerberos grant_type to get token in exchange for Kerberos ticket #42847

Merged
merged 24 commits into from
Jun 18, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
22 changes: 20 additions & 2 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,19 @@ 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`,
(string) The type of grant. Supported grant types are: `password`, `_kerberos` (beta),
bizybot marked this conversation as resolved.
Show resolved Hide resolved
`client_credentials` and `refresh_token`.

`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 +165,17 @@ 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
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 = unsupportedFieldValidation(type, "kerberos_ticket", kerberosTicket, validationException);
validationException = unsupportedFieldValidation(type, "refresh_token", refreshToken, validationException);
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
validationException = validateRequiredField("username", username, validationException);
validationException = validateRequiredField("password", password, validationException);
break;
case KERBEROS:
validationException = unsupportedFieldValidation(type, "username", username, validationException);
validationException = unsupportedFieldValidation(type, "password", password, validationException);
validationException = unsupportedFieldValidation(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 = unsupportedFieldValidation(type, "username", username, validationException);
validationException = unsupportedFieldValidation(type, "password", password, validationException);
validationException = unsupportedFieldValidation(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 = unsupportedFieldValidation(type, "username", username, validationException);
validationException = unsupportedFieldValidation(type, "password", password, validationException);
validationException = unsupportedFieldValidation(type, "kerberos_ticket", kerberosTicket, validationException);
validationException = unsupportedFieldValidation(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.getChars().length == 0) {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
validationException = addValidationError(String.format(Locale.ROOT, "%s is missing", field), validationException);
}
return validationException;
}

private static ActionRequestValidationException unsupportedFieldValidation(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 @@ -19,7 +19,7 @@ public void testRequestValidation() {
ActionRequestValidationException ve = request.validate();
assertNotNull(ve);
assertEquals(1, ve.validationErrors().size());
assertThat(ve.validationErrors().get(0), containsString("[password, refresh_token, client_credentials]"));
assertThat(ve.validationErrors().get(0), containsString("[password, _kerberos, refresh_token, client_credentials]"));
assertThat(ve.validationErrors().get(0), containsString("grant_type"));

request.setGrantType("password");
Expand Down Expand Up @@ -49,20 +49,24 @@ public void testRequestValidation() {
assertNull(ve);

request.setRefreshToken(randomAlphaOfLengthBetween(1, 10));
request.setKerberosTicket(new SecureString(randomAlphaOfLengthBetween(1, 256).toCharArray()));
ve = request.validate();
assertNotNull(ve);
assertEquals(1, ve.validationErrors().size());
assertThat(ve.validationErrors().get(0), containsString("refresh_token is not supported"));
assertEquals(2, ve.validationErrors().size());
assertThat(ve.validationErrors(), hasItem(containsString("kerberos_ticket is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("refresh_token is not supported")));

request.setGrantType("refresh_token");
ve = request.validate();
assertNotNull(ve);
assertEquals(2, ve.validationErrors().size());
assertEquals(3, ve.validationErrors().size());
assertThat(ve.validationErrors(), hasItem(containsString("username is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("password is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("kerberos_ticket is not supported")));

request.setUsername(null);
request.setPassword(null);
request.setKerberosTicket(null);
ve = request.validate();
assertNull(ve);

Expand All @@ -78,12 +82,28 @@ public void testRequestValidation() {

request.setUsername(randomAlphaOfLengthBetween(1, 32));
request.setPassword(new SecureString(randomAlphaOfLengthBetween(1, 32).toCharArray()));
request.setKerberosTicket(new SecureString(randomAlphaOfLengthBetween(1, 256).toCharArray()));
request.setRefreshToken(randomAlphaOfLengthBetween(1, 32));
ve = request.validate();
assertNotNull(ve);
assertEquals(4, ve.validationErrors().size());
assertThat(ve.validationErrors(), hasItem(containsString("username is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("password is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("refresh_token is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("kerberos_ticket is not supported")));

request.setGrantType("_kerberos");
ve = request.validate();
assertNotNull(ve);
assertEquals(3, ve.validationErrors().size());
assertThat(ve.validationErrors(), hasItem(containsString("username is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("password is not supported")));
assertThat(ve.validationErrors(), hasItem(containsString("refresh_token is not supported")));

request.setUsername(null);
request.setPassword(null);
request.setRefreshToken(null);
ve = request.validate();
assertNull(ve);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest.GrantType;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken;

import java.util.Base64;
import java.util.Collections;

/**
Expand Down Expand Up @@ -51,7 +56,8 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
assert type != null : "type should have been validated in the action";
switch (type) {
case PASSWORD:
authenticateAndCreateToken(request, listener);
case KERBEROS:
authenticateAndCreateToken(type, request, listener);
break;
case CLIENT_CREDENTIALS:
Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext());
Expand All @@ -64,26 +70,59 @@ protected void doExecute(Task task, CreateTokenRequest request, ActionListener<C
}
}

private void authenticateAndCreateToken(CreateTokenRequest request, ActionListener<CreateTokenResponse> listener) {
private void authenticateAndCreateToken(GrantType grantType, CreateTokenRequest request, ActionListener<CreateTokenResponse> listener) {
Authentication originatingAuthentication = Authentication.getAuthentication(threadPool.getThreadContext());
try (ThreadContext.StoredContext ignore = threadPool.getThreadContext().stashContext()) {
final UsernamePasswordToken authToken = new UsernamePasswordToken(request.getUsername(), request.getPassword());
final AuthenticationToken authToken = extractAuthenticationToken(grantType, request, listener);
if (authToken == null) {
listener.onFailure(new IllegalStateException(
"grant_type [" + request.getGrantType() + "] is not supported by the create token action"));
return;
}

authenticationService.authenticate(CreateTokenAction.NAME, request, authToken,
ActionListener.wrap(authentication -> {
request.getPassword().close();
clearCredentialsFromRequest(grantType, request);

if (authentication != null) {
bizybot marked this conversation as resolved.
Show resolved Hide resolved
createToken(request, authentication, originatingAuthentication, true, listener);
} else {
listener.onFailure(new UnsupportedOperationException("cannot create token if authentication is not allowed"));
}
}, e -> {
// clear the request password
request.getPassword().close();
clearCredentialsFromRequest(grantType, request);
listener.onFailure(e);
}));
}
}

private AuthenticationToken extractAuthenticationToken(GrantType grantType, CreateTokenRequest request,
ActionListener<CreateTokenResponse> listener) {
AuthenticationToken authToken = null;
if (grantType == GrantType.PASSWORD) {
authToken = new UsernamePasswordToken(request.getUsername(), request.getPassword());
} else if (grantType == GrantType.KERBEROS) {
SecureString kerberosTicket = request.getKerberosTicket();
String base64EncodedToken = kerberosTicket.toString();
byte[] decodedKerberosTicket = null;
try {
decodedKerberosTicket = Base64.getDecoder().decode(base64EncodedToken);
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
} catch (IllegalArgumentException iae) {
listener.onFailure(new UnsupportedOperationException("could not decode base64 kerberos ticket " + base64EncodedToken));
}
authToken = new KerberosAuthenticationToken(decodedKerberosTicket);
}
return authToken;
}

private void clearCredentialsFromRequest(GrantType grantType, CreateTokenRequest request) {
if (grantType == GrantType.PASSWORD) {
request.getPassword().close();
} else if (grantType == GrantType.KERBEROS) {
request.getKerberosTicket().close();
}
}

private void createToken(CreateTokenRequest request, Authentication authentication, Authentication originatingAuth,
boolean includeRefreshToken, ActionListener<CreateTokenResponse> listener) {
tokenService.createOAuth2Tokens(authentication, originatingAuth, Collections.emptyMap(), includeRefreshToken,
Expand Down
Loading