Skip to content

Commit

Permalink
Adding authentication information to access token create APIs (#62490)
Browse files Browse the repository at this point in the history
* 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 51dbd9e)

* 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
  • Loading branch information
BigPandaToo authored Oct 16, 2020
1 parent 2b4bde4 commit 2351bb3
Show file tree
Hide file tree
Showing 28 changed files with 500 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
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;
Expand All @@ -38,7 +40,7 @@
* 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");
Expand Down Expand Up @@ -123,6 +125,27 @@ public String getAuthenticationType() {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -77,6 +79,8 @@ public String getKerberosAuthenticationResponseToken() {
return kerberosAuthenticationResponseToken;
}

public AuthenticateResponse getAuthenticationResponse() { return authenticationResponse; }

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -91,17 +95,19 @@ public boolean equals(Object o) {
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"));
Expand All @@ -110,6 +116,7 @@ public int hashCode() {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -53,6 +56,8 @@ public TimeValue getExpiresIn() {
return expiresIn;
}

public AuthenticateResponse getAuthenticationResponse() { return authenticationResponse; }

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -64,22 +69,26 @@ public boolean equals(Object o) {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@
import org.elasticsearch.test.ESTestCase;

import java.io.IOException;
import java.util.Arrays;

import static org.hamcrest.Matchers.equalTo;

Expand All @@ -38,6 +40,10 @@ public void testFromXContent() throws IOException {
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);
Expand All @@ -54,6 +60,7 @@ public void testFromXContent() throws IOException {
if (kerberosAuthenticationResponseToken != null) {
builder.field("kerberos_authentication_response_token", kerberosAuthenticationResponseToken);
}
builder.field("authentication", authenticateResponse);
builder.endObject();
BytesReference xContent = BytesReference.bytes(builder);

Expand All @@ -64,5 +71,6 @@ public void testFromXContent() throws IOException {
assertThat(response.getType(), equalTo(type));
assertThat(response.getExpiresIn(), equalTo(expiresIn));
assertThat(response.getKerberosAuthenticationResponseToken(), equalTo(kerberosAuthenticationResponseToken));
assertThat(response.getAuthenticationResponse(), equalTo(authenticateResponse));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -51,5 +59,52 @@ protected void assertInstances(org.elasticsearch.xpack.core.security.action.Dele
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));
}
}
4 changes: 3 additions & 1 deletion docs/java-rest/high-level/security/create-token.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
--------------------------------------------------
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
--------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/]
Loading

0 comments on commit 2351bb3

Please sign in to comment.