Allow Transport Actions to indicate authN realm (#45946)

This commit allows the Transport Actions for the SSO realms to
indicate the realm that should be used to authenticate the
constructed AuthenticationToken. This is useful in the case that
many authentication realms of the same type have been configured
and where the caller of the API(Kibana or a custom web app) already
know which realm should be used so there is no need to iterate all
the realms of the same type.
The realm parameter is added in the relevant REST APIs as optional
so as not to introduce any breaking change.
This commit is contained in:
Ioannis Kakavas 2019-08-25 19:36:41 +03:00 committed by GitHub
parent 040a810b3c
commit 2bee27dd54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 340 additions and 108 deletions

View File

@ -179,8 +179,8 @@ task verifyVersions {
* after the backport of the backcompat code is complete.
*/
boolean bwc_tests_enabled = true
final String bwc_tests_disabled_issue = "" /* place a PR link here when committing bwc changes */
boolean bwc_tests_enabled = false
final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/45767"
if (bwc_tests_enabled == false) {
if (bwc_tests_disabled_issue.isEmpty()) {
throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")

View File

@ -31,24 +31,28 @@ and <<security-api-oidc-logout,OpenID Connect logout API>>
==== {api-request-body-title}
`redirect_uri`::
The URL to which the OpenID Connect Provider redirected the User Agent in
(Required, string) The URL to which the OpenID Connect Provider redirected the User Agent in
response to an authentication request, after a successful authentication. This
URL is expected to be provided as-is (URL encoded), taken from the body of the
response or as the value of a `Location` header in the response from the OpenID
Connect Provider.
`state`::
String value used to maintain state between the authentication request and the
(Required, string) Used to maintain state between the authentication request and the
response. This value needs to be the same as the one that was provided to the
call to `/_security/oidc/prepare` earlier, or the one that was generated by {es}
and included in the response to that call.
`nonce`::
String value used to associate a Client session with an ID Token and to mitigate
(Required, string) Used to associate a Client session with an ID Token and to mitigate
replay attacks. This value needs to be the same as the one that was provided to
the call to `/_security/oidc/prepare` earlier, or the one that was generated by
{es} and included in the response to that call.
`realm`::
(Optional, string) Used to identify the name of the OpenID Connect realm that should
be used to authenticate this. Useful when multiple realms have been defined.
[[security-api-oidc-authenticate-example]]
==== {api-examples-title}
@ -63,7 +67,8 @@ POST /_security/oidc/authenticate
{
"redirect_uri" : "https://oidc-kibana.elastic.co:5603/api/security/v1/oidc?code=jtI3Ntt8v3_XvcLzCFGq&state=4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"state" : "4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM"
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM",
"realm" : "oidc1"
}
--------------------------------------------------
// CONSOLE

View File

@ -29,10 +29,10 @@ and
==== {api-request-body-title}
`access_token`::
The value of the access token to be invalidated as part of the logout.
(Required, string) The value of the access token to be invalidated as part of the logout.
`refresh_token`::
(Optional) The value of the refresh token to be invalidated as part of the logout.
(Optional, string) The value of the refresh token to be invalidated as part of the logout.
[[security-api-oidc-logout-example]]

View File

@ -33,28 +33,28 @@ and <<security-api-oidc-logout,OpenID Connect logout API>>.
The following parameters can be specified in the body of the request:
`realm`::
The name of the OpenID Connect realm in {es} the configuration of which should
(Optional, string) The name of the OpenID Connect realm in {es} the configuration of which should
be used in order to generate the authentication request. Cannot be specified
when `iss` is specified.
when `iss` is specified. One of `realm`, `iss` is required.
`state`::
String value used to maintain state between the authentication request and the
(Optional, string) Value used to maintain state between the authentication request and the
response, typically used as a Cross-Site Request Forgery mitigation. If the
caller of the API doesn't provide a value, {es} will generate one with
sufficient entropy itself and return it in the response.
`nonce`::
String value used to associate a Client session with an ID Token and to mitigate
(Optional, string) Value used to associate a Client session with an ID Token and to mitigate
replay attacks. If the caller of the API doesn't provide a value, {es} will
generate one with sufficient entropy itself and return it in the response.
`issuer`::
In the case of a 3rd Party initiated Single Sign On, this is the Issuer
`iss`::
(Optional, string) In the case of a 3rd Party initiated Single Sign On, this is the Issuer
Identifier for the OP that the RP is to send the Authentication Request to.
Cannot be specified when `realm` is specified.
Cannot be specified when `realm` is specified. One of `realm`, `iss` is required.
`login_hint`::
In the case of a 3rd Party initiated Single Sign On, a string value to be
(Optional, string) In the case of a 3rd Party initiated Single Sign On, a string value to be
included in the authentication request, as the `login_hint` parameter. This
parameter is not valid when `realm` is specified

View File

@ -649,7 +649,9 @@ POST /_security/oidc/prepare
this HTTP GET request, the custom web app will need to make an HTTP POST request to
`_security/oidc/authenticate`, again - authenticating as the `facilitator` user - passing the URL
where the user's browser was redirected to, as a parameter, along with the
values for `nonce` and `state` it had saved in the user's session previously.
values for `nonce` and `state` it had saved in the user's session previously. If more than one
OpenID Connect realms are configured, the custom web app can specify the name of the realm to be
used for handling this, but this parameter is optional.
See {ref}/security-api-oidc-authenticate.html[OIDC Authenticate API] for more details
+
[source,js]
@ -658,7 +660,8 @@ POST /_security/oidc/authenticate
{
"redirect_uri" : "https://oidc-kibana.elastic.co:5603/api/security/v1/oidc?code=jtI3Ntt8v3_XvcLzCFGq&state=4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"state" : "4dbrihtIAt3wBTwo6DxK-vdk-sSyDBV8Yf0AjdkdT5I",
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM"
"nonce" : "WaBPH0KqPVdG5HHdSxPRjfoZbXMCicm5v1OiAj0DUFM",
"realm" : "oidc1"
}
-----------------------------------------------------------------------
// CONSOLE

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.core.security.action.oidc;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
@ -38,6 +39,11 @@ public class OpenIdConnectAuthenticateRequest extends ActionRequest {
*/
private String nonce;
/**
* The name of the OIDC Realm that should consume the authentication request
*/
private String realm;
public OpenIdConnectAuthenticateRequest() {
}
@ -47,6 +53,10 @@ public class OpenIdConnectAuthenticateRequest extends ActionRequest {
redirectUri = in.readString();
state = in.readString();
nonce = in.readString();
if (in.getVersion().onOrAfter(Version.V_7_4_0)) {
realm = in.readOptionalString();
}
}
public String getRedirectUri() {
@ -73,6 +83,14 @@ public class OpenIdConnectAuthenticateRequest extends ActionRequest {
this.nonce = nonce;
}
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
@ -94,10 +112,13 @@ public class OpenIdConnectAuthenticateRequest extends ActionRequest {
out.writeString(redirectUri);
out.writeString(state);
out.writeString(nonce);
if (out.getVersion().onOrAfter(Version.V_7_4_0)) {
out.writeOptionalString(realm);
}
}
public String toString() {
return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + "}";
return "{redirectUri=" + redirectUri + ", state=" + state + ", nonce=" + nonce + ", realm=" +realm+"}";
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.core.security.action.saml;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
@ -19,6 +20,8 @@ public final class SamlAuthenticateRequest extends ActionRequest {
private byte[] saml;
private List<String> validRequestIds;
@Nullable
private String realm;
public SamlAuthenticateRequest(StreamInput in) throws IOException {
super(in);
@ -47,4 +50,12 @@ public final class SamlAuthenticateRequest extends ActionRequest {
public void setValidRequestIds(List<String> validRequestIds) {
this.validRequestIds = validRequestIds;
}
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
}

View File

@ -29,4 +29,9 @@ public final class SamlAuthenticateRequestBuilder
request.setValidRequestIds(validRequestIds);
return this;
}
public SamlAuthenticateRequestBuilder authenticatingRealm(String realm) {
request.setRealm(realm);
return this;
}
}

View File

@ -55,7 +55,7 @@ public class TransportOpenIdConnectAuthenticateAction
protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request,
ActionListener<OpenIdConnectAuthenticateResponse> listener) {
final OpenIdConnectToken token = new OpenIdConnectToken(request.getRedirectUri(), new State(request.getState()),
new Nonce(request.getNonce()));
new Nonce(request.getNonce()), request.getRealm());
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication originatingAuthentication = Authentication.getAuthentication(threadContext);
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {

View File

@ -48,7 +48,7 @@ public final class TransportSamlAuthenticateAction extends HandledTransportActio
@Override
protected void doExecute(Task task, SamlAuthenticateRequest request, ActionListener<SamlAuthenticateResponse> listener) {
final SamlToken saml = new SamlToken(request.getSaml(), request.getValidRequestIds());
final SamlToken saml = new SamlToken(request.getSaml(), request.getValidRequestIds(), request.getRealm());
logger.trace("Attempting to authenticate SamlToken [{}]", saml);
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication originatingAuthentication = Authentication.getAuthentication(threadContext);

View File

@ -140,8 +140,8 @@ public class AuthenticationService {
}
/**
* Authenticates the username and password that are provided as parameters. This will not look
* at the values in the ThreadContext for Authentication.
* Authenticates the user based on the contents of the token that is provided as parameter. This will not look at the values in the
* ThreadContext for Authentication.
*
* @param action The action of the message
* @param message The message that resulted in this authenticate call
@ -347,9 +347,10 @@ public class AuthenticationService {
/**
* Consumes the {@link AuthenticationToken} provided by the caller. In the case of a {@code null} token, {@link #handleNullToken()}
* is called. In the case of a {@code non-null} token, the realms are iterated over and the first realm that returns a non-null
* {@link User} is the authenticating realm and iteration is stopped. This user is then passed to {@link #consumeUser(User, Map)}
* if no exception was caught while trying to authenticate the token
* is called. In the case of a {@code non-null} token, the realms are iterated over in the order defined in the configuration
* while possibly also taking into consideration the last realm that authenticated this principal. When consulting multiple realms,
* the first realm that returns a non-null {@link User} is the authenticating realm and iteration is stopped. This user is then
* passed to {@link #consumeUser(User, Map)} if no exception was caught while trying to authenticate the token
*/
private void consumeToken(AuthenticationToken token) {
if (token == null) {
@ -411,6 +412,12 @@ public class AuthenticationService {
}
}
/**
* Possibly reorders the realm list depending on whether this principal has been recently authenticated by a specific realm
*
* @param principal The principal of the {@link AuthenticationToken} to be authenticated by a realm
* @return a list of realms ordered based on which realm should authenticate the current {@link AuthenticationToken}
*/
private List<Realm> getRealmList(String principal) {
final List<Realm> orderedRealmList = this.defaultOrderedRealmList;
if (lastSuccessfulAuthCache != null) {

View File

@ -145,6 +145,14 @@ public class OpenIdConnectRealm extends Realm implements Releasable {
return token instanceof OpenIdConnectToken;
}
private boolean isTokenForRealm(OpenIdConnectToken oidcToken) {
if (oidcToken.getAuthenticatingRealm() == null) {
return true;
} else {
return oidcToken.getAuthenticatingRealm().equals(this.name());
}
}
@Override
public AuthenticationToken token(ThreadContext context) {
return null;
@ -152,7 +160,7 @@ public class OpenIdConnectRealm extends Realm implements Releasable {
@Override
public void authenticate(AuthenticationToken token, ActionListener<AuthenticationResult> listener) {
if (token instanceof OpenIdConnectToken) {
if (token instanceof OpenIdConnectToken && isTokenForRealm((OpenIdConnectToken) token)) {
OpenIdConnectToken oidcToken = (OpenIdConnectToken) token;
openIdConnectAuthenticator.authenticate(oidcToken, ActionListener.wrap(
jwtClaimsSet -> {

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.oidc;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.Nonce;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
/**
@ -19,6 +20,7 @@ public class OpenIdConnectToken implements AuthenticationToken {
private String redirectUrl;
private State state;
private Nonce nonce;
private String authenticatingRealm;
/**
* @param redirectUrl The URI where the OP redirected the browser after the authentication event at the OP. This is passed as is from
@ -28,11 +30,13 @@ public class OpenIdConnectToken implements AuthenticationToken {
* user's session with the facilitator.
* @param nonce The nonce value that we generated or the facilitator provided for this specific flow and should be stored at the
* user's session with the facilitator.
* @param authenticatingRealm The realm that should authenticate this OpenId Connect Authentication Response
*/
public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce) {
public OpenIdConnectToken(String redirectUrl, State state, Nonce nonce, @Nullable String authenticatingRealm) {
this.redirectUrl = redirectUrl;
this.state = state;
this.nonce = nonce;
this.authenticatingRealm = authenticatingRealm;
}
@Override
@ -62,7 +66,10 @@ public class OpenIdConnectToken implements AuthenticationToken {
return redirectUrl;
}
public String getAuthenticatingRealm() { return authenticatingRealm; }
public String toString() {
return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + "}";
return getClass().getSimpleName() + "{ redirectUrl=" + redirectUrl + ", state=" + state + ", nonce=" + nonce + ", " +
"authenticatingRealm="+ authenticatingRealm +"}";
}
}

View File

@ -384,6 +384,14 @@ public final class SamlRealm extends Realm implements Releasable {
return token instanceof SamlToken;
}
private boolean isTokenForRealm(SamlToken samlToken) {
if (samlToken.getAuthenticatingRealm() == null) {
return true;
} else {
return samlToken.getAuthenticatingRealm().equals(this.name());
}
}
/**
* Always returns {@code null} as there is no support for reading a SAML token out of a request
*
@ -396,7 +404,7 @@ public final class SamlRealm extends Realm implements Releasable {
@Override
public void authenticate(AuthenticationToken authenticationToken, ActionListener<AuthenticationResult> listener) {
if (authenticationToken instanceof SamlToken) {
if (authenticationToken instanceof SamlToken && isTokenForRealm((SamlToken) authenticationToken)) {
try {
final SamlToken token = (SamlToken) authenticationToken;
final SamlAttributes attributes = authenticator.authenticate(token);

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc.saml;
import java.util.List;
import org.apache.commons.codec.binary.Hex;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
@ -20,14 +21,18 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
public class SamlToken implements AuthenticationToken {
private byte[] content;
private final List<String> allowedSamlRequestIds;
private final String authenticatingRealm;
/**
* @param content The content of the SAML message. This should be raw XML. In particular it should <strong>not</strong> be
* base64 encoded.
* @param allowedSamlRequestIds The request Ids for the authentication requests this SAML response is allowed to be in response to.
* @param authenticatingRealm The realm that should autenticate this SAML message.
*/
public SamlToken(byte[] content, List<String> allowedSamlRequestIds) {
public SamlToken(byte[] content, List<String> allowedSamlRequestIds, @Nullable String authenticatingRealm) {
this.content = content;
this.allowedSamlRequestIds = allowedSamlRequestIds;
this.authenticatingRealm = authenticatingRealm;
}
@Override
@ -53,6 +58,11 @@ public class SamlToken implements AuthenticationToken {
return allowedSamlRequestIds;
}
public String getAuthenticatingRealm() {
return authenticatingRealm;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{" + Strings.cleanTruncate(Hex.encodeHexString(content), 128) + "...}";

View File

@ -38,6 +38,7 @@ public class RestOpenIdConnectAuthenticateAction extends OpenIdConnectBaseRestHa
PARSER.declareString(OpenIdConnectAuthenticateRequest::setRedirectUri, new ParseField("redirect_uri"));
PARSER.declareString(OpenIdConnectAuthenticateRequest::setState, new ParseField("state"));
PARSER.declareString(OpenIdConnectAuthenticateRequest::setNonce, new ParseField("nonce"));
PARSER.declareStringOrNull(OpenIdConnectAuthenticateRequest::setRealm, new ParseField("realm"));
}
public RestOpenIdConnectAuthenticateAction(Settings settings, RestController controller, XPackLicenseState licenseState) {

View File

@ -42,6 +42,7 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R
static class Input {
String content;
List<String> ids;
String realm;
void setContent(String content) {
this.content = content;
@ -50,6 +51,8 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R
void setIds(List<String> ids) {
this.ids = ids;
}
void setRealm(String realm) { this.realm = realm;}
}
static final ObjectParser<Input, Void> PARSER = new ObjectParser<>("saml_authenticate", Input::new);
@ -57,6 +60,7 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R
static {
PARSER.declareString(Input::setContent, new ParseField("content"));
PARSER.declareStringArray(Input::setIds, new ParseField("ids"));
PARSER.declareStringOrNull(Input::setRealm, new ParseField("realm"));
}
public RestSamlAuthenticateAction(Settings settings, RestController controller,
@ -80,7 +84,8 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R
logger.trace("SAML Authenticate: [{}...] [{}]", Strings.cleanTruncate(input.content, 128), input.ids);
return channel -> {
final byte[] bytes = decodeBase64(input.content);
final SamlAuthenticateRequestBuilder requestBuilder = new SecurityClient(client).prepareSamlAuthenticate(bytes, input.ids);
final SamlAuthenticateRequestBuilder requestBuilder = new SecurityClient(client).prepareSamlAuthenticate(bytes, input.ids)
.authenticatingRealm(input.realm);
requestBuilder.execute(new RestBuilderListener<SamlAuthenticateResponse>(channel) {
@Override
public RestResponse buildResponse(SamlAuthenticateResponse response, XContentBuilder builder) throws Exception {

View File

@ -131,7 +131,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
public void testEmptyRedirectUrlIsRejected() throws Exception {
authenticator = buildAuthenticator();
OpenIdConnectToken token = new OpenIdConnectToken(null, new State(), new Nonce());
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
OpenIdConnectToken token = new OpenIdConnectToken(null, new State(), new Nonce(), authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -145,7 +146,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String state = randomAlphaOfLengthBetween(8, 12);
final String invalidState = state.concat(randomAlphaOfLength(2));
final String redirectUrl = "https://rp.elastic.co/cb?code=" + code + "&state=" + state;
OpenIdConnectToken token = new OpenIdConnectToken(redirectUrl, new State(invalidState), new Nonce());
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
OpenIdConnectToken token = new OpenIdConnectToken(redirectUrl, new State(invalidState), new Nonce(),authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -173,7 +175,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID();
final Tuple<AccessToken, JWT> tokens = buildTokens(invalidNonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -197,7 +200,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
@ -218,7 +222,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
@ -239,7 +244,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
@ -275,7 +281,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId, subject,
true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
JWTClaimsSet claimsSet = future.actionGet();
@ -312,7 +319,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -356,7 +364,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -399,7 +408,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -442,7 +452,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(idTokenBuilder.build(), key, jwk.getAlgorithm().getName(), keyId,
subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -471,7 +482,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -496,7 +508,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), jwk.getKeyID(), subject, true, true);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -520,7 +533,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String subject = "janedoe";
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), null, subject, true, true);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -550,7 +564,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, key, jwk.getAlgorithm().getName(), keyId, subject, true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), new BearerAccessToken("someforgedAccessToken"), state,
rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -588,7 +603,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
String fordedTokenString = encodedForgedHeader + "." + serializedParts[1] + "." + serializedParts[2];
idToken = SignedJWT.parse(fordedTokenString);
final String responseUrl = buildAuthResponse(idToken, tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -619,7 +635,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final Tuple<AccessToken, JWT> tokens = buildTokens(nonce, hmacKey, "HS384", null, subject,
true, false);
final String responseUrl = buildAuthResponse(tokens.v2(), tokens.v1(), state, rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,
@ -656,7 +673,8 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
final String responseUrl = buildAuthResponse(new PlainJWT(idTokenBuilder.build()), null, state,
rpConfig.getRedirectUri());
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken(responseUrl, state, nonce, authenticatingRealm);
final PlainActionFuture<JWTClaimsSet> future = new PlainActionFuture<>();
authenticator.authenticate(token, future);
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class,

View File

@ -68,6 +68,7 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
public void testAuthentication() throws Exception {
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
final String principal = randomAlphaOfLength(12);
AtomicReference<UserRoleMapper.UserData> userData = new AtomicReference<>();
doAnswer(invocation -> {
assert invocation.getArguments().length == 2;
@ -78,8 +79,13 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
final boolean notPopulateMetadata = randomBoolean();
AuthenticationResult result = authenticateWithOidc(roleMapper, notPopulateMetadata, false);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, notPopulateMetadata, false, authenticatingRealm);
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(principal));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("kibana_user", "role1"));
if (notPopulateMetadata == false) {
assertThat(result.getUser().metadata().get("oidc(iss)"), equalTo("https://op.company.org"));
@ -89,16 +95,21 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
public void testWithAuthorizingRealm() throws Exception {
final UserRoleMapper roleMapper = mock(UserRoleMapper.class);
final String principal = randomAlphaOfLength(12);
doAnswer(invocation -> {
assert invocation.getArguments().length == 2;
ActionListener<Set<String>> listener = (ActionListener<Set<String>>) invocation.getArguments()[1];
listener.onFailure(new RuntimeException("Role mapping should not be called"));
return null;
}).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class));
AuthenticationResult result = authenticateWithOidc(roleMapper, randomBoolean(), true);
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role"));
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
AuthenticationResult result = authenticateWithOidc(principal, roleMapper, randomBoolean(), true, authenticatingRealm);
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(principal));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role"));
assertThat(result.getUser().metadata().entrySet(), Matchers.iterableWithSize(1));
assertThat(result.getUser().metadata().get("is_lookup"), Matchers.equalTo(true));
assertNotNull(result.getMetadata().get(CONTEXT_TOKEN_DATA));
@ -107,6 +118,14 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
assertThat(tokenMetadata.get("id_token_hint"), equalTo("thisis.aserialized.jwt"));
}
public void testAuthenticationWithWrongRealm() throws Exception{
final String principal = randomAlphaOfLength(12);
AuthenticationResult result = authenticateWithOidc(principal, mock(UserRoleMapper.class), randomBoolean(), true,
REALM_NAME+randomAlphaOfLength(8));
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE));
}
public void testClaimPatternParsing() throws Exception {
final Settings.Builder builder = getBasicRealmSettings();
builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)");
@ -126,7 +145,8 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
public void testInvalidPrincipalClaimPatternParsing() {
final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class);
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce());
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce(), authenticatingRealm);
final Settings.Builder builder = getBasicRealmSettings();
builder.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getPattern()), "^OIDC-(.+)");
final RealmConfig config = buildConfig(builder.build(), threadContext);
@ -278,10 +298,10 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
state + "&nonce=" + nonce + "&client_id=rp-my"));
}
private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boolean notPopulateMetadata, boolean useAuthorizingRealm)
private AuthenticationResult authenticateWithOidc(String principal, UserRoleMapper roleMapper, boolean notPopulateMetadata,
boolean useAuthorizingRealm
,String authenticatingRealm)
throws Exception {
final String principal = "324235435454";
final MockLookupRealm lookupRealm = new MockLookupRealm(
new RealmConfig(new RealmConfig.RealmIdentifier("mock", "mock_lookup"), globalSettings, env, threadContext));
final OpenIdConnectAuthenticator authenticator = mock(OpenIdConnectAuthenticator.class);
@ -300,7 +320,7 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
final RealmConfig config = buildConfig(builder.build(), threadContext);
final OpenIdConnectRealm realm = new OpenIdConnectRealm(config, authenticator, roleMapper);
initializeRealms(realm, lookupRealm);
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce());
final OpenIdConnectToken token = new OpenIdConnectToken("", new State(), new Nonce(), authenticatingRealm);
final JWTClaimsSet claims = new JWTClaimsSet.Builder()
.subject(principal)
.audience("https://rp.elastic.co/cb")
@ -322,14 +342,7 @@ public class OpenIdConnectRealmTests extends OpenIdConnectTestCase {
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
realm.authenticate(token, future);
final AuthenticationResult result = future.get();
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(principal));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
return result;
return future.get();
}
private void initializeRealms(Realm... realms) {

View File

@ -2158,7 +2158,7 @@ public class SamlAuthenticatorTests extends SamlTestCase {
}
private SamlToken token(byte[] content) {
return new SamlToken(content, singletonList(requestId));
return new SamlToken(content, singletonList(requestId), null);
}
}

View File

@ -179,8 +179,13 @@ public class SamlRealmTests extends SamlTestCase {
final boolean useNameId = randomBoolean();
final boolean principalIsEmailAddress = randomBoolean();
final Boolean populateUserMetadata = randomFrom(Boolean.TRUE, Boolean.FALSE, null);
AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, populateUserMetadata, false);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, populateUserMetadata, false,
authenticatingRealm);
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(useNameId ? "clint.barton" : "cbarton"));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("superuser"));
if (populateUserMetadata == Boolean.FALSE) {
// TODO : "saml_nameid" should be null too, but the logout code requires it for now.
@ -208,16 +213,29 @@ public class SamlRealmTests extends SamlTestCase {
final boolean useNameId = randomBoolean();
final boolean principalIsEmailAddress = randomBoolean();
AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, null, true);
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, null, true,
authenticatingRealm);
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(useNameId ? "clint.barton" : "cbarton"));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role"));
assertThat(result.getUser().fullName(), equalTo("Clinton Barton"));
assertThat(result.getUser().metadata().entrySet(), Matchers.iterableWithSize(1));
assertThat(result.getUser().metadata().get("is_lookup"), Matchers.equalTo(true));
}
public void testAuthenticateWithWrongRealmName() throws Exception {
AuthenticationResult result = performAuthentication(mock(UserRoleMapper.class), randomBoolean(), randomBoolean(), null, true,
REALM_NAME+randomAlphaOfLength(8));
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE));
}
private AuthenticationResult performAuthentication(UserRoleMapper roleMapper, boolean useNameId, boolean principalIsEmailAddress,
Boolean populateUserMetadata, boolean useAuthorizingRealm) throws Exception {
Boolean populateUserMetadata, boolean useAuthorizingRealm,
String authenticatingRealm) throws Exception {
final EntityDescriptor idp = mockIdp();
final SpConfiguration sp = new SpConfiguration("<sp>", "https://saml/", null, null, null, Collections.emptyList());
final SamlAuthenticator authenticator = mock(SamlAuthenticator.class);
@ -255,7 +273,7 @@ public class SamlRealmTests extends SamlTestCase {
final SamlRealm realm = buildRealm(config, roleMapper, authenticator, logoutHandler, idp, sp);
initializeRealms(realm, lookupRealm);
final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("<id>"));
final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("<id>"), authenticatingRealm);
final SamlAttributes attributes = new SamlAttributes(
new SamlNameId(NameIDType.PERSISTENT, nameIdValue, idp.getEntityID(), sp.getEntityId(), null),
@ -269,13 +287,7 @@ public class SamlRealmTests extends SamlTestCase {
final PlainActionFuture<AuthenticationResult> future = new PlainActionFuture<>();
realm.authenticate(token, future);
final AuthenticationResult result = future.get();
assertThat(result, notNullValue());
assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS));
assertThat(result.getUser().principal(), equalTo(userPrincipal));
assertThat(result.getUser().email(), equalTo("cbarton@shield.gov"));
return result;
return future.get();
}
private void initializeRealms(Realm... realms) {
@ -370,7 +382,8 @@ public class SamlRealmTests extends SamlTestCase {
final RealmConfig config = buildConfig(realmSettings);
final SamlRealm realm = buildRealm(config, roleMapper, authenticator, logoutHandler, idp, sp);
final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("<id>"));
final String authenticatingRealm = randomBoolean() ? REALM_NAME : null;
final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("<id>"), authenticatingRealm);
for (String mail : Arrays.asList("john@your-corp.example.com", "john@mycorp.example.com.example.net", "john")) {
final SamlAttributes attributes = new SamlAttributes(

View File

@ -28,7 +28,9 @@ import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.SecureString;
@ -242,19 +244,34 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
public void testAuthenticateWithCodeFlow() throws Exception {
final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME);
final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri());
final String realm = randomBoolean() ? null : prepareAuthResponse.getRealm();
Tuple<String, String> tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(),
prepareAuthResponse.getNonce());
prepareAuthResponse.getNonce(), realm);
verifyElasticsearchAccessTokenForCodeFlow(tokens.v1());
}
public void testAuthenticateWithImplicitFlow() throws Exception {
final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME_IMPLICIT);
final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri());
final String realm = randomBoolean() ? null : prepareAuthResponse.getRealm();
Tuple<String, String> tokens = completeAuthentication(redirectUri, prepareAuthResponse.getState(),
prepareAuthResponse.getNonce());
prepareAuthResponse.getNonce(), realm);
verifyElasticsearchAccessTokenForImplicitFlow(tokens.v1());
}
public void testAuthenticateWithCodeFlowFailsForWrongRealm() throws Exception {
final PrepareAuthResponse prepareAuthResponse = getRedirectedFromFacilitator(REALM_NAME);
final String redirectUri = authenticateAtOP(prepareAuthResponse.getAuthUri());
// Use existing realm that can't authenticate the response, or a non-existent realm
ResponseException e = expectThrows(ResponseException.class, () -> {
completeAuthentication(redirectUri,
prepareAuthResponse.getState(),
prepareAuthResponse.getNonce(), randomFrom(REALM_NAME_IMPLICIT, REALM_NAME + randomAlphaOfLength(8)));
});
assertThat(401, equalTo(e.getResponse().getStatusLine().getStatusCode()));
}
private void verifyElasticsearchAccessTokenForCodeFlow(String accessToken) throws IOException {
final Map<String, Object> map = callAuthenticateApiUsingAccessToken(accessToken);
logger.info("Authentication with token Response: " + map);
@ -290,14 +307,19 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
final String state = (String) responseBody.get("state");
final String nonce = (String) responseBody.get("nonce");
final String authUri = (String) responseBody.get("redirect");
return new PrepareAuthResponse(new URI(authUri), state, nonce);
final String realm = (String) responseBody.get("realm");
return new PrepareAuthResponse(new URI(authUri), state, nonce, realm);
}
private Tuple<String, String> completeAuthentication(String redirectUri, String state, String nonce) throws Exception {
private Tuple<String, String> completeAuthentication(String redirectUri, String state, String nonce, @Nullable String realm)
throws Exception {
final Map<String, String> body = new HashMap<>();
body.put("redirect_uri", redirectUri);
body.put("state", state);
body.put("nonce", nonce);
if (realm != null){
body.put("realm", realm);
}
Request request = buildRequest("POST", "/_security/oidc/authenticate", body, facilitatorAuth());
final Response authenticate = client().performRequest(request);
assertOK(authenticate);
@ -387,11 +409,13 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
private URI authUri;
private String state;
private String nonce;
private String realm;
PrepareAuthResponse(URI authUri, String state, String nonce) {
PrepareAuthResponse(URI authUri, String state, String nonce, @Nullable String realm) {
this.authUri = authUri;
this.state = state;
this.nonce = nonce;
this.realm = realm;
}
URI getAuthUri() {
@ -405,5 +429,7 @@ public class OpenIdConnectAuthIT extends ESRestTestCase {
String getNonce() {
return nonce;
}
String getRealm() { return realm;}
}
}

View File

@ -67,7 +67,15 @@ testClusters.integTest {
setting 'xpack.security.authc.realms.saml.shibboleth_native.sp.acs', 'http://localhost:54321/saml/acs2'
setting 'xpack.security.authc.realms.saml.shibboleth_native.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.saml.shibboleth_native.authorization_realms', 'native'
setting 'xpack.security.authc.realms.native.native.order', '3'
// SAML realm 3 (used for negative tests with multiple realms)
setting 'xpack.security.authc.realms.saml.shibboleth_negative.order', '3'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.idp.entity_id', 'https://test.shibboleth.elastic.local/'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.idp.metadata.path', 'idp-metadata.xml'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.sp.entity_id', 'somethingwronghere'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.sp.acs', 'http://localhost:54321/saml/acs3'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.authorization_realms', 'native'
setting 'xpack.security.authc.realms.native.native.order', '4'
setting 'xpack.ml.enabled', 'false'
setting 'logger.org.elasticsearch.xpack.security', 'TRACE'

View File

@ -41,6 +41,7 @@ import org.elasticsearch.cli.SuppressForbidden;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
@ -107,8 +108,9 @@ public class SamlAuthenticationIT extends ESRestTestCase {
private static final String SP_LOGIN_PATH = "/saml/login";
private static final String SP_ACS_PATH_1 = "/saml/acs1";
private static final String SP_ACS_PATH_2 = "/saml/acs2";
private static final String SP_ACS_PATH_WRONG_REALM = "/saml/acs3";
private static final String SAML_RESPONSE_FIELD = "SAMLResponse";
private static final String REQUEST_ID_COOKIE = "saml-request-id";
private static final String SAML_REQUEST_COOKIE = "saml-request";
private static final String KIBANA_PASSWORD = "K1b@na K1b@na K1b@na";
private static HttpServer httpServer;
@ -137,6 +139,7 @@ public class SamlAuthenticationIT extends ESRestTestCase {
httpServer.createContext(SP_LOGIN_PATH, wrapFailures(this::httpLogin));
httpServer.createContext(SP_ACS_PATH_1, wrapFailures(this::httpAcs));
httpServer.createContext(SP_ACS_PATH_2, wrapFailures(this::httpAcs));
httpServer.createContext(SP_ACS_PATH_WRONG_REALM, wrapFailures(this::httpAcsFailure));
}
/**
@ -253,6 +256,17 @@ public class SamlAuthenticationIT extends ESRestTestCase {
verifyElasticsearchAccessTokenForAuthorizingRealms(accessToken);
}
public void testLoginWithWrongRealmFails() throws Exception {
this.acs = new URI("http://localhost:54321" + SP_ACS_PATH_WRONG_REALM);
final BasicHttpContext context = new BasicHttpContext();
try (CloseableHttpClient client = getHttpClient()) {
final URI loginUri = goToLoginPage(client, context);
final URI consentUri = submitLoginForm(client, context, loginUri);
final Tuple<URI, String> tuple = submitConsentForm(context, client, consentUri);
submitSamlResponse(context, client, tuple.v1(), tuple.v2(), false);
}
}
private Tuple<String, String> loginViaSaml(String acs) throws Exception {
this.acs = new URI(acs);
final BasicHttpContext context = new BasicHttpContext();
@ -260,7 +274,7 @@ public class SamlAuthenticationIT extends ESRestTestCase {
final URI loginUri = goToLoginPage(client, context);
final URI consentUri = submitLoginForm(client, context, loginUri);
final Tuple<URI, String> tuple = submitConsentForm(context, client, consentUri);
final Map<String, Object> result = submitSamlResponse(context, client, tuple.v1(), tuple.v2());
final Map<String, Object> result = submitSamlResponse(context, client, tuple.v1(), tuple.v2(), true);
assertThat(result.get("username"), equalTo("thor"));
final Object expiresIn = result.get("expires_in");
@ -411,7 +425,8 @@ public class SamlAuthenticationIT extends ESRestTestCase {
* @param acs The URI to the Service Provider's Assertion-Consumer-Service.
* @param saml The (deflated + base64 encoded) {@code SAMLResponse} parameter to post the ACS
*/
private Map<String, Object> submitSamlResponse(BasicHttpContext context, CloseableHttpClient client, URI acs, String saml)
private Map<String, Object> submitSamlResponse(BasicHttpContext context, CloseableHttpClient client, URI acs, String saml,
boolean shouldSucceed)
throws IOException {
assertThat("SAML submission target", acs, notNullValue());
assertThat(acs, equalTo(this.acs));
@ -425,7 +440,11 @@ public class SamlAuthenticationIT extends ESRestTestCase {
form.setEntity(new UrlEncodedFormEntity(params));
return execute(client, form, context, response -> {
assertHttpOk(response.getStatusLine());
if (shouldSucceed) {
assertHttpOk(response.getStatusLine());
} else {
assertHttpUnauthorized(response.getStatusLine());
}
return parseResponseAsMap(response.getEntity());
});
}
@ -520,7 +539,7 @@ public class SamlAuthenticationIT extends ESRestTestCase {
assertOK(prepare);
final Map<String, Object> responseBody = parseResponseAsMap(prepare.getEntity());
logger.info("Created SAML authentication request {}", responseBody);
http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + responseBody.get("id"));
http.getResponseHeaders().add("Set-Cookie", SAML_REQUEST_COOKIE + "=" + responseBody.get("id") + "&" + responseBody.get("realm"));
http.getResponseHeaders().add("Location", (String) responseBody.get("redirect"));
http.sendResponseHeaders(302, 0);
http.close();
@ -541,26 +560,63 @@ public class SamlAuthenticationIT extends ESRestTestCase {
http.close();
}
private Response samlAuthenticate(HttpExchange http) throws IOException {
/**
* Provides the "Assertion-Consumer-Service" handler for the fake WebApp that can handle failures.
* This interacts with Elasticsearch (using the rest client) to perform a SAML login, asserts that it
* failed with a 401 and returns 401 to the browser.
*/
private void httpAcsFailure(HttpExchange http) throws IOException {
final List<NameValuePair> pairs = parseRequestForm(http);
assertThat(pairs, iterableWithSize(1));
final String saml = pairs.stream()
.filter(p -> SAML_RESPONSE_FIELD.equals(p.getName()))
.map(p -> p.getValue())
.findFirst()
.orElseGet(() -> {
fail("Cannot find " + SAML_RESPONSE_FIELD + " in form fields");
return null;
});
final String id = getCookie(REQUEST_ID_COOKIE, http);
final String saml = getSamlContentFromParams(pairs);
final Tuple<String, String> storedValues = getCookie(http);
assertThat(storedValues, notNullValue());
final String id = storedValues.v1();
assertThat(id, notNullValue());
final String realmName = randomFrom("shibboleth_" + randomAlphaOfLength(8), "shibboleth_native");
final Map<String, ?> body = MapBuilder.<String, Object>newMapBuilder()
.put("content", saml)
.put("ids", Collections.singletonList(id))
.put("realm", realmName)
.map();
return client().performRequest(buildRequest("POST", "/_security/saml/authenticate", body, kibanaAuth()));
ResponseException e = expectThrows(ResponseException.class, () -> {
client().performRequest(buildRequest("POST", "/_security/saml/authenticate", body, kibanaAuth()));
});
assertThat(401, equalTo(e.getResponse().getStatusLine().getStatusCode()));
http.sendResponseHeaders(401, 0);
http.close();
}
private Response samlAuthenticate(HttpExchange http) throws IOException {
final List<NameValuePair> pairs = parseRequestForm(http);
assertThat(pairs, iterableWithSize(1));
final String saml = getSamlContentFromParams(pairs);
final Tuple<String, String> storedValues = getCookie(http);
assertThat(storedValues, notNullValue());
final String id = storedValues.v1();
final String realmName = storedValues.v2();
assertThat(id, notNullValue());
assertThat(realmName, notNullValue());
final MapBuilder<String, Object> bodyBuilder = new MapBuilder()
.put("content", saml)
.put("ids", Collections.singletonList(id));
if (randomBoolean()) {
bodyBuilder.put("realm", realmName);
}
return client().performRequest(buildRequest("POST", "/_security/saml/authenticate", bodyBuilder.map(), kibanaAuth()));
}
private String getSamlContentFromParams(List<NameValuePair> params) {
return params.stream()
.filter(p -> SAML_RESPONSE_FIELD.equals(p.getName()))
.map(p -> p.getValue())
.findFirst()
.orElseGet(() -> {
fail("Cannot find " + SAML_RESPONSE_FIELD + " in form fields");
return null;
});
}
private List<NameValuePair> parseRequestForm(HttpExchange http) throws IOException {
@ -570,7 +626,7 @@ public class SamlAuthenticationIT extends ESRestTestCase {
return URLEncodedUtils.parse(buffer, HTTP.DEF_CONTENT_CHARSET, '&');
}
private String getCookie(String name, HttpExchange http) throws IOException {
private Tuple<String, String> getCookie(HttpExchange http) throws IOException {
try {
final String cookies = http.getRequestHeaders().getFirst("Cookie");
if (cookies == null) {
@ -582,7 +638,10 @@ public class SamlAuthenticationIT extends ESRestTestCase {
final URI requestURI = http.getRequestURI();
final CookieOrigin origin = new CookieOrigin(serverUri.getHost(), serverUri.getPort(), requestURI.getPath(), false);
final List<Cookie> parsed = new DefaultCookieSpec().parse(header, origin);
return parsed.stream().filter(c -> name.equals(c.getName())).map(c -> c.getValue()).findFirst().orElse(null);
return parsed.stream().filter(c -> SAML_REQUEST_COOKIE.equals(c.getName())).map(c -> {
String[] values = c.getValue().split("&");
return new Tuple(values[0], values[1]);
}).findFirst().orElse(null);
} catch (MalformedCookieException e) {
throw new IOException("Cannot read cookies", e);
}
@ -592,6 +651,10 @@ public class SamlAuthenticationIT extends ESRestTestCase {
assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(200));
}
private void assertHttpUnauthorized(StatusLine status) {
assertThat("Unexpected HTTP Response status: " + status, status.getStatusCode(), Matchers.equalTo(401));
}
private static void assertSingletonList(Object value, String expectedElement) {
assertThat(value, instanceOf(List.class));
assertThat(((List<?>) value), contains(expectedElement));