diff --git a/build.gradle b/build.gradle index 1719f70367bbc..7f94db8a8d04e 100644 --- a/build.gradle +++ b/build.gradle @@ -176,8 +176,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 836e5cbbf3dc2..a3bc026e330f2 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 @@ -143,6 +143,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; @@ -150,7 +158,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 fa49bdbb68623..4f25d681d0e8d 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 a61aaf650f99d..f1efc9437a8ef 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 @@ -41,6 +41,7 @@ public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements R static class Input { String content; List ids; + String realm; void setContent(String content) { this.content = content; @@ -49,6 +50,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); @@ -56,6 +59,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,7 @@ public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient c return channel -> { final byte[] bytes = decodeBase64(input.content); final SamlAuthenticateRequestBuilder requestBuilder = - new SamlAuthenticateRequestBuilder(client).saml(bytes).validRequestIds(input.ids); + new SamlAuthenticateRequestBuilder(client).saml(bytes).validRequestIds(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 824655d59c7e5..3a26e6b1fb3e2 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 4b8f3df4159b5..34a5dca5f8460 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 23bc287086b97..41f0362bd787b 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));