From 8ab58f95f22241b6a549341bddf6f8ef8bda0ef5 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Sun, 25 Aug 2019 18:00:33 +0300 Subject: [PATCH] Allow Transport Actions to indicate authN realm (#45767) 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. --- build.gradle | 4 +- .../security/oidc-authenticate-api.asciidoc | 13 ++- .../security/oidc-logout-api.asciidoc | 4 +- .../oidc-prepare-authentication-api.asciidoc | 16 +-- .../authentication/oidc-guide.asciidoc | 7 +- .../OpenIdConnectAuthenticateRequest.java | 23 +++- .../action/saml/SamlAuthenticateRequest.java | 11 ++ .../saml/SamlAuthenticateRequestBuilder.java | 5 + ...nsportOpenIdConnectAuthenticateAction.java | 2 +- .../saml/TransportSamlAuthenticateAction.java | 2 +- .../security/authc/AuthenticationService.java | 17 ++- .../authc/oidc/OpenIdConnectRealm.java | 10 +- .../authc/oidc/OpenIdConnectToken.java | 11 +- .../xpack/security/authc/saml/SamlRealm.java | 10 +- .../xpack/security/authc/saml/SamlToken.java | 12 ++- .../RestOpenIdConnectAuthenticateAction.java | 1 + .../saml/RestSamlAuthenticateAction.java | 7 +- .../oidc/OpenIdConnectAuthenticatorTests.java | 54 ++++++---- .../authc/oidc/OpenIdConnectRealmTests.java | 49 +++++---- .../authc/saml/SamlAuthenticatorTests.java | 2 +- .../security/authc/saml/SamlRealmTests.java | 41 ++++--- .../authc/oidc/OpenIdConnectAuthIT.java | 36 ++++++- x-pack/qa/saml-idp-tests/build.gradle | 10 +- .../authc/saml/SamlAuthenticationIT.java | 101 ++++++++++++++---- 24 files changed, 340 insertions(+), 108 deletions(-) diff --git a/build.gradle b/build.gradle index a199afaae9cf8..31562dbf6ef83 100644 --- a/build.gradle +++ b/build.gradle @@ -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") diff --git a/x-pack/docs/en/rest-api/security/oidc-authenticate-api.asciidoc b/x-pack/docs/en/rest-api/security/oidc-authenticate-api.asciidoc index 31c532450219e..67e6d68a109ec 100644 --- a/x-pack/docs/en/rest-api/security/oidc-authenticate-api.asciidoc +++ b/x-pack/docs/en/rest-api/security/oidc-authenticate-api.asciidoc @@ -31,24 +31,28 @@ and <> ==== {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 diff --git a/x-pack/docs/en/rest-api/security/oidc-logout-api.asciidoc b/x-pack/docs/en/rest-api/security/oidc-logout-api.asciidoc index cd130e9a531fe..47d7125b94d99 100644 --- a/x-pack/docs/en/rest-api/security/oidc-logout-api.asciidoc +++ b/x-pack/docs/en/rest-api/security/oidc-logout-api.asciidoc @@ -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]] diff --git a/x-pack/docs/en/rest-api/security/oidc-prepare-authentication-api.asciidoc b/x-pack/docs/en/rest-api/security/oidc-prepare-authentication-api.asciidoc index f443ea4297e06..daf1854e81f88 100644 --- a/x-pack/docs/en/rest-api/security/oidc-prepare-authentication-api.asciidoc +++ b/x-pack/docs/en/rest-api/security/oidc-prepare-authentication-api.asciidoc @@ -33,28 +33,28 @@ and <>. 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 diff --git a/x-pack/docs/en/security/authentication/oidc-guide.asciidoc b/x-pack/docs/en/security/authentication/oidc-guide.asciidoc index 369f476f38283..6cc59f8231651 100644 --- a/x-pack/docs/en/security/authentication/oidc-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/oidc-guide.asciidoc @@ -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 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java index b90a3a69c840d..8d1e891f0061d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/oidc/OpenIdConnectAuthenticateRequest.java @@ -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 OpenIdConnectAuthenticateRequest(StreamInput in) throws IOException { 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 void setNonce(String nonce) { 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 void writeTo(StreamOutput out) throws IOException { 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+"}"; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequest.java index 40fce11edbc08..753dbbc1b4a5e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequest.java @@ -7,6 +7,7 @@ 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 validRequestIds; + @Nullable + private String realm; public SamlAuthenticateRequest(StreamInput in) throws IOException { super(in); @@ -47,4 +50,12 @@ public List getValidRequestIds() { public void setValidRequestIds(List validRequestIds) { this.validRequestIds = validRequestIds; } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequestBuilder.java index 17cff756e2622..54199e94e78eb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlAuthenticateRequestBuilder.java @@ -29,4 +29,9 @@ public SamlAuthenticateRequestBuilder validRequestIds(List validRequestI request.setValidRequestIds(validRequestIds); return this; } + + public SamlAuthenticateRequestBuilder authenticatingRealm(String realm) { + request.setRealm(realm); + return this; + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java index 4bab16cf92115..8700bfb0f1076 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectAuthenticateAction.java @@ -55,7 +55,7 @@ public TransportOpenIdConnectAuthenticateAction(ThreadPool threadPool, Transport protected void doExecute(Task task, OpenIdConnectAuthenticateRequest request, ActionListener 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()) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java index 96eec7e8fd6c7..528663fbce642 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java @@ -48,7 +48,7 @@ public TransportSamlAuthenticateAction(ThreadPool threadPool, TransportService t @Override protected void doExecute(Task task, SamlAuthenticateRequest request, ActionListener 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); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 1fe3ed67f7337..c140b2c397824 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -140,8 +140,8 @@ public void authenticate(String action, TransportMessage message, User fallbackU } /** - * 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 @@ void extractToken(Consumer consumer) { /** * 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 @@ private void consumeToken(AuthenticationToken token) { } } + /** + * 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 getRealmList(String principal) { final List orderedRealmList = this.defaultOrderedRealmList; if (lastSuccessfulAuthCache != null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java index 77ea1d57ad16f..4e4a54a4ce124 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealm.java @@ -145,6 +145,14 @@ public boolean supports(AuthenticationToken token) { 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 AuthenticationToken token(ThreadContext context) { @Override public void authenticate(AuthenticationToken token, ActionListener listener) { - if (token instanceof OpenIdConnectToken) { + if (token instanceof OpenIdConnectToken && isTokenForRealm((OpenIdConnectToken) token)) { OpenIdConnectToken oidcToken = (OpenIdConnectToken) token; openIdConnectAuthenticator.authenticate(oidcToken, ActionListener.wrap( jwtClaimsSet -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java index ab61fd8fb9d5f..94c05db887286 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectToken.java @@ -7,6 +7,7 @@ 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 String getRedirectUrl() { 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 +"}"; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index 3eb252577f0a7..1d3f1b731dcf3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -384,6 +384,14 @@ public boolean supports(AuthenticationToken token) { 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 AuthenticationToken token(ThreadContext threadContext) { @Override public void authenticate(AuthenticationToken authenticationToken, ActionListener listener) { - if (authenticationToken instanceof SamlToken) { + if (authenticationToken instanceof SamlToken && isTokenForRealm((SamlToken) authenticationToken)) { try { final SamlToken token = (SamlToken) authenticationToken; final SamlAttributes attributes = authenticator.authenticate(token); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlToken.java index 3420733f74a61..8a4ee00ae2a0e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlToken.java @@ -8,6 +8,7 @@ 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 @@ public class SamlToken implements AuthenticationToken { private byte[] content; private final List allowedSamlRequestIds; + private final String authenticatingRealm; /** * @param content The content of the SAML message. This should be raw XML. In particular it should not 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 allowedSamlRequestIds) { + public SamlToken(byte[] content, List allowedSamlRequestIds, @Nullable String authenticatingRealm) { this.content = content; this.allowedSamlRequestIds = allowedSamlRequestIds; + this.authenticatingRealm = authenticatingRealm; } @Override @@ -53,6 +58,11 @@ public List getAllowedSamlRequestIds() { return allowedSamlRequestIds; } + public String getAuthenticatingRealm() { + return authenticatingRealm; + } + + @Override public String toString() { return getClass().getSimpleName() + "{" + Strings.cleanTruncate(Hex.encodeHexString(content), 128) + "...}"; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java index 2ac75872b7c8a..904300ffbdced 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oidc/RestOpenIdConnectAuthenticateAction.java @@ -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) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java index b73fb70c3fa9d..331b484ff49fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java @@ -42,6 +42,7 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R static class Input { String content; List ids; + String realm; void setContent(String content) { this.content = content; @@ -50,6 +51,8 @@ void setContent(String content) { void setIds(List ids) { this.ids = ids; } + + void setRealm(String realm) { this.realm = realm;} } static final ObjectParser PARSER = new ObjectParser<>("saml_authenticate", Input::new); @@ -57,6 +60,7 @@ void setIds(List ids) { 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 RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient c 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(channel) { @Override public RestResponse buildResponse(SamlAuthenticateResponse response, XContentBuilder builder) throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java index 5c31ad850be96..d4734ce93e076 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java @@ -131,7 +131,8 @@ private OpenIdConnectAuthenticator buildAuthenticator(OpenIdConnectProviderConfi 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -145,7 +146,8 @@ public void testInvalidStateIsRejected() throws URISyntaxException { 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -173,7 +175,8 @@ public void testInvalidNonceIsRejected() throws Exception { final String keyId = (jwk.getAlgorithm().getName().startsWith("HS")) ? null : jwk.getKeyID(); final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -197,7 +200,8 @@ public void testAuthenticateImplicitFlowWithRsa() throws Exception { final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); JWTClaimsSet claimsSet = future.actionGet(); @@ -218,7 +222,8 @@ public void testAuthenticateImplicitFlowWithEcdsa() throws Exception { final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); JWTClaimsSet claimsSet = future.actionGet(); @@ -239,7 +244,8 @@ public void testAuthenticateImplicitFlowWithHmac() throws Exception { final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); JWTClaimsSet claimsSet = future.actionGet(); @@ -275,7 +281,8 @@ public void testClockSkewIsHonored() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); JWTClaimsSet claimsSet = future.actionGet(); @@ -312,7 +319,8 @@ public void testImplicitFlowFailsWithExpiredToken() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -356,7 +364,8 @@ public void testImplicitFlowFailsNotYetIssuedToken() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -399,7 +408,8 @@ public void testImplicitFlowFailsInvalidIssuer() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -442,7 +452,8 @@ public void testImplicitFlowFailsInvalidAudience() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -471,7 +482,8 @@ public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Excep final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -496,7 +508,8 @@ public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Ex final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -520,7 +533,8 @@ public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exce final String subject = "janedoe"; final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -550,7 +564,8 @@ public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exce final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -588,7 +603,8 @@ public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception { 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -619,7 +635,8 @@ public void testImplicitFlowFailsWithAlgorithmMixupAttack() throws Exception { final Tuple 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, @@ -656,7 +673,8 @@ public void testImplicitFlowFailsWithUnsignedJwt() throws Exception { 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 future = new PlainActionFuture<>(); authenticator.authenticate(token, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java index 162b88224414e..8b0a435101a0a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java @@ -68,6 +68,7 @@ public void setupEnv() { public void testAuthentication() throws Exception { final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + final String principal = randomAlphaOfLength(12); AtomicReference userData = new AtomicReference<>(); doAnswer(invocation -> { assert invocation.getArguments().length == 2; @@ -78,8 +79,13 @@ public void testAuthentication() throws Exception { }).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 void testAuthentication() throws Exception { public void testWithAuthorizingRealm() throws Exception { final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + final String principal = randomAlphaOfLength(12); doAnswer(invocation -> { assert invocation.getArguments().length == 2; ActionListener> listener = (ActionListener>) 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 void testWithAuthorizingRealm() throws Exception { 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 void testClaimPatternParsing() throws Exception { 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 void testBuildingAuthenticationRequestWithLoginHint() { 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 @@ private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boo 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 @@ private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boo final PlainActionFuture 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) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java index 184b7ae225ce2..773272548dda1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java @@ -2158,7 +2158,7 @@ private SamlToken token(String content) { } private SamlToken token(byte[] content) { - return new SamlToken(content, singletonList(requestId)); + return new SamlToken(content, singletonList(requestId), null); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java index 272d3375f835b..1e873e302367e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java @@ -179,8 +179,13 @@ public void testAuthenticateWithRoleMapping() throws Exception { 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 void testAuthenticateWithAuthorizingRealm() throws Exception { 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("", "https://saml/", null, null, null, Collections.emptyList()); final SamlAuthenticator authenticator = mock(SamlAuthenticator.class); @@ -255,7 +273,7 @@ private AuthenticationResult performAuthentication(UserRoleMapper roleMapper, bo final SamlRealm realm = buildRealm(config, roleMapper, authenticator, logoutHandler, idp, sp); initializeRealms(realm, lookupRealm); - final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("")); + final SamlToken token = new SamlToken(new byte[0], Collections.singletonList(""), authenticatingRealm); final SamlAttributes attributes = new SamlAttributes( new SamlNameId(NameIDType.PERSISTENT, nameIdValue, idp.getEntityID(), sp.getEntityId(), null), @@ -269,13 +287,7 @@ private AuthenticationResult performAuthentication(UserRoleMapper roleMapper, bo final PlainActionFuture 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 void testNonMatchingPrincipalPatternThrowsSamlException() throws Exceptio 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("")); + final String authenticatingRealm = randomBoolean() ? REALM_NAME : null; + final SamlToken token = new SamlToken(new byte[0], Collections.singletonList(""), authenticatingRealm); for (String mail : Arrays.asList("john@your-corp.example.com", "john@mycorp.example.com.example.net", "john")) { final SamlAttributes attributes = new SamlAttributes( diff --git a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java index 3022b34c4aeec..67643af723894 100644 --- a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java +++ b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java @@ -28,7 +28,9 @@ 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 @@ private void configureJsonRequest(HttpEntityEnclosingRequestBase request, String 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 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 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 map = callAuthenticateApiUsingAccessToken(accessToken); logger.info("Authentication with token Response: " + map); @@ -290,14 +307,19 @@ private PrepareAuthResponse getRedirectedFromFacilitator(String realmName) throw 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 completeAuthentication(String redirectUri, String state, String nonce) throws Exception { + private Tuple completeAuthentication(String redirectUri, String state, String nonce, @Nullable String realm) + throws Exception { final Map 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 @@ class PrepareAuthResponse { 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 @@ String getState() { String getNonce() { return nonce; } + + String getRealm() { return realm;} } } diff --git a/x-pack/qa/saml-idp-tests/build.gradle b/x-pack/qa/saml-idp-tests/build.gradle index b34e96dc0b401..0ca9dfae214c7 100644 --- a/x-pack/qa/saml-idp-tests/build.gradle +++ b/x-pack/qa/saml-idp-tests/build.gradle @@ -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' diff --git a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java index 2dbe427c23bb7..8c16647b2ef11 100644 --- a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java +++ b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java @@ -41,6 +41,7 @@ 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 void setupHttpContext() { 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 void testLoginUserWithAuthorizingRealm() throws Exception { 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 tuple = submitConsentForm(context, client, consentUri); + submitSamlResponse(context, client, tuple.v1(), tuple.v2(), false); + } + } + private Tuple loginViaSaml(String acs) throws Exception { this.acs = new URI(acs); final BasicHttpContext context = new BasicHttpContext(); @@ -260,7 +274,7 @@ private Tuple loginViaSaml(String acs) throws Exception { final URI loginUri = goToLoginPage(client, context); final URI consentUri = submitLoginForm(client, context, loginUri); final Tuple tuple = submitConsentForm(context, client, consentUri); - final Map result = submitSamlResponse(context, client, tuple.v1(), tuple.v2()); + final Map 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 @@ private Tuple submitConsentForm(BasicHttpContext context, Closeable * @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 submitSamlResponse(BasicHttpContext context, CloseableHttpClient client, URI acs, String saml) + private Map 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 @@ private Map submitSamlResponse(BasicHttpContext context, Closeab 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 @@ private void httpLogin(HttpExchange http) throws IOException { assertOK(prepare); final Map 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 @@ private void httpAcs(HttpExchange http) throws IOException { 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 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 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 body = MapBuilder.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 pairs = parseRequestForm(http); + assertThat(pairs, iterableWithSize(1)); + final String saml = getSamlContentFromParams(pairs); + final Tuple storedValues = getCookie(http); + assertThat(storedValues, notNullValue()); + final String id = storedValues.v1(); + final String realmName = storedValues.v2(); + assertThat(id, notNullValue()); + assertThat(realmName, notNullValue()); + + final MapBuilder 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 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 parseRequestForm(HttpExchange http) throws IOException { @@ -570,7 +626,7 @@ private List parseRequestForm(HttpExchange http) throws IOExcepti return URLEncodedUtils.parse(buffer, HTTP.DEF_CONTENT_CHARSET, '&'); } - private String getCookie(String name, HttpExchange http) throws IOException { + private Tuple getCookie(HttpExchange http) throws IOException { try { final String cookies = http.getRequestHeaders().getFirst("Cookie"); if (cookies == null) { @@ -582,7 +638,10 @@ private String getCookie(String name, HttpExchange http) throws IOException { final URI requestURI = http.getRequestURI(); final CookieOrigin origin = new CookieOrigin(serverUri.getHost(), serverUri.getPort(), requestURI.getPath(), false); final List 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 @@ private void assertHttpOk(StatusLine status) { 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));