Browse Source

Support Profile Activate with JWTs with client authn (#105439)

Adds support for JWTs with client authentication
to the activate user profile API.

Closes #105342
Albert Zaharovits 1 year ago
parent
commit
b2e626e7df

+ 6 - 0
docs/changelog/105439.yaml

@@ -0,0 +1,6 @@
+pr: 105439
+summary: Support Profile Activate with JWTs with client authn
+area: Authentication
+type: enhancement
+issues:
+ - 105342

+ 13 - 9
docs/reference/rest-api/security/activate-user-profile.asciidoc

@@ -28,6 +28,9 @@ Creates or updates a user profile on behalf of another user.
 The activate user profile API creates or updates a profile document for end
 The activate user profile API creates or updates a profile document for end
 users with information that is extracted from the user's authentication object,
 users with information that is extracted from the user's authentication object,
 including `username`, `full_name`, `roles`, and the authentication realm.
 including `username`, `full_name`, `roles`, and the authentication realm.
+For example, in the JWT `access_token` case, the profile user's `username` is
+extracted from the JWT token claim pointed to by the `claims.principal`
+setting of the JWT realm that authenticated the token.
 
 
 When updating a profile document, the API enables the document if it was
 When updating a profile document, the API enables the document if it was
 disabled. Any updates do not change existing content for either the `labels` or
 disabled. Any updates do not change existing content for either the `labels` or
@@ -46,8 +49,11 @@ is intended for.
 
 
 `access_token`::
 `access_token`::
 (Required*, string)
 (Required*, string)
-The user's access token. If you specify the `access_token` grant type, this
-parameter is required. It is not valid with other grant types.
+The user's <<security-api-get-token, {es} access token>>, or JWT. Both <<jwt-realm-oauth2, access>> and
+<<jwt-realm-oidc, id>> JWT token types are supported, and they depend on the underlying JWT realm configuration.
+If you specify the `access_token` grant type, this parameter is required. It is not valid with other grant types.
+
+include::client-authentication.asciidoc[]
 
 
 `grant_type`::
 `grant_type`::
 (Required, string)
 (Required, string)
@@ -57,24 +63,22 @@ The type of grant.
 [%collapsible%open]
 [%collapsible%open]
 ====
 ====
 `access_token`::
 `access_token`::
-(Required*, string)
-In this type of grant, you must supply an access token that was created by the
-{es} token service. For more information, see
-<<security-api-get-token>> and <<token-service-settings>>.
+In this type of grant, you must supply either an access token, that was created by the
+{es} token service (see <<security-api-get-token>> and <<encrypt-http-communication>>),
+or a <<jwt-auth-realm, JWT>> (either a JWT `access_token` or a JWT `id_token`).
 
 
 `password`::
 `password`::
-(Required*, string)
 In this type of grant, you must supply the `username` and `password` for the
 In this type of grant, you must supply the `username` and `password` for the
 user that you want to create the API key for.
 user that you want to create the API key for.
 ====
 ====
 
 
 `password`::
 `password`::
-(Optional*, string)
+(Required*, string)
 The user's password. If you specify the `password` grant type, this parameter is
 The user's password. If you specify the `password` grant type, this parameter is
 required. It is not valid with other grant types.
 required. It is not valid with other grant types.
 
 
 `username`::
 `username`::
-(Optional*, string)
+(Required*, string)
 The username that identifies the user. If you specify the `password` grant type,
 The username that identifies the user. If you specify the `password` grant type,
 this parameter is required. It is not valid with other grant types.
 this parameter is required. It is not valid with other grant types.
 
 

+ 16 - 0
docs/reference/rest-api/security/client-authentication.asciidoc

@@ -0,0 +1,16 @@
+`client_authentication`::
+(Optional, object) When using the `access_token` grant type, and when supplying a
+JWT, this specifies the client authentication for <<jwt-auth-realm, JWTs>> that
+need it (i.e. what's normally specified by the `ES-Client-Authentication` request header).
+
+`scheme`:::
+(Required, string) The scheme (case-sensitive) as it's supplied in the
+`ES-Client-Authentication` request header. Currently, the only supported
+value is <<jwt-auth-shared-secret-scheme-example, `SharedSecret`>>.
+
+`value`:::
+(Required, string) The value that follows the scheme for the client credentials
+as it's supplied in the `ES-Client-Authentication` request header. For example,
+if the request header would be `ES-Client-Authentication: SharedSecret myShar3dS3cret`
+if the client were to authenticate directly with a JWT, then `value` here should
+be `myShar3dS3cret`.

+ 5 - 19
docs/reference/rest-api/security/grant-api-keys.asciidoc

@@ -89,29 +89,13 @@ It supports nested data structure.
 Within the `metadata` object, keys beginning with `_` are reserved for
 Within the `metadata` object, keys beginning with `_` are reserved for
 system usage.
 system usage.
 
 
-`client_authentication`::
-(Optional, object) When using the `access_token` grant type, and when supplying a
-JWT, this specifies the client authentication for <<jwt-auth-realm, JWTs>> that
-need it (i.e. what's normally specified by the `ES-Client-Authentication` request header).
-
-`scheme`:::
-(Required, string) The scheme (case-sensitive) as it's supplied in the
-`ES-Client-Authentication` request header. Currently, the only supported
-value is <<jwt-auth-shared-secret-scheme-example, `SharedSecret`>>.
-
-`value`:::
-(Required, string) The value that follows the scheme for the client credentials
-as it's supplied in the `ES-Client-Authentication` request header. For example,
-if the request header would be `ES-Client-Authentication: SharedSecret myShar3dS3cret`
-if the client were to authenticate directly with a JWT, then `value` here should
-be `myShar3dS3cret`.
+include::client-authentication.asciidoc[]
 
 
 `grant_type`::
 `grant_type`::
 (Required, string)
 (Required, string)
 The type of grant. Supported grant types are: `access_token`,`password`.
 The type of grant. Supported grant types are: `access_token`,`password`.
 
 
 `access_token`:::
 `access_token`:::
-(Required*, string)
 In this type of grant, you must supply either an access token, that was created by the
 In this type of grant, you must supply either an access token, that was created by the
 {es} token service (see <<security-api-get-token>> and <<encrypt-http-communication>>),
 {es} token service (see <<security-api-get-token>> and <<encrypt-http-communication>>),
 or a <<jwt-auth-realm, JWT>> (either a JWT `access_token` or a JWT `id_token`).
 or a <<jwt-auth-realm, JWT>> (either a JWT `access_token` or a JWT `id_token`).
@@ -121,12 +105,12 @@ In this type of grant, you must supply the user ID and password for which you
 want to create the API key.
 want to create the API key.
 
 
 `password`::
 `password`::
-(Optional*, string)
+(Required*, string)
 The user's password. If you specify the `password` grant type, this parameter is
 The user's password. If you specify the `password` grant type, this parameter is
 required. It is not valid with other grant types.
 required. It is not valid with other grant types.
 
 
 `username`::
 `username`::
-(Optional*, string)
+(Required*, string)
 The user name that identifies the user. If you specify the `password` grant type,
 The user name that identifies the user. If you specify the `password` grant type,
 this parameter is required. It is not valid with other grant types.
 this parameter is required. It is not valid with other grant types.
 
 
@@ -134,6 +118,8 @@ this parameter is required. It is not valid with other grant types.
 (Optional, string)
 (Optional, string)
 The name of the user to be <<run-as-privilege,impersonated>>.
 The name of the user to be <<run-as-privilege,impersonated>>.
 
 
+*Indicates that the setting is required in some, but not all situations.
+
 [[security-api-grant-api-key-example]]
 [[security-api-grant-api-key-example]]
 ==== {api-examples-title}
 ==== {api-examples-title}
 
 

+ 95 - 0
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSingleNodeTests.java

@@ -41,6 +41,12 @@ import org.elasticsearch.xpack.core.security.action.Grant;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
+import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction;
+import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileRequest;
+import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileResponse;
+import org.elasticsearch.xpack.core.security.action.profile.GetProfilesAction;
+import org.elasticsearch.xpack.core.security.action.profile.GetProfilesRequest;
+import org.elasticsearch.xpack.core.security.action.profile.GetProfilesResponse;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse;
 import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse;
@@ -237,6 +243,84 @@ public class JwtRealmSingleNodeTests extends SecuritySingleNodeTestCase {
         }
         }
     }
     }
 
 
+    public void testActivateProfileForJWT() throws Exception {
+        final JWTClaimsSet.Builder jwtClaims = new JWTClaimsSet.Builder();
+        final String principal;
+        final String sharedSecret;
+        final String realmName;
+        // id_token or access_token
+        if (randomBoolean()) {
+            principal = "me";
+            // JWT "id_token" valid for jwt0
+            jwtClaims.audience("es-01")
+                .issuer("my-issuer-01")
+                .subject(principal)
+                .claim("groups", "admin")
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .build();
+            sharedSecret = jwt0SharedSecret;
+            realmName = "jwt0";
+        } else {
+            principal = "me@example.com";
+            // JWT "access_token" valid for jwt2
+            jwtClaims.audience("es-03")
+                .issuer("my-issuer-03")
+                .subject("user-03")
+                .claim("groups", "admin")
+                .claim("email", principal)
+                .issueTime(Date.from(Instant.now()))
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)));
+            sharedSecret = jwt2SharedSecret;
+            realmName = "jwt2";
+        }
+        {
+            // JWT is valid but the client authentication is NOT
+            ActivateProfileRequest activateProfileRequest = getActivateProfileForJWT(
+                getSignedJWT(jwtClaims.build()),
+                randomFrom("WRONG", null)
+            );
+            ElasticsearchSecurityException e = expectThrows(
+                ElasticsearchSecurityException.class,
+                () -> client().execute(ActivateProfileAction.INSTANCE, activateProfileRequest).actionGet()
+            );
+            assertThat(e.getMessage(), containsString("unable to authenticate user"));
+        }
+        {
+            // both JWT and client authentication are valid
+            ActivateProfileRequest activateProfileRequest = getActivateProfileForJWT(getSignedJWT(jwtClaims.build()), sharedSecret);
+            ActivateProfileResponse activateProfileResponse = client().execute(ActivateProfileAction.INSTANCE, activateProfileRequest)
+                .actionGet();
+            assertThat(activateProfileResponse.getProfile(), notNullValue());
+            assertThat(activateProfileResponse.getProfile().uid(), notNullValue());
+            assertThat(activateProfileResponse.getProfile().user().username(), is(principal));
+            assertThat(activateProfileResponse.getProfile().user().realmName(), is(realmName));
+            // test to get the profile by uid
+            GetProfilesRequest getProfilesRequest = new GetProfilesRequest(List.of(activateProfileResponse.getProfile().uid()), Set.of());
+            GetProfilesResponse getProfilesResponse = client().execute(GetProfilesAction.INSTANCE, getProfilesRequest).actionGet();
+            assertThat(getProfilesResponse.getProfiles().size(), is(1));
+            assertThat(getProfilesResponse.getProfiles().get(0).uid(), is(activateProfileResponse.getProfile().uid()));
+            assertThat(getProfilesResponse.getProfiles().get(0).enabled(), is(true));
+            assertThat(getProfilesResponse.getProfiles().get(0).user().username(), is(principal));
+            assertThat(getProfilesResponse.getProfiles().get(0).user().realmName(), is(realmName));
+        }
+        {
+            // client authentication is valid but the JWT is not
+            final SignedJWT wrongJWT;
+            if (randomBoolean()) {
+                wrongJWT = getSignedJWT(jwtClaims.build(), ("wrong key that's longer than 256 bits").getBytes(StandardCharsets.UTF_8));
+            } else {
+                wrongJWT = getSignedJWT(jwtClaims.audience("wrong audience claim value").build());
+            }
+            ActivateProfileRequest activateProfileRequest = getActivateProfileForJWT(wrongJWT, sharedSecret);
+            ElasticsearchSecurityException e = expectThrows(
+                ElasticsearchSecurityException.class,
+                () -> client().execute(ActivateProfileAction.INSTANCE, activateProfileRequest).actionGet()
+            );
+            assertThat(e.getMessage(), containsString("unable to authenticate user"));
+        }
+    }
+
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
     public void testInvalidJWTDoesNotFallbackToAnonymousAccess() throws Exception {
     public void testInvalidJWTDoesNotFallbackToAnonymousAccess() throws Exception {
         // anonymous access works when no valid Bearer
         // anonymous access works when no valid Bearer
@@ -696,4 +780,15 @@ public class JwtRealmSingleNodeTests extends SecuritySingleNodeTestCase {
         grantApiKeyRequest.getApiKeyRequest().setName(randomAlphaOfLength(8));
         grantApiKeyRequest.getApiKeyRequest().setName(randomAlphaOfLength(8));
         return grantApiKeyRequest;
         return grantApiKeyRequest;
     }
     }
+
+    private static ActivateProfileRequest getActivateProfileForJWT(SignedJWT signedJWT, String sharedSecret) {
+        ActivateProfileRequest activateProfileRequest = new ActivateProfileRequest();
+        activateProfileRequest.getGrant().setType("access_token");
+        activateProfileRequest.getGrant().setAccessToken(new SecureString(signedJWT.serialize().toCharArray()));
+        if (sharedSecret != null) {
+            activateProfileRequest.getGrant()
+                .setClientAuthentication(new Grant.ClientAuthentication("SharedSecret", new SecureString(sharedSecret.toCharArray())));
+        }
+        return activateProfileRequest;
+    }
 }
 }

+ 26 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java

@@ -7,14 +7,21 @@
 package org.elasticsearch.xpack.security.rest.action;
 package org.elasticsearch.xpack.security.rest.action;
 
 
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestResponse;
 import org.elasticsearch.rest.RestResponse;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ObjectParser;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.XPackSettings;
+import org.elasticsearch.xpack.core.security.action.Grant;
 
 
 import java.io.IOException;
 import java.io.IOException;
+import java.util.Arrays;
 
 
 /**
 /**
  * Base class for security rest handlers. This handler takes care of ensuring that the license
  * Base class for security rest handlers. This handler takes care of ensuring that the license
@@ -22,6 +29,25 @@ import java.io.IOException;
  */
  */
 public abstract class SecurityBaseRestHandler extends BaseRestHandler {
 public abstract class SecurityBaseRestHandler extends BaseRestHandler {
 
 
+    protected static final ConstructingObjectParser<Grant.ClientAuthentication, Void> CLIENT_AUTHENTICATION_PARSER =
+        new ConstructingObjectParser<>("client_authentication", a -> new Grant.ClientAuthentication((String) a[0], (SecureString) a[1]));
+
+    static {
+        CLIENT_AUTHENTICATION_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("scheme"));
+        CLIENT_AUTHENTICATION_PARSER.declareField(
+            ConstructingObjectParser.constructorArg(),
+            SecurityBaseRestHandler::getSecureString,
+            new ParseField("value"),
+            ObjectParser.ValueType.STRING
+        );
+    }
+
+    protected static SecureString getSecureString(XContentParser parser) throws IOException {
+        return new SecureString(
+            Arrays.copyOfRange(parser.textCharacters(), parser.textOffset(), parser.textOffset() + parser.textLength())
+        );
+    }
+
     protected final Settings settings;
     protected final Settings settings;
     protected final XPackLicenseState licenseState;
     protected final XPackLicenseState licenseState;
 
 

+ 4 - 27
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java

@@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
-import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestRequest;
@@ -20,19 +19,17 @@ import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.rest.action.RestToXContentListener;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
 import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.ObjectParser;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xpack.core.security.action.Grant;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
+import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
 
 
 import java.io.IOException;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
@@ -46,31 +43,19 @@ import static org.elasticsearch.rest.RestRequest.Method.PUT;
 @ServerlessScope(Scope.INTERNAL)
 @ServerlessScope(Scope.INTERNAL)
 public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implements RestRequestFilter {
 public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implements RestRequestFilter {
 
 
-    private static final ConstructingObjectParser<Grant.ClientAuthentication, Void> CLIENT_AUTHENTICATION_PARSER =
-        new ConstructingObjectParser<>("client_authentication", a -> new Grant.ClientAuthentication((String) a[0], (SecureString) a[1]));
-    static {
-        CLIENT_AUTHENTICATION_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("scheme"));
-        CLIENT_AUTHENTICATION_PARSER.declareField(
-            ConstructingObjectParser.constructorArg(),
-            RestGrantApiKeyAction::getSecureString,
-            new ParseField("value"),
-            ObjectParser.ValueType.STRING
-        );
-    }
-
     static final ObjectParser<GrantApiKeyRequest, Void> PARSER = new ObjectParser<>("grant_api_key_request", GrantApiKeyRequest::new);
     static final ObjectParser<GrantApiKeyRequest, Void> PARSER = new ObjectParser<>("grant_api_key_request", GrantApiKeyRequest::new);
     static {
     static {
         PARSER.declareString((req, str) -> req.getGrant().setType(str), new ParseField("grant_type"));
         PARSER.declareString((req, str) -> req.getGrant().setType(str), new ParseField("grant_type"));
         PARSER.declareString((req, str) -> req.getGrant().setUsername(str), new ParseField("username"));
         PARSER.declareString((req, str) -> req.getGrant().setUsername(str), new ParseField("username"));
         PARSER.declareField(
         PARSER.declareField(
             (req, secStr) -> req.getGrant().setPassword(secStr),
             (req, secStr) -> req.getGrant().setPassword(secStr),
-            RestGrantApiKeyAction::getSecureString,
+            SecurityBaseRestHandler::getSecureString,
             new ParseField("password"),
             new ParseField("password"),
             ObjectParser.ValueType.STRING
             ObjectParser.ValueType.STRING
         );
         );
         PARSER.declareField(
         PARSER.declareField(
             (req, secStr) -> req.getGrant().setAccessToken(secStr),
             (req, secStr) -> req.getGrant().setAccessToken(secStr),
-            RestGrantApiKeyAction::getSecureString,
+            SecurityBaseRestHandler::getSecureString,
             new ParseField("access_token"),
             new ParseField("access_token"),
             ObjectParser.ValueType.STRING
             ObjectParser.ValueType.STRING
         );
         );
@@ -87,12 +72,6 @@ public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implement
         );
         );
     }
     }
 
 
-    private static SecureString getSecureString(XContentParser parser) throws IOException {
-        return new SecureString(
-            Arrays.copyOfRange(parser.textCharacters(), parser.textOffset(), parser.textOffset() + parser.textLength())
-        );
-    }
-
     public RestGrantApiKeyAction(Settings settings, XPackLicenseState licenseState) {
     public RestGrantApiKeyAction(Settings settings, XPackLicenseState licenseState) {
         super(settings, licenseState);
         super(settings, licenseState);
     }
     }
@@ -138,10 +117,8 @@ public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implement
         }
         }
     }
     }
 
 
-    private static final Set<String> FILTERED_FIELDS = Set.of("password", "access_token", "client_authentication.value");
-
     @Override
     @Override
     public Set<String> getFilteredFields() {
     public Set<String> getFilteredFields() {
-        return FILTERED_FIELDS;
+        return Set.of("password", "access_token", "client_authentication.value");
     }
     }
 }
 }

+ 14 - 13
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/profile/RestActivateProfileAction.java

@@ -8,7 +8,6 @@
 package org.elasticsearch.xpack.security.rest.action.profile;
 package org.elasticsearch.xpack.security.rest.action.profile;
 
 
 import org.elasticsearch.client.internal.node.NodeClient;
 import org.elasticsearch.client.internal.node.NodeClient;
-import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.RestRequest;
@@ -24,7 +23,6 @@ import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileReque
 import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
 import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
 
 
 import java.io.IOException;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
@@ -42,16 +40,21 @@ public class RestActivateProfileAction extends SecurityBaseRestHandler implement
         PARSER.declareString((req, str) -> req.getGrant().setUsername(str), new ParseField("username"));
         PARSER.declareString((req, str) -> req.getGrant().setUsername(str), new ParseField("username"));
         PARSER.declareField(
         PARSER.declareField(
             (req, secStr) -> req.getGrant().setPassword(secStr),
             (req, secStr) -> req.getGrant().setPassword(secStr),
-            RestActivateProfileAction::getSecureString,
+            SecurityBaseRestHandler::getSecureString,
             new ParseField("password"),
             new ParseField("password"),
             ObjectParser.ValueType.STRING
             ObjectParser.ValueType.STRING
         );
         );
         PARSER.declareField(
         PARSER.declareField(
             (req, secStr) -> req.getGrant().setAccessToken(secStr),
             (req, secStr) -> req.getGrant().setAccessToken(secStr),
-            RestActivateProfileAction::getSecureString,
+            SecurityBaseRestHandler::getSecureString,
             new ParseField("access_token"),
             new ParseField("access_token"),
             ObjectParser.ValueType.STRING
             ObjectParser.ValueType.STRING
         );
         );
+        PARSER.declareObject(
+            (req, clientAuthentication) -> req.getGrant().setClientAuthentication(clientAuthentication),
+            CLIENT_AUTHENTICATION_PARSER,
+            new ParseField("client_authentication")
+        );
     }
     }
 
 
     public RestActivateProfileAction(Settings settings, XPackLicenseState licenseState) {
     public RestActivateProfileAction(Settings settings, XPackLicenseState licenseState) {
@@ -68,23 +71,21 @@ public class RestActivateProfileAction extends SecurityBaseRestHandler implement
         return "xpack_security_activate_profile";
         return "xpack_security_activate_profile";
     }
     }
 
 
+    // package-private for tests
+    static ActivateProfileRequest fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
     @Override
     @Override
     protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
     protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
         try (XContentParser parser = request.contentParser()) {
         try (XContentParser parser = request.contentParser()) {
-            final ActivateProfileRequest activateProfileRequest = PARSER.parse(parser, null);
+            final ActivateProfileRequest activateProfileRequest = fromXContent(parser);
             return channel -> client.execute(ActivateProfileAction.INSTANCE, activateProfileRequest, new RestToXContentListener<>(channel));
             return channel -> client.execute(ActivateProfileAction.INSTANCE, activateProfileRequest, new RestToXContentListener<>(channel));
         }
         }
     }
     }
 
 
-    // TODO: extract to standalone helper method
-    private static SecureString getSecureString(XContentParser parser) throws IOException {
-        return new SecureString(
-            Arrays.copyOfRange(parser.textCharacters(), parser.textOffset(), parser.textOffset() + parser.textLength())
-        );
-    }
-
     @Override
     @Override
     public Set<String> getFilteredFields() {
     public Set<String> getFilteredFields() {
-        return Set.of("password", "access_token");
+        return Set.of("password", "access_token", "client_authentication.value");
     }
     }
 }
 }

+ 54 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/profile/RestActivateProfileActionTests.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.rest.action.profile;
+
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xcontent.XContentFactory;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileRequest;
+
+import static org.hamcrest.Matchers.is;
+
+public class RestActivateProfileActionTests extends ESTestCase {
+
+    public void testParseXContentForGrantApiKeyRequest() throws Exception {
+        final String grantType = randomAlphaOfLength(8);
+        final String username = randomAlphaOfLength(8);
+        final String password = randomAlphaOfLength(8);
+        final String accessToken = randomAlphaOfLength(8);
+        final String clientAuthenticationScheme = randomAlphaOfLength(8);
+        final String clientAuthenticationValue = randomAlphaOfLength(8);
+        try (
+            XContentParser content = createParser(
+                XContentFactory.jsonBuilder()
+                    .startObject()
+                    .field("grant_type", grantType)
+                    .field("username", username)
+                    .field("password", password)
+                    .field("access_token", accessToken)
+                    .startObject("client_authentication")
+                    .field("scheme", clientAuthenticationScheme)
+                    .field("value", clientAuthenticationValue)
+                    .endObject()
+                    .endObject()
+            )
+        ) {
+            ActivateProfileRequest activateProfileRequest = RestActivateProfileAction.fromXContent(content);
+            assertThat(activateProfileRequest.getGrant().getType(), is(grantType));
+            assertThat(activateProfileRequest.getGrant().getUsername(), is(username));
+            assertThat(activateProfileRequest.getGrant().getPassword(), is(new SecureString(password.toCharArray())));
+            assertThat(activateProfileRequest.getGrant().getAccessToken(), is(new SecureString(accessToken.toCharArray())));
+            assertThat(activateProfileRequest.getGrant().getClientAuthentication().scheme(), is(clientAuthenticationScheme));
+            assertThat(
+                activateProfileRequest.getGrant().getClientAuthentication().value(),
+                is(new SecureString(clientAuthenticationValue.toCharArray()))
+            );
+        }
+    }
+}