Browse Source

Adding authentication information to access token create APIs (#62490)

* Adding authentication information to access token create APIs

Adding authentication object to following APIs:
/_security/oauth2/token
/_security/delegate_pki
/_security/saml/authenticate
/_security/oidc/authenticate

Resolves: #59685
(cherry picked from commit 51dbd9e584813dcb82f9c28cfff8a1a9f4af846c)

* Addressing PR commends, fixing tests

* Returning tokenGroups attribute as SID string instead of byte array (AD metadata)

Addressing PR comments

* Returning tokenGroups attribute as SID string instead of byte array (AD metadata)

Update version check

* Returning tokenGroups attribute as SID string instead of byte array (AD metadata)

Update version check

* Addressing more PR comments

* Adding more to integration tests + some small fixes
Lyudmila Fokina 5 years ago
parent
commit
2351bb399c
28 changed files with 500 additions and 96 deletions
  1. 24 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java
  2. 11 4
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateTokenResponse.java
  3. 13 4
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java
  4. 8 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateTokenResponseTests.java
  5. 57 2
      client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java
  6. 3 1
      docs/java-rest/high-level/security/create-token.asciidoc
  7. 2 0
      docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc
  8. 22 1
      x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc
  9. 91 15
      x-pack/docs/en/rest-api/security/get-tokens.asciidoc
  10. 44 6
      x-pack/docs/en/rest-api/security/invalidate-tokens.asciidoc
  11. 26 25
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java
  12. 15 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java
  13. 14 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateResponse.java
  14. 20 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java
  15. 96 3
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java
  16. 8 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java
  17. 3 0
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java
  18. 3 0
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java
  19. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java
  20. 1 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java
  21. 1 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java
  22. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java
  23. 6 3
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportRefreshTokenAction.java
  24. 7 6
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java
  25. 8 7
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java
  26. 10 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenActionTests.java
  27. 1 0
      x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java
  28. 4 0
      x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java

+ 24 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java

@@ -22,6 +22,8 @@ package org.elasticsearch.client.security;
 import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.io.IOException;
@@ -38,7 +40,7 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optiona
  * user object contains all user metadata which Elasticsearch uses to map roles,
  * etc.
  */
-public final class AuthenticateResponse {
+public final class AuthenticateResponse implements ToXContentObject {
 
     static final ParseField USERNAME = new ParseField("username");
     static final ParseField ROLES = new ParseField("roles");
@@ -123,6 +125,27 @@ public final class AuthenticateResponse {
         return authenticationType;
     }
 
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject()
+            .field("username", user.getUsername())
+            .field("roles", user.getRoles())
+            .field("metadata", user.getMetadata())
+            .field("full_name", user.getFullName())
+            .field("email", user.getEmail())
+            .field("enabled", enabled);
+        builder.startObject("authentication_realm")
+            .field("name", authenticationRealm.name)
+            .field("type", authenticationRealm.type);
+        builder.endObject();
+        builder.startObject("lookup_realm")
+            .field("name", lookupRealm == null? authenticationRealm.name: lookupRealm.name)
+            .field("type", lookupRealm == null? authenticationRealm.type: lookupRealm.type);
+        builder.endObject();
+            builder.field("authentication_type", authenticationType);
+        return builder.endObject();
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;

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

@@ -42,15 +42,17 @@ public final class CreateTokenResponse {
     private final String scope;
     private final String refreshToken;
     private final String kerberosAuthenticationResponseToken;
+    private final AuthenticateResponse authenticationResponse;
 
     public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken,
-                               String kerberosAuthenticationResponseToken) {
+                               String kerberosAuthenticationResponseToken, AuthenticateResponse authenticationResponse) {
         this.accessToken = accessToken;
         this.type = type;
         this.expiresIn = expiresIn;
         this.scope = scope;
         this.refreshToken = refreshToken;
         this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
+        this.authenticationResponse = authenticationResponse;
     }
 
     public String getAccessToken() {
@@ -77,6 +79,8 @@ public final class CreateTokenResponse {
         return kerberosAuthenticationResponseToken;
     }
 
+    public AuthenticateResponse getAuthenticationResponse() { return authenticationResponse; }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -91,17 +95,19 @@ public final class CreateTokenResponse {
             Objects.equals(expiresIn, that.expiresIn) &&
             Objects.equals(scope, that.scope) &&
             Objects.equals(refreshToken, that.refreshToken) &&
-            Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken);
+            Objects.equals(kerberosAuthenticationResponseToken, that.kerberosAuthenticationResponseToken)&&
+            Objects.equals(authenticationResponse, that.authenticationResponse);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(accessToken, type, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
+        return Objects.hash(accessToken, type, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken, authenticationResponse);
     }
 
     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], (String) args[5]));
+                    TimeValue.timeValueSeconds((Long) args[2]), (String) args[3], (String) args[4], (String) args[5],
+                    (AuthenticateResponse) args[6]));
 
     static {
         PARSER.declareString(constructorArg(), new ParseField("access_token"));
@@ -110,6 +116,7 @@ public final class CreateTokenResponse {
         PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("scope"));
         PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("refresh_token"));
         PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("kerberos_authentication_response_token"));
+        PARSER.declareObject(constructorArg(), (p, c) -> AuthenticateResponse.fromXContent(p), new ParseField("authentication"));
     }
 
     public static CreateTokenResponse fromXContent(XContentParser parser) throws IOException {

+ 13 - 4
client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java

@@ -34,11 +34,14 @@ public final class DelegatePkiAuthenticationResponse {
     private final String accessToken;
     private final String type;
     private final TimeValue expiresIn;
+    private final AuthenticateResponse authenticationResponse;
 
-    public DelegatePkiAuthenticationResponse(String accessToken, String type, TimeValue expiresIn) {
+    public DelegatePkiAuthenticationResponse(String accessToken, String type, TimeValue expiresIn,
+                                             AuthenticateResponse authenticationResponse) {
         this.accessToken = accessToken;
         this.type = type;
         this.expiresIn = expiresIn;
+        this.authenticationResponse = authenticationResponse;
     }
 
     public String getAccessToken() {
@@ -53,6 +56,8 @@ public final class DelegatePkiAuthenticationResponse {
         return expiresIn;
     }
 
+    public AuthenticateResponse getAuthenticationResponse() { return authenticationResponse; }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -64,22 +69,26 @@ public final class DelegatePkiAuthenticationResponse {
         final DelegatePkiAuthenticationResponse that = (DelegatePkiAuthenticationResponse) o;
         return Objects.equals(accessToken, that.accessToken) &&
             Objects.equals(type, that.type) &&
-            Objects.equals(expiresIn, that.expiresIn);
+            Objects.equals(expiresIn, that.expiresIn) &&
+            Objects.equals(authenticationResponse, that.authenticationResponse);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(accessToken, type, expiresIn);
+        return Objects.hash(accessToken, type, expiresIn, authenticationResponse);
     }
 
+    @SuppressWarnings("unchecked")
     private static final ConstructingObjectParser<DelegatePkiAuthenticationResponse, Void> PARSER = new ConstructingObjectParser<>(
             "delegate_pki_response", true,
-            args -> new DelegatePkiAuthenticationResponse((String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2])));
+            args -> new DelegatePkiAuthenticationResponse((String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2]),
+                (AuthenticateResponse) args[3]));
 
     static {
         PARSER.declareString(constructorArg(), new ParseField("access_token"));
         PARSER.declareString(constructorArg(), new ParseField("type"));
         PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
+        PARSER.declareObject(constructorArg(), (p, c) -> AuthenticateResponse.fromXContent(p), new ParseField("authentication"));
     }
 
     public static DelegatePkiAuthenticationResponse fromXContent(XContentParser parser) throws IOException {

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

@@ -18,6 +18,7 @@
  */
 package org.elasticsearch.client.security;
 
+import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -26,6 +27,7 @@ import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
+import java.util.Arrays;
 
 import static org.hamcrest.Matchers.equalTo;
 
@@ -38,6 +40,10 @@ public class CreateTokenResponseTests extends ESTestCase {
         final String scope = randomBoolean() ? null : randomAlphaOfLength(4);
         final String type = randomAlphaOfLength(6);
         final String kerberosAuthenticationResponseToken = randomBoolean() ? null : randomAlphaOfLength(7);
+        final AuthenticateResponse authenticateResponse = new AuthenticateResponse(new User(randomAlphaOfLength(7),
+            Arrays.asList( randomAlphaOfLength(9) )),
+            true, new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(7) ),
+            new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5) ), "realm");
 
         final XContentType xContentType = randomFrom(XContentType.values());
         final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
@@ -54,6 +60,7 @@ public class CreateTokenResponseTests extends ESTestCase {
         if (kerberosAuthenticationResponseToken != null) {
             builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
         }
+        builder.field("authentication", authenticateResponse);
         builder.endObject();
         BytesReference xContent = BytesReference.bytes(builder);
 
@@ -64,5 +71,6 @@ public class CreateTokenResponseTests extends ESTestCase {
         assertThat(response.getType(), equalTo(type));
         assertThat(response.getExpiresIn(), equalTo(expiresIn));
         assertThat(response.getKerberosAuthenticationResponseToken(), equalTo(kerberosAuthenticationResponseToken));
+        assertThat(response.getAuthenticationResponse(), equalTo(authenticateResponse));
     }
 }

+ 57 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java

@@ -19,14 +19,21 @@
 
 package org.elasticsearch.client.security;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.client.AbstractResponseTestCase;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
 import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.core.security.user.User;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 
 public class DelegatePkiAuthenticationResponseTests extends
@@ -37,7 +44,8 @@ public class DelegatePkiAuthenticationResponseTests extends
     protected org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse createServerTestInstance(
         XContentType xContentType) {
         return new org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse(randomAlphaOfLength(6),
-                TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"));
+                TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"),
+                createAuthentication());
     }
 
     @Override
@@ -51,5 +59,52 @@ public class DelegatePkiAuthenticationResponseTests extends
         assertThat(serverTestInstance.getAccessToken(), is(clientInstance.getAccessToken()));
         assertThat(serverTestInstance.getExpiresIn(), is(clientInstance.getExpiresIn()));
         assertThat(clientInstance.getType(), is("Bearer"));
+        AuthenticateResponse serverAuthenticationResponse = createServerAuthenticationResponse(serverTestInstance.getAuthentication());
+        User user = serverTestInstance.getAuthentication().getUser();
+        assertThat(serverAuthenticationResponse, equalTo(clientInstance.getAuthenticationResponse()));
+    }
+
+    protected Authentication createAuthentication() {
+        final String username = randomAlphaOfLengthBetween(1, 4);
+        final String[] roles = generateRandomStringArray(4, 4, false, true);
+        final Map<String, Object> metadata;
+        metadata = new HashMap<>();
+        if (randomBoolean()) {
+            metadata.put("string", null);
+        } else {
+            metadata.put("string", randomAlphaOfLengthBetween(0, 4));
+        }
+        if (randomBoolean()) {
+            metadata.put("string_list", null);
+        } else {
+            metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true)));
+        }
+        final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final boolean enabled = randomBoolean();
+        final String authenticationRealmName = randomAlphaOfLength(5);
+        final String authenticationRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos");
+        final String lookupRealmName = randomAlphaOfLength(5);
+        final String lookupRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos");
+        final String nodeName = randomAlphaOfLengthBetween(1, 10);
+        final Authentication.AuthenticationType authenticationType = randomFrom(Authentication.AuthenticationType.values());
+        return new Authentication(
+            new User(username, roles, fullName, email, metadata, true),
+            new Authentication.RealmRef(authenticationRealmName, authenticationRealmType, nodeName),
+            new Authentication.RealmRef(lookupRealmName, lookupRealmType, nodeName), Version.CURRENT, authenticationType, metadata);
+    }
+
+    AuthenticateResponse createServerAuthenticationResponse(Authentication authentication){
+        User user = authentication.getUser();
+        org.elasticsearch.client.security.user.User cUser = new org.elasticsearch.client.security.user.User(user.principal(),
+            Arrays.asList(user.roles()), user.metadata(), user.fullName(), user.email());
+        AuthenticateResponse.RealmInfo authenticatedBy = new AuthenticateResponse.RealmInfo(authentication.getAuthenticatedBy().getName(),
+            authentication.getAuthenticatedBy().getType());
+        AuthenticateResponse.RealmInfo lookedUpBy = new AuthenticateResponse.RealmInfo(authentication.getLookedUpBy() == null?
+            authentication.getAuthenticatedBy().getName(): authentication.getLookedUpBy().getName(),
+            authentication.getLookedUpBy() == null?
+                authentication.getAuthenticatedBy().getType(): authentication.getLookedUpBy().getType());
+        return new AuthenticateResponse(cUser, user.enabled(), authenticatedBy, lookedUpBy,
+            authentication.getAuthenticationType().toString().toLowerCase(Locale.ROOT));
     }
 }

+ 3 - 1
docs/java-rest/high-level/security/create-token.asciidoc

@@ -49,6 +49,8 @@ The returned `CreateTokenResponse` contains the following properties:
 `scope`:: The scope of the token. May be `null`.
 `refreshToken`:: A secondary "refresh" token that may be used to extend
  the life of an access token. May be `null`.
+`authentication`:: This is the authentication object for the newly created token. See also
+<<{upid}-authenticate-response, authenticate response>> for details.
 
 ["source","java",subs="attributes,callouts,macros"]
 --------------------------------------------------
@@ -83,4 +85,4 @@ include-tagged::{doc-tests}/SecurityDocumentationIT.java[create-token-execute-li
 --------------------------------------------------
 <1> Called when the execution is successfully completed. The response is
 provided as an argument
-<2> Called in case of failure. The raised exception is provided as an argument
+<2> Called in case of failure. The raised exception is provided as an argument

+ 2 - 0
docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc

@@ -52,6 +52,8 @@ The returned +{response}+ contains the following properties:
 `type`:: The type of the token, this is always `"Bearer"`.
 `expiresIn`:: The length of time (in seconds) until the token will expire.
    The token will be considered invalid after that time.
+`authentication`:: This is the authentication object for the newly created token. See also
+<<{upid}-authenticate-response, authenticate response>> for details.
 
 ["source","java",subs="attributes,callouts,macros"]
 --------------------------------------------------

+ 22 - 1
x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc

@@ -89,7 +89,28 @@ Which returns the following response:
 {
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
-  "expires_in" : 1200
+  "expires_in" : 1200,
+  "authentication" : {
+    "username" : "Elasticsearch Test Client",
+    "roles" : [ ],
+    "full_name" : null,
+    "email" : null,
+    "metadata" : {
+      "pki_dn" : "O=org, OU=Elasticsearch, CN=Elasticsearch Test Client",
+      "pki_delegated_by_user" : "test_admin",
+      "pki_delegated_by_realm" : "file"
+      },
+    "enabled" : true,
+    "authentication_realm" : {
+      "name" : "pki1",
+      "type" : "pki"
+      },
+    "lookup_realm" : {
+      "name" : "pki1",
+      "type" : "pki"
+      },
+    "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]

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

@@ -10,7 +10,7 @@ Creates a bearer token for access without requiring basic authentication.
 [[security-api-get-token-request]]
 ==== {api-request-title}
 
-`POST /_security/oauth2/token` 
+`POST /_security/oauth2/token`
 
 [[security-api-get-token-prereqs]]
 ==== {api-prereq-title}
@@ -22,9 +22,9 @@ Creates a bearer token for access without requiring basic authentication.
 
 The tokens are created by the {es} Token Service, which is automatically enabled
 when you configure TLS on the HTTP interface. See <<tls-http>>. Alternatively,
-you can explicitly enable the `xpack.security.authc.token.enabled` setting. When 
-you are running in production mode, a bootstrap check prevents you from enabling 
-the token service unless you also enable TLS on the HTTP interface. 
+you can explicitly enable the `xpack.security.authc.token.enabled` setting. When
+you are running in production mode, a bootstrap check prevents you from enabling
+the token service unless you also enable TLS on the HTTP interface.
 
 The get token API takes the same parameters as a typical OAuth 2.0 token API
 except for the use of a JSON request body.
@@ -38,7 +38,7 @@ they are valid and after that time period, they can no longer be used. That time
 period is defined by the `xpack.security.authc.token.timeout` setting. For more
 information, see <<token-service-settings>>.
 
-If you want to invalidate a token immediately, you can do so by using the 
+If you want to invalidate a token immediately, you can do so by using the
 <<security-api-invalidate-token,invalidate token API>>.
 
 [[security-api-get-token-request-body]]
@@ -58,9 +58,9 @@ for machine to machine communication and is not suitable or designed for the
 self-service user creation of tokens. It generates only access tokens that
 cannot be refreshed. The premise is that the entity that uses
 `client_credentials` has constant access to a set of (client, not end-user)
-credentials and can authenticate itself at will. 
+credentials and can authenticate itself at will.
 
-`_kerberos`::: 
+`_kerberos`:::
 This grant type is supported internally and implements SPNEGO based Kerberos
 support. The `_kerberos` grant type may change from version to version.
 
@@ -87,7 +87,7 @@ grant type.
 with any other supported grant type.
 
 `refresh_token`::
-(Optional^*^, string) The string that was returned when you created the token, 
+(Optional^*^, string) The string that was returned when you created the token,
 which enables you to extend its life. If you specify the `refresh_token` grant
 type, this parameter is required. This parameter is not valid with any other
 supported grant type.
@@ -97,7 +97,7 @@ supported grant type.
 `FULL` regardless of the value sent with the request.
 
 `username`::
-(Optional^*^, string) The username that identifies the user. If you specify the `password` 
+(Optional^*^, string) The username that identifies the user. If you specify the `password`
 grant type, this parameter is required. This parameter is not valid with any
 other supported grant type.
 
@@ -123,7 +123,26 @@ seconds) that the token expires in, and the type:
 {
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
-  "expires_in" : 1200
+  "expires_in" : 1200,
+  "authentication" : {
+   "username" : "test_admin",
+   "roles" : [
+    "superuser"
+   ],
+  "full_name" : null,
+  "email" : null,
+  "metadata" : { },
+  "enabled" : true,
+  "authentication_realm" : {
+   "name" : "file",
+   "type" : "file"
+  },
+  "lookup_realm" : {
+   "name" : "file",
+   "type" : "file"
+  },
+  "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
@@ -161,7 +180,26 @@ seconds) that the token expires in, the type, and the refresh token:
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
   "expires_in" : 1200,
-  "refresh_token": "vLBPvmAB6KvwvJZr27cS"
+  "refresh_token": "vLBPvmAB6KvwvJZr27cS",
+  "authentication" : {
+   "username" : "test_admin",
+   "roles" : [
+    "superuser"
+    ],
+   "full_name" : null,
+   "email" : null,
+   "metadata" : { },
+   "enabled" : true,
+   "authentication_realm" : {
+    "name" : "file",
+    "type" : "file"
+    },
+   "lookup_realm" : {
+    "name" : "file",
+    "type" : "file"
+   },
+   "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
@@ -183,7 +221,7 @@ POST /_security/oauth2/token
 // TEST[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
 // TEST[continued]
 
-The API will return a new token and refresh token. Each refresh token may only 
+The API will return a new token and refresh token. Each refresh token may only
 be used one time.
 
 [source,console-result]
@@ -192,7 +230,26 @@ be used one time.
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
   "expires_in" : 1200,
-  "refresh_token": "vLBPvmAB6KvwvJZr27cS"
+  "refresh_token": "vLBPvmAB6KvwvJZr27cS",
+  "authentication" : {
+   "username" : "test_admin",
+   "roles" : [
+    "superuser"
+    ],
+   "full_name" : null,
+   "email" : null,
+   "metadata" : { },
+   "enabled" : true,
+   "authentication_realm" : {
+    "name" : "file",
+    "type" : "file"
+    },
+   "lookup_realm" : {
+    "name" : "file",
+    "type" : "file"
+    },
+   "authentication_type" : "token"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
@@ -223,7 +280,26 @@ Each refresh token may only be used one time. When the mutual authentication is
   "type" : "Bearer",
   "expires_in" : 1200,
   "refresh_token": "vLBPvmAB6KvwvJZr27cS"
-  "kerberos_authentication_response_token": "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAg"
+  "kerberos_authentication_response_token": "YIIB6wYJKoZIhvcSAQICAQBuggHaMIIB1qADAg",
+  "authentication" : {
+   "username" : "test_admin",
+   "roles" : [
+    "superuser"
+    ],
+   "full_name" : null,
+   "email" : null,
+   "metadata" : { },
+   "enabled" : true,
+   "authentication_realm" : {
+    "name" : "file",
+    "type" : "file"
+    },
+   "lookup_realm" : {
+    "name" : "file",
+    "type" : "file"
+   },
+   "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
-// NOTCONSOLE
+// NOTCONSOLE

+ 44 - 6
x-pack/docs/en/rest-api/security/invalidate-tokens.asciidoc

@@ -16,9 +16,9 @@ Invalidates one or more access tokens or refresh tokens.
 ==== {api-description-title}
 
 The access tokens returned by the <<security-api-get-token,get token API>> have a
-finite period of time for which they are valid and after that time period, they 
-can no longer be used. That time period is defined by the 
-`xpack.security.authc.token.timeout` setting. For more information, see 
+finite period of time for which they are valid and after that time period, they
+can no longer be used. That time period is defined by the
+`xpack.security.authc.token.timeout` setting. For more information, see
 <<token-service-settings>>.
 
 The refresh tokens returned by the <<security-api-get-token,get token API>> are
@@ -83,7 +83,26 @@ The get token API returns the following information about the access token:
 {
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
-  "expires_in" : 1200
+  "expires_in" : 1200,
+  "authentication" : {
+    "username" : "test_admin",
+    "roles" : [
+      "superuser"
+      ],
+    "full_name" : null,
+    "email" : null,
+    "metadata" : { },
+    "enabled" : true,
+    "authentication_realm" : {
+      "name" : "file",
+      "type" : "file"
+      },
+    "lookup_realm" : {
+      "name" : "file",
+      "type" : "file"
+      },
+    "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
@@ -122,13 +141,32 @@ The get token API returns the following information:
   "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
   "type" : "Bearer",
   "expires_in" : 1200,
-  "refresh_token": "vLBPvmAB6KvwvJZr27cS"
+  "refresh_token": "vLBPvmAB6KvwvJZr27cS",
+  "authentication" : {
+    "username" : "test_admin",
+    "roles" : [
+      "superuser"
+      ],
+    "full_name" : null,
+    "email" : null,
+    "metadata" : { },
+    "enabled" : true,
+    "authentication_realm" : {
+      "name" : "file",
+      "type" : "file"
+      },
+    "lookup_realm" : {
+      "name" : "file",
+      "type" : "file"
+      },
+    "authentication_type" : "realm"
+  }
 }
 --------------------------------------------------
 // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
 // TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
 
-The refresh token can now also be immediately invalidated as shown 
+The refresh token can now also be immediately invalidated as shown
 in the following example:
 
 [source,console]

+ 26 - 25
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java

@@ -6,14 +6,15 @@
 
 package org.elasticsearch.xpack.core.security.action;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -26,39 +27,28 @@ public final class DelegatePkiAuthenticationResponse extends ActionResponse impl
     private static final ParseField ACCESS_TOKEN_FIELD = new ParseField("access_token");
     private static final ParseField TYPE_FIELD = new ParseField("type");
     private static final ParseField EXPIRES_IN_FIELD = new ParseField("expires_in");
-
-    public static final ConstructingObjectParser<DelegatePkiAuthenticationResponse, Void> PARSER = new ConstructingObjectParser<>(
-            "delegate_pki_response", true, a -> {
-                final String accessToken = (String) a[0];
-                final String type = (String) a[1];
-                if (false == "Bearer".equals(type)) {
-                    throw new IllegalArgumentException("Unknown token type [" + type + "], only [Bearer] type permitted");
-                }
-                final Long expiresIn = (Long) a[2];
-                return new DelegatePkiAuthenticationResponse(accessToken, TimeValue.timeValueSeconds(expiresIn));
-            });
-
-    static {
-        PARSER.declareString(ConstructingObjectParser.constructorArg(), ACCESS_TOKEN_FIELD);
-        PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD);
-        PARSER.declareLong(ConstructingObjectParser.constructorArg(), EXPIRES_IN_FIELD);
-    }
+    private static final ParseField AUTHENTICATION = new ParseField("authentication");
 
     private String accessToken;
     private TimeValue expiresIn;
+    private Authentication authentication;
 
     DelegatePkiAuthenticationResponse() { }
 
-    public DelegatePkiAuthenticationResponse(String accessToken, TimeValue expiresIn) {
+    public DelegatePkiAuthenticationResponse(String accessToken, TimeValue expiresIn, Authentication authentication) {
         this.accessToken = Objects.requireNonNull(accessToken);
         // always store expiration in seconds because this is how we "serialize" to JSON and we need to parse back
         this.expiresIn = TimeValue.timeValueSeconds(Objects.requireNonNull(expiresIn).getSeconds());
+        this.authentication = authentication;
     }
 
     public DelegatePkiAuthenticationResponse(StreamInput input) throws IOException {
         super(input);
         accessToken = input.readString();
         expiresIn = input.readTimeValue();
+        if (input.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication = new Authentication(input);
+        }
     }
 
     public String getAccessToken() {
@@ -69,10 +59,17 @@ public final class DelegatePkiAuthenticationResponse extends ActionResponse impl
         return expiresIn;
     }
 
+    public Authentication getAuthentication() {
+        return authentication;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(accessToken);
         out.writeTimeValue(expiresIn);
+        if (out.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication.writeTo(out);
+        }
     }
 
     @Override
@@ -81,20 +78,24 @@ public final class DelegatePkiAuthenticationResponse extends ActionResponse impl
         if (o == null || getClass() != o.getClass()) return false;
         DelegatePkiAuthenticationResponse that = (DelegatePkiAuthenticationResponse) o;
         return Objects.equals(accessToken, that.accessToken) &&
-            Objects.equals(expiresIn, that.expiresIn);
+            Objects.equals(expiresIn, that.expiresIn) &&
+            Objects.equals(authentication, that.authentication);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(accessToken, expiresIn);
+        return Objects.hash(accessToken, expiresIn, authentication);
     }
 
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject()
-            .field(ACCESS_TOKEN_FIELD.getPreferredName(), accessToken)
-            .field(TYPE_FIELD.getPreferredName(), "Bearer")
-            .field(EXPIRES_IN_FIELD.getPreferredName(), expiresIn.getSeconds());
+        builder.startObject();
+        builder.field(ACCESS_TOKEN_FIELD.getPreferredName(), accessToken);
+        builder.field(TYPE_FIELD.getPreferredName(), "Bearer");
+        builder.field(EXPIRES_IN_FIELD.getPreferredName(), expiresIn.getSeconds());
+        if (authentication != null) {
+            builder.field(AUTHENTICATION.getPreferredName(), authentication);
+        }
         return builder.endObject();
     }
 }

+ 15 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateResponse.java

@@ -5,10 +5,12 @@
  */
 package org.elasticsearch.xpack.core.security.action.oidc;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 
 import java.io.IOException;
 
@@ -17,12 +19,15 @@ public class OpenIdConnectAuthenticateResponse extends ActionResponse {
     private String accessTokenString;
     private String refreshTokenString;
     private TimeValue expiresIn;
+    private Authentication authentication;
 
-    public OpenIdConnectAuthenticateResponse(String principal, String accessTokenString, String refreshTokenString, TimeValue expiresIn) {
-        this.principal = principal;
+    public OpenIdConnectAuthenticateResponse(Authentication authentication, String accessTokenString, String refreshTokenString,
+                                             TimeValue expiresIn) {
+        this.principal = authentication.getUser().principal();;
         this.accessTokenString = accessTokenString;
         this.refreshTokenString = refreshTokenString;
         this.expiresIn = expiresIn;
+        this.authentication = authentication;
     }
 
     public OpenIdConnectAuthenticateResponse(StreamInput in) throws IOException {
@@ -31,6 +36,9 @@ public class OpenIdConnectAuthenticateResponse extends ActionResponse {
         accessTokenString = in.readString();
         refreshTokenString = in.readString();
         expiresIn = in.readTimeValue();
+        if (in.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication = new Authentication(in);
+        }
     }
 
     public String getPrincipal() {
@@ -49,11 +57,16 @@ public class OpenIdConnectAuthenticateResponse extends ActionResponse {
         return expiresIn;
     }
 
+    public Authentication getAuthentication() { return authentication; }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(principal);
         out.writeString(accessTokenString);
         out.writeString(refreshTokenString);
         out.writeTimeValue(expiresIn);
+        if (out.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication.writeTo(out);
+        }
     }
 }

+ 14 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateResponse.java

@@ -10,6 +10,7 @@ import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 
 import java.io.IOException;
 
@@ -24,6 +25,7 @@ public final class SamlAuthenticateResponse extends ActionResponse {
     private String refreshToken;
     private String realm;
     private TimeValue expiresIn;
+    private Authentication authentication;
 
     public SamlAuthenticateResponse(StreamInput in) throws IOException {
         super(in);
@@ -34,14 +36,18 @@ public final class SamlAuthenticateResponse extends ActionResponse {
         tokenString = in.readString();
         refreshToken = in.readString();
         expiresIn = in.readTimeValue();
+        if (in.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication = new Authentication(in);
+        }
     }
 
-    public SamlAuthenticateResponse(String principal, String realm, String tokenString, String refreshToken, TimeValue expiresIn) {
-        this.principal = principal;
-        this.realm = realm;
+    public SamlAuthenticateResponse(Authentication authentication, String tokenString, String refreshToken, TimeValue expiresIn) {
+        this.principal = authentication.getUser().principal();
+        this.realm = authentication.getAuthenticatedBy().getName();
         this.tokenString = tokenString;
         this.refreshToken = refreshToken;
         this.expiresIn = expiresIn;
+        this.authentication = authentication;
     }
 
     public String getPrincipal() {
@@ -64,6 +70,8 @@ public final class SamlAuthenticateResponse extends ActionResponse {
         return expiresIn;
     }
 
+    public Authentication getAuthentication() { return authentication; }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(principal);
@@ -73,6 +81,9 @@ public final class SamlAuthenticateResponse extends ActionResponse {
         out.writeString(tokenString);
         out.writeString(refreshToken);
         out.writeTimeValue(expiresIn);
+        if (out.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication.writeTo(out);
+        }
     }
 
     }

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

@@ -5,12 +5,14 @@
  */
 package org.elasticsearch.xpack.core.security.action.token;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -27,6 +29,7 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
     private String scope;
     private String refreshToken;
     private String kerberosAuthenticationResponseToken;
+    private Authentication authentication;
 
     CreateTokenResponse() {}
 
@@ -37,15 +40,19 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
         scope = in.readOptionalString();
         refreshToken = in.readOptionalString();
         kerberosAuthenticationResponseToken = in.readOptionalString();
+        if (in.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication = new Authentication(in);
+        }
     }
 
     public CreateTokenResponse(String tokenString, TimeValue expiresIn, String scope, String refreshToken,
-                               String kerberosAuthenticationResponseToken) {
+                               String kerberosAuthenticationResponseToken, Authentication authentication) {
         this.tokenString = Objects.requireNonNull(tokenString);
         this.expiresIn = Objects.requireNonNull(expiresIn);
         this.scope = scope;
         this.refreshToken = refreshToken;
         this.kerberosAuthenticationResponseToken = kerberosAuthenticationResponseToken;
+        this.authentication = authentication;
     }
 
     public String getTokenString() {
@@ -68,6 +75,8 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
         return kerberosAuthenticationResponseToken;
     }
 
+    public Authentication getAuthentication() { return authentication; }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         out.writeString(tokenString);
@@ -75,6 +84,9 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
         out.writeOptionalString(scope);
         out.writeOptionalString(refreshToken);
         out.writeOptionalString(kerberosAuthenticationResponseToken);
+        if (out.getVersion().onOrAfter(Version.V_7_11_0)) {
+            authentication.writeTo(out);
+        }
     }
 
     @Override
@@ -93,6 +105,9 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
         if (kerberosAuthenticationResponseToken != null) {
             builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
         }
+        if (authentication != null) {
+            builder.field("authentication", authentication);
+        }
         return builder.endObject();
     }
 
@@ -105,11 +120,13 @@ public final class CreateTokenResponse extends ActionResponse implements ToXCont
             Objects.equals(expiresIn, that.expiresIn) &&
             Objects.equals(scope, that.scope) &&
             Objects.equals(refreshToken, that.refreshToken) &&
-            Objects.equals(kerberosAuthenticationResponseToken,  that.kerberosAuthenticationResponseToken);
+            Objects.equals(kerberosAuthenticationResponseToken,  that.kerberosAuthenticationResponseToken) &&
+            Objects.equals(authentication, that.authentication);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(tokenString, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken);
+        return Objects.hash(tokenString, expiresIn, scope, refreshToken, kerberosAuthenticationResponseToken,
+            authentication);
     }
 }

+ 96 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java

@@ -6,15 +6,26 @@
 
 package org.elasticsearch.xpack.core.action;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.test.AbstractXContentTestCase;
 import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.core.security.user.User;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 import static org.hamcrest.Matchers.is;
 
 public class DelegatePkiAuthenticationResponseTests extends AbstractXContentTestCase<DelegatePkiAuthenticationResponse> {
@@ -27,6 +38,7 @@ public class DelegatePkiAuthenticationResponseTests extends AbstractXContentTest
                 DelegatePkiAuthenticationResponse serialized = new DelegatePkiAuthenticationResponse(input);
                 assertThat(response.getAccessToken(), is(serialized.getAccessToken()));
                 assertThat(response.getExpiresIn(), is(serialized.getExpiresIn()));
+                assertThat(response.getAuthentication(), is(serialized.getAuthentication()));
                 assertThat(response, is(serialized));
             }
         }
@@ -35,16 +47,97 @@ public class DelegatePkiAuthenticationResponseTests extends AbstractXContentTest
     @Override
     protected DelegatePkiAuthenticationResponse createTestInstance() {
         return new DelegatePkiAuthenticationResponse(randomAlphaOfLengthBetween(0, 10),
-                TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"));
+                TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"),
+                createAuthentication());
     }
 
     @Override
     protected DelegatePkiAuthenticationResponse doParseInstance(XContentParser parser) throws IOException {
-        return DelegatePkiAuthenticationResponse.PARSER.apply(parser, null);
+        return DelegatePkiAuthenticationResponseTests.PARSER.apply(parser, null);
     }
 
     @Override
     protected boolean supportsUnknownFields() {
-        return true;
+        return false;
+    }
+
+    private static final ParseField ACCESS_TOKEN_FIELD = new ParseField("access_token");
+    private static final ParseField TYPE_FIELD = new ParseField("type");
+    private static final ParseField EXPIRES_IN_FIELD = new ParseField("expires_in");
+    private static final ParseField AUTHENTICATION = new ParseField("authentication");
+
+    public static final ConstructingObjectParser<DelegatePkiAuthenticationResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "delegate_pki_response", true, a -> {
+        final String accessToken = (String) a[0];
+        final String type = (String) a[1];
+        if (false == "Bearer".equals(type)) {
+            throw new IllegalArgumentException("Unknown token type [" + type + "], only [Bearer] type permitted");
+        }
+        final Long expiresIn = (Long) a[2];
+        final Authentication authentication = (Authentication) a[3];
+
+        return new DelegatePkiAuthenticationResponse(accessToken, TimeValue.timeValueSeconds(expiresIn), authentication);
+    });
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), ACCESS_TOKEN_FIELD);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD);
+        PARSER.declareLong(ConstructingObjectParser.constructorArg(), EXPIRES_IN_FIELD);
+        PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseAuthentication(p), AUTHENTICATION);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<Authentication, Void> AUTH_PARSER = new ConstructingObjectParser<>(
+        "authentication", true,
+        a -> new Authentication(new User((String) a[0], ((ArrayList<String>) a[1]).toArray(new String[0]), (String) a[2], (String) a[3],
+            (Map<String, Object>) a[4], (boolean) a[5]), (Authentication.RealmRef) a[6], (Authentication.RealmRef) a[7], Version.CURRENT,
+            Authentication.AuthenticationType.valueOf(a[8].toString().toUpperCase(Locale.ROOT)), (Map<String, Object>) a[4]));
+    static {
+        final ConstructingObjectParser<Authentication.RealmRef, Void> realmInfoParser = new ConstructingObjectParser<>("realm_info", true,
+            a -> new Authentication.RealmRef((String) a[0], (String) a[1], "node_name"));
+        realmInfoParser.declareString(ConstructingObjectParser.constructorArg(), User.Fields.REALM_NAME);
+        realmInfoParser.declareString(ConstructingObjectParser.constructorArg(), User.Fields.REALM_TYPE);
+        AUTH_PARSER.declareString(ConstructingObjectParser.constructorArg(), User.Fields.USERNAME);
+        AUTH_PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), User.Fields.ROLES);
+        AUTH_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), User.Fields.FULL_NAME);
+        AUTH_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), User.Fields.EMAIL);
+        AUTH_PARSER.declareObject(ConstructingObjectParser.constructorArg(), (parser, c) -> parser.map(), User.Fields.METADATA);
+        AUTH_PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), User.Fields.ENABLED);
+        AUTH_PARSER.declareObject(ConstructingObjectParser.constructorArg(), realmInfoParser, User.Fields.AUTHENTICATION_REALM);
+        AUTH_PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), realmInfoParser, User.Fields.LOOKUP_REALM);
+        AUTH_PARSER.declareString(ConstructingObjectParser.constructorArg(), User.Fields.AUTHENTICATION_TYPE);
+    }
+
+    public static Authentication parseAuthentication(final XContentParser parser) throws IOException {
+        return AUTH_PARSER.apply(parser, null);
+    }
+
+    public static Authentication createAuthentication() {
+        final String username = randomAlphaOfLengthBetween(1, 4);
+        final String[] roles = generateRandomStringArray(4, 4, false, true);
+        final Map<String, Object> metadata;
+        metadata = new HashMap<>();
+        if (randomBoolean()) {
+            metadata.put("string", null);
+        } else {
+            metadata.put("string", randomAlphaOfLengthBetween(0, 4));
+        }
+        if (randomBoolean()) {
+            metadata.put("string_list", null);
+        } else {
+            metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true)));
+        }
+        final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
+        final String authenticationRealmName = randomAlphaOfLength(5);
+        final String authenticationRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos");
+        final String lookupRealmName = randomAlphaOfLength(5);
+        final String lookupRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos");
+        final String nodeName = "node_name";
+        final Authentication.AuthenticationType authenticationType = randomFrom(Authentication.AuthenticationType.values());
+        return new Authentication(
+            new User(username, roles, fullName, email, metadata, true),
+            new Authentication.RealmRef(authenticationRealmName, authenticationRealmType, nodeName),
+            new Authentication.RealmRef(lookupRealmName, lookupRealmType, nodeName), Version.CURRENT, authenticationType, metadata);
     }
 }

+ 8 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java

@@ -9,12 +9,16 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.core.security.user.User;
 
 public class CreateTokenResponseTests extends ESTestCase {
 
     public void testSerialization() throws Exception {
         CreateTokenResponse response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L),
-            randomBoolean() ? null : "FULL", randomAlphaOfLengthBetween(1, 10), randomBoolean() ? null :randomAlphaOfLengthBetween(1, 10));
+            randomBoolean() ? null : "FULL", randomAlphaOfLengthBetween(1, 10), randomBoolean() ? null :randomAlphaOfLengthBetween(1, 10),
+            new Authentication(new User("joe", new String[]{"custom_superuser"}, new User("bar", "not_superuser")),
+                new Authentication.RealmRef("test", "test", "node"), new Authentication.RealmRef("test", "test", "node")));
         try (BytesStreamOutput output = new BytesStreamOutput()) {
             response.writeTo(output);
             try (StreamInput input = output.bytes().streamInput()) {
@@ -24,7 +28,9 @@ public class CreateTokenResponseTests extends ESTestCase {
         }
 
         response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L),
-            randomBoolean() ? null : "FULL", null, null);
+            randomBoolean() ? null : "FULL", null, null,
+            new Authentication(new User("joe", new String[]{"custom_superuser"}, new User("bar", "not_superuser")),
+                new Authentication.RealmRef("test", "test", "node"), new Authentication.RealmRef("test", "test", "node")));
         try (BytesStreamOutput output = new BytesStreamOutput()) {
             response.writeTo(output);
             try (StreamInput input = output.bytes().streamInput()) {

+ 3 - 0
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java

@@ -104,6 +104,7 @@ public class TokenAuthIntegTests extends SecurityIntegTestCase {
         PlainActionFuture<UserToken> userTokenFuture = new PlainActionFuture<>();
         tokenService.decodeToken(response.getAccessToken(), userTokenFuture);
         assertNotNull(userTokenFuture.actionGet());
+        assertNotNull(response.getAuthenticationResponse());
     }
 
 
@@ -133,6 +134,7 @@ public class TokenAuthIntegTests extends SecurityIntegTestCase {
             assertNotNull(userTokenFuture.actionGet());
             assertNotEquals(activeKeyHash, tokenService.getActiveKeyHash());
         }
+        assertNotNull(response.getAuthenticationResponse());
     }
 
     public void testExpiredTokensDeletedAfterExpiration() throws Exception {
@@ -365,6 +367,7 @@ public class TokenAuthIntegTests extends SecurityIntegTestCase {
 
         // Assert that we can authenticate with the refreshed access token
         assertAuthenticateWithToken(refreshResponse.getAccessToken(), SecuritySettingsSource.TEST_USER_NAME);
+        assertNotNull(refreshResponse.getAuthenticationResponse());
     }
 
     public void testRefreshingInvalidatedToken() throws IOException {

+ 3 - 0
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java

@@ -149,6 +149,8 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
                         optionsBuilder.build());
                 String token = delegatePkiResponse.getAccessToken();
                 assertThat(token, is(notNullValue()));
+                assertNotNull(delegatePkiResponse.getAuthenticationResponse());
+
                 // authenticate
                 optionsBuilder = RequestOptions.DEFAULT.toBuilder();
                 optionsBuilder.addHeader("Authorization", "Bearer " + token);
@@ -187,6 +189,7 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
                     optionsBuilder.build());
             String token = delegatePkiResponse.getAccessToken();
             assertThat(token, is(notNullValue()));
+            assertNotNull(delegatePkiResponse.getAuthenticationResponse());
             // authenticate
             optionsBuilder = RequestOptions.DEFAULT.toBuilder();
             optionsBuilder.addHeader("Authorization", "Bearer " + token);

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java

@@ -84,7 +84,7 @@ public final class TransportDelegatePkiAuthenticationAction
                         tokenService.createOAuth2Tokens(authentication, delegateeAuthentication, Map.of(), false,
                                 ActionListener.wrap(tuple -> {
                                     final TimeValue expiresIn = tokenService.getExpirationDelay();
-                                    listener.onResponse(new DelegatePkiAuthenticationResponse(tuple.v1(), expiresIn));
+                                    listener.onResponse(new DelegatePkiAuthenticationResponse(tuple.v1(), expiresIn, authentication));
                                 }, listener::onFailure));
                     }, e -> {
                         logger.debug((Supplier<?>) () -> new ParameterizedMessage("Delegated x509Token [{}] could not be authenticated",

+ 1 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java

@@ -74,8 +74,7 @@ public class TransportOpenIdConnectAuthenticateAction
                     tokenService.createOAuth2Tokens(authentication, originatingAuthentication, tokenMetadata, true,
                         ActionListener.wrap(tuple -> {
                             final TimeValue expiresIn = tokenService.getExpirationDelay();
-                            listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication.getUser().principal(), tuple.v1(),
-                                tuple.v2(), expiresIn));
+                            listener.onResponse(new OpenIdConnectAuthenticateResponse(authentication, tuple.v1(), tuple.v2(), expiresIn));
                         }, listener::onFailure));
                 }, e -> {
                     logger.debug(() -> new ParameterizedMessage("OpenIDConnectToken [{}] could not be authenticated", token), e);

+ 1 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java

@@ -68,8 +68,7 @@ public final class TransportSamlAuthenticateAction extends HandledTransportActio
                         tokenMeta, true, ActionListener.wrap(tuple -> {
                             final TimeValue expiresIn = tokenService.getExpirationDelay();
                             listener.onResponse(
-                                new SamlAuthenticateResponse(authentication.getUser().principal(),
-                                    authentication.getAuthenticatedBy().getName(), tuple.v1(), tuple.v2(), expiresIn));
+                                new SamlAuthenticateResponse(authentication, tuple.v1(), tuple.v2(), expiresIn));
                         }, listener::onFailure));
             }, e -> {
                 logger.debug(() -> new ParameterizedMessage("SamlToken [{}] could not be authenticated", saml), e);

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java

@@ -135,7 +135,7 @@ public final class TransportCreateTokenAction extends HandledTransportAction<Cre
                     final String scope = getResponseScopeValue(request.getScope());
                     final String base64AuthenticateResponse = (grantType == GrantType.KERBEROS) ? extractOutToken() : null;
                     final CreateTokenResponse response = new CreateTokenResponse(tuple.v1(), tokenService.getExpirationDelay(), scope,
-                            tuple.v2(), base64AuthenticateResponse);
+                            tuple.v2(), base64AuthenticateResponse, authentication);
                     listener.onResponse(response);
                 }, listener::onFailure));
     }

+ 6 - 3
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportRefreshTokenAction.java

@@ -9,6 +9,7 @@ import org.elasticsearch.action.ActionListener;
 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.tasks.Task;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest;
@@ -32,9 +33,11 @@ public class TransportRefreshTokenAction extends HandledTransportAction<CreateTo
     protected void doExecute(Task task, CreateTokenRequest request, ActionListener<CreateTokenResponse> listener) {
         tokenService.refreshToken(request.getRefreshToken(), ActionListener.wrap(tuple -> {
             final String scope = getResponseScopeValue(request.getScope());
-            final CreateTokenResponse response =
-                    new CreateTokenResponse(tuple.v1(), tokenService.getExpirationDelay(), scope, tuple.v2(), null);
-            listener.onResponse(response);
+            tokenService.authenticateToken(new SecureString(tuple.v1()), ActionListener.wrap(authentication -> {
+                listener.onResponse(new CreateTokenResponse(tuple.v1(), tokenService.getExpirationDelay(), scope, tuple.v2(), null,
+                    authentication));
+            },
+                listener::onFailure));
         }, listener::onFailure));
     }
 }

+ 7 - 6
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java

@@ -65,12 +65,13 @@ public class RestOpenIdConnectAuthenticateAction extends OpenIdConnectBaseRestHa
                     @Override
                     public RestResponse buildResponse(OpenIdConnectAuthenticateResponse response, XContentBuilder builder)
                         throws Exception {
-                        builder.startObject()
-                            .field("username", response.getPrincipal())
-                            .field("access_token", response.getAccessTokenString())
-                            .field("refresh_token", response.getRefreshTokenString())
-                            .field("expires_in", response.getExpiresIn().seconds())
-                            .endObject();
+                        builder.startObject();
+                        builder.field("username", response.getPrincipal());
+                        builder.field("access_token", response.getAccessTokenString());
+                        builder.field("refresh_token", response.getRefreshTokenString());
+                        builder.field("expires_in", response.getExpiresIn().seconds());
+                        builder.field("authentication", response.getAuthentication());
+                        builder.endObject();
                         return new BytesRestResponse(RestStatus.OK, builder);
                     }
                 });

+ 8 - 7
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java

@@ -97,13 +97,14 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R
                 requestBuilder.execute(new RestBuilderListener<>(channel) {
                     @Override
                     public RestResponse buildResponse(SamlAuthenticateResponse response, XContentBuilder builder) throws Exception {
-                        builder.startObject()
-                                .field("username", response.getPrincipal())
-                                .field("realm", response.getRealm())
-                                .field("access_token", response.getTokenString())
-                                .field("refresh_token", response.getRefreshToken())
-                                .field("expires_in", response.getExpiresIn().seconds())
-                                .endObject();
+                        builder.startObject();
+                        builder.field("username", response.getPrincipal());
+                        builder.field("realm", response.getRealm());
+                        builder.field("access_token", response.getTokenString());
+                        builder.field("refresh_token", response.getRefreshToken());
+                        builder.field("expires_in", response.getExpiresIn().seconds());
+                        builder.field("authentication", response.getAuthentication());
+                        builder.endObject();
                         return new BytesRestResponse(RestStatus.OK, builder);
                     }
                 });

+ 10 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenActionTests.java

@@ -23,7 +23,9 @@ import org.elasticsearch.test.SecuritySettingsSourceField;
 import org.elasticsearch.test.rest.FakeRestRequest;
 import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest;
 import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.support.NoOpLogger;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken;
 import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction.CreateTokenResponseActionListener;
 
@@ -31,6 +33,7 @@ import java.util.Locale;
 import java.util.Map;
 
 import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasKey;
 
 public class RestGetTokenActionTests extends ESTestCase {
 
@@ -71,7 +74,9 @@ public class RestGetTokenActionTests extends ESTestCase {
         CreateTokenResponseActionListener listener = new CreateTokenResponseActionListener(restChannel, restRequest, NoOpLogger.INSTANCE);
         CreateTokenResponse createTokenResponse =
                 new CreateTokenResponse(randomAlphaOfLengthBetween(1, 256), TimeValue.timeValueHours(1L), null, randomAlphaOfLength(4),
-                        randomAlphaOfLength(5));
+                        randomAlphaOfLength(5), new Authentication(new User("joe", new String[]{"custom_superuser"},
+                    new User("bar", "not_superuser")), new Authentication.RealmRef("test", "test", "node"),
+                    new Authentication.RealmRef("test", "test", "node")));
         listener.onResponse(createTokenResponse);
 
         RestResponse response = responseSetOnce.get();
@@ -85,7 +90,10 @@ public class RestGetTokenActionTests extends ESTestCase {
         assertThat(map, hasEntry("expires_in", Math.toIntExact(createTokenResponse.getExpiresIn().seconds())));
         assertThat(map, hasEntry("refresh_token", createTokenResponse.getRefreshToken()));
         assertThat(map, hasEntry("kerberos_authentication_response_token", createTokenResponse.getKerberosAuthenticationResponseToken()));
-        assertEquals(5, map.size());
+        assertThat(map, hasKey("authentication"));
+        assertThat((Map<String, Object>)(map.get("authentication")),
+            hasEntry("username", createTokenResponse.getAuthentication().getUser().principal()));
+        assertEquals(6, map.size());
     }
 
     public void testSendResponseKerberosError() {

+ 1 - 0
x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java

@@ -420,6 +420,7 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
             logger.info(" OpenIDConnect authentication response {}", responseBody);
             assertNotNull(responseBody.get("access_token"));
             assertNotNull(responseBody.get("refresh_token"));
+            assertNotNull(responseBody.get("authentication"));
             return Tuple.tuple(responseBody.get("access_token").toString(), responseBody.get("refresh_token").toString());
         }
     }

+ 4 - 0
x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java

@@ -204,6 +204,10 @@ public class SamlAuthenticationIT extends ESRestTestCase {
             assertThat(refreshToken, notNullValue());
             assertThat(refreshToken, instanceOf(String.class));
 
+            final Object authentication = result.get("authentication");
+            assertThat(authentication, notNullValue());
+            assertThat(authentication, instanceOf(Map.class));
+
             return new Tuple<>((String) accessToken, (String) refreshToken);
         }
     }