diff --git a/test/framework/src/main/java/org/elasticsearch/common/util/NamedFormatter.java b/test/framework/src/main/java/org/elasticsearch/common/util/NamedFormatter.java new file mode 100644 index 0000000000000..7fba3e4241283 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/common/util/NamedFormatter.java @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.util; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A formatter that allows named placeholders e.g. "%(param)" to be replaced. + */ +public class NamedFormatter { + private static final Pattern PARAM_REGEX = Pattern + .compile( + // Match either any backlash-escaped characters, or a "%(param)" pattern. + // COMMENTS is specified to allow whitespace in this pattern, for clarity + "\\\\(.) | (% \\( ([^)]+) \\) )", + Pattern.COMMENTS + ); + + private NamedFormatter() {} + + /** + * Replaces named parameters of the form %(param) in format strings. For example: + * + * + * + * @param fmt The format string. Any %(param) is replaced by its corresponding value in the values map. + * Parameter patterns can be escaped by prefixing with a backslash. + * @param values a map of parameter names to values. + * @return The formatted string. + * @throws IllegalArgumentException if a parameter is found in the format string with no corresponding value + */ + public static String format(String fmt, Map values) { + final Matcher matcher = PARAM_REGEX.matcher(fmt); + + boolean result = matcher.find(); + + if (result) { + final StringBuffer sb = new StringBuffer(); + do { + String replacement; + + // Escaped characters are unchanged + if (matcher.group(1) != null) { + replacement = matcher.group(1); + } else { + final String paramName = matcher.group(3); + if (values.containsKey(paramName) == true) { + replacement = values.get(paramName).toString(); + } else { + throw new IllegalArgumentException("No parameter value for %(" + paramName + ")"); + } + } + + matcher.appendReplacement(sb, replacement); + result = matcher.find(); + } while (result); + + matcher.appendTail(sb); + return sb.toString(); + } + + return fmt; + } +} diff --git a/test/framework/src/test/java/org/elasticsearch/common/util/NamedFormatterTests.java b/test/framework/src/test/java/org/elasticsearch/common/util/NamedFormatterTests.java new file mode 100644 index 0000000000000..f0cdf5b6514dd --- /dev/null +++ b/test/framework/src/test/java/org/elasticsearch/common/util/NamedFormatterTests.java @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.util; + +import org.elasticsearch.test.ESTestCase; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.equalTo; + +public class NamedFormatterTests extends ESTestCase { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + public void testPatternAreFormatted() { + assertThat(NamedFormatter.format("Hello, %(name)!", singletonMap("name", "world")), equalTo("Hello, world!")); + } + + public void testDuplicatePatternsAreFormatted() { + assertThat(NamedFormatter.format("Hello, %(name) and %(name)!", singletonMap("name", "world")), equalTo("Hello, world and world!")); + } + + public void testMultiplePatternsAreFormatted() { + final Map values = new HashMap<>(); + values.put("name", "world"); + values.put("second_name", "fred"); + + assertThat( + NamedFormatter.format("Hello, %(name) and %(second_name)!", values), + equalTo("Hello, world and fred!") + ); + } + + public void testEscapedPatternsAreNotFormatted() { + assertThat(NamedFormatter.format("Hello, \\%(name)!", singletonMap("name", "world")), equalTo("Hello, %(name)!")); + } + + public void testUnknownPatternsThrowException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("No parameter value for %(name)"); + NamedFormatter.format("Hello, %(name)!", singletonMap("foo", "world")); + } +} 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 e8b406da90b77..cb2ada89e34d4 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 @@ -21,6 +21,7 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.NamedFormatter; import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; import org.hamcrest.Matchers; @@ -209,13 +210,30 @@ public void testParseEmptyContentIsRejected() throws Exception { public void testParseContentWithNoAssertionsIsRejected() throws Exception { Instant now = clock.instant(); - SamlToken token = token("\n" + - "" + - "" + - IDP_ENTITY_ID + "" + - "" + - ""); + final String xml = "\n" + + "" + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("now", now); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + + SamlToken token = token(NamedFormatter.format(xml, replacements)); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("No assertions found in SAML response")); assertThat(exception.getCause(), nullValue()); @@ -227,38 +245,59 @@ public void testSuccessfullyParseContentWithASingleValidAssertion() throws Excep Instant validUntil = now.plusSeconds(30); final String nameId = randomAlphaOfLengthBetween(12, 24); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + nameId + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " %(nameId)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("nameId", nameId); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final SamlAttributes attributes = authenticator.authenticate(token); assertThat(attributes, notNullValue()); assertThat(attributes.attributes(), iterableWithSize(1)); @@ -409,38 +448,56 @@ public void testIncorrectResponseIssuerIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "xxx" + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)xxx" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("Issuer")); assertThat(exception.getCause(), nullValue()); @@ -451,38 +508,56 @@ public void testIncorrectAssertionIssuerIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "_" + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)_" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("Issuer")); assertThat(exception.getCause(), nullValue()); @@ -493,38 +568,61 @@ public void testIncorrectDestinationIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = randomBoolean() ? token(signDoc(xml)) : token(signAssertions(xml, idpSigningCertificatePair)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + final String xmlWithReplacements = NamedFormatter.format(xml, replacements); + + SamlToken token = randomBoolean() + ? token(signDoc(xmlWithReplacements)) + : token(signAssertions(xmlWithReplacements, idpSigningCertificatePair)); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("destination")); assertThat(exception.getCause(), nullValue()); @@ -535,38 +633,56 @@ public void testMissingDestinationIsNotRejectedForNotSignedResponse() throws Exc Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signAssertions(xml, idpSigningCertificatePair)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signAssertions(NamedFormatter.format(xml, replacements), idpSigningCertificatePair)); final SamlAttributes attributes = authenticator.authenticate(token); assertThat(attributes, notNullValue()); assertThat(attributes.attributes(), iterableWithSize(1)); @@ -582,38 +698,58 @@ public void testIncorrectRequestIdIsRejected() throws Exception { Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); final String incorrectId = "_012345"; - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("incorrectId", incorrectId); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("in-response-to")); assertThat(exception.getMessage(), containsString(requestId)); @@ -626,38 +762,60 @@ public void testIncorrectRecipientIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("SAML Assertion SubjectConfirmationData Recipient")); assertThat(exception.getMessage(), containsString(SP_ACS_URL + "/fake")); @@ -667,25 +825,45 @@ public void testIncorrectRecipientIsRejected() throws Exception { public void testAssertionWithoutSubjectIsRejected() throws Exception { Instant now = clock.instant(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("randomId2", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("has no Subject")); assertThat(exception.getCause(), nullValue()); @@ -695,32 +873,52 @@ public void testAssertionWithoutSubjectIsRejected() throws Exception { public void testAssertionWithoutAuthnStatementIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("randomId2", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("Authn Statements while exactly one was expected.")); assertThat(exception.getCause(), nullValue()); @@ -733,39 +931,59 @@ public void testExpiredAuthnStatementSessionIsRejected() throws Exception { Instant sessionValidUntil = now.plusSeconds(60); final String nameId = randomAlphaOfLengthBetween(12, 24); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + nameId + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " %(nameId)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("nameId", nameId); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("sessionValidUntil", sessionValidUntil); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + // check that the content is valid "now" - final SamlToken token = token(signDoc(xml)); + final SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); assertThat(authenticator.authenticate(token), notNullValue()); // and still valid if we advance partway through the session expiry time @@ -790,40 +1008,59 @@ public void testIncorrectAuthnContextClassRefIsRejected() throws Exception { Instant validUntil = now.plusSeconds(30); final String nameId = randomAlphaOfLengthBetween(12, 24); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + nameId + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " %(nameId)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("nameId", nameId); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + SamlAuthenticator authenticatorWithReqAuthnCtx = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), Arrays.asList(X509_AUTHN_CTX, KERBEROS_AUTHN_CTX)); - SamlToken token = token(signDoc(xml)); + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticatorWithReqAuthnCtx.authenticate(token)); assertThat(exception.getMessage(), containsString("Rejecting SAML assertion as the AuthnContextClassRef")); assertThat(SamlUtils.isSamlException(exception), is(true)); @@ -831,28 +1068,46 @@ public void testIncorrectAuthnContextClassRefIsRejected() throws Exception { public void testAssertionWithoutSubjectConfirmationIsRejected() throws Exception { Instant now = clock.instant(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("now", now); + replacements.put("randomId", randomId()); + replacements.put("randomId2", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("SAML Assertion subject contains [0] bearer SubjectConfirmation")); assertThat(exception.getCause(), nullValue()); @@ -861,29 +1116,48 @@ public void testAssertionWithoutSubjectConfirmationIsRejected() throws Exception public void testAssertionWithoutSubjectConfirmationDataIsRejected() throws Exception { Instant now = clock.instant(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("randomId", randomId()); + replacements.put("randomId2", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("bearer SubjectConfirmation, while exactly one was expected.")); assertThat(exception.getCause(), nullValue()); @@ -894,38 +1168,57 @@ public void testAssetionWithoutBearerSubjectConfirmationMethodIsRejected() throw Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_ATTRIB_NAME", METHOD_ATTRIB_NAME); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("bearer SubjectConfirmation, while exactly one was expected.")); assertThat(exception.getCause(), nullValue()); @@ -937,38 +1230,61 @@ public void testIncorrectSubjectConfirmationDataInResponseToIsRejected() throws Instant validUntil = now.plusSeconds(30); final String incorrectId = "_123456"; final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("incorrectId", incorrectId); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token)); assertThat(exception.getMessage(), containsString("SAML Assertion SubjectConfirmationData is in-response-to")); assertThat(exception.getMessage(), containsString(requestId)); @@ -981,40 +1297,58 @@ public void testExpiredSubjectConfirmationDataIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(120); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); // check that the content is valid "now" - final SamlToken token = token(signDoc(xml)); + final SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); assertThat(authenticator.authenticate(token), notNullValue()); // and still valid if we advance partway through the expiry time @@ -1043,37 +1377,56 @@ public void testIdpInitiatedLoginIsAllowed() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; - final SamlToken token = token(signDoc(xml)); + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + final SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); final SamlAttributes attributes = authenticator.authenticate(token); assertThat(attributes, notNullValue()); assertThat(attributes.attributes(), iterableWithSize(1)); @@ -1084,45 +1437,67 @@ public void testIncorrectSigningKeyIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + final String xmlWithReplacements = NamedFormatter.format(xml, replacements); // check that the content is valid when signed by the correct key-pair - assertThat(authenticator.authenticate(token(signer.transform(xml, idpSigningCertificatePair))), notNullValue()); + assertThat(authenticator.authenticate(token(signer.transform(xmlWithReplacements, idpSigningCertificatePair))), notNullValue()); // check is rejected when signed by a different key-pair final Tuple wrongKey = readKeyPair("RSA_4096_updated"); - final ElasticsearchSecurityException exception = expectThrows(ElasticsearchSecurityException.class, - () -> authenticator.authenticate(token(signer.transform(xml, wrongKey)))); + final ElasticsearchSecurityException exception = expectThrows( + ElasticsearchSecurityException.class, + () -> authenticator.authenticate(token(signer.transform(xmlWithReplacements, wrongKey))) + ); assertThat(exception.getMessage(), containsString("SAML Signature")); assertThat(exception.getMessage(), containsString("could not be validated")); assertThat(exception.getCause(), nullValue()); @@ -1149,40 +1524,58 @@ public void testParsingRejectsTamperedContent() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); // check that the original signed content is valid - final String signed = signer.transform(xml, idpSigningCertificatePair); + final String signed = signer.transform(NamedFormatter.format(xml, replacements), idpSigningCertificatePair); assertThat(authenticator.authenticate(token(signed)), notNullValue()); // but altered content is rejected @@ -1208,41 +1601,61 @@ public void testSigningWhenIdpHasMultipleKeys() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(30); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + final String xmlWithReplacements = NamedFormatter.format(xml, replacements); // check that the content is valid when signed by the each of the key-pairs for (Tuple key : keys) { - assertThat(authenticator.authenticate(token(signer.transform(xml, key))), notNullValue()); + assertThat(authenticator.authenticate(token(signer.transform(xmlWithReplacements, key))), notNullValue()); } } @@ -1305,41 +1718,59 @@ public void testExpiredContentIsRejected() throws Exception { Instant now = clock.instant(); Instant validUntil = now.plusSeconds(120); final String sessionindex = randomId(); - final String xml = "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "randomopaquestring" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " randomopaquestring" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); // check that the content is valid "now" - final SamlToken token = token(signDoc(xml)); + final SamlToken token = token(signDoc(NamedFormatter.format(xml, replacements))); assertThat(authenticator.authenticate(token), notNullValue()); // and still valid if we advance partway through the expiry time @@ -2079,19 +2510,31 @@ private Response toResponse(String xml) throws SAXException, IOException, Parser private String getStatusFailedResponse() { final Instant now = clock.instant(); - return "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + - ""; + final String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("now", now); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("SP_ACS_URL", SP_ACS_URL); + + return NamedFormatter.format(xml, replacements); } private String getSimpleResponse(Instant now) { @@ -2101,43 +2544,66 @@ private String getSimpleResponse(Instant now) { private String getSimpleResponse(Instant now, String nameId, String sessionindex) { Instant validUntil = now.plusSeconds(30); - return "\n" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + - "" + IDP_ENTITY_ID + "" + - "" + - "" + nameId + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + PASSWORD_AUTHN_CTX + "" + - "" + - "" + - "" + - "daredevil" + - "" + - "" + - "defenders" + - "netflix" + - "" + - "" + - ""; + String xml = "\n" + + "" + + " %(IDP_ENTITY_ID)" + + " " + + " " + + " %(IDP_ENTITY_ID)" + + " " + + " %(nameId)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(PASSWORD_AUTHN_CTX)" + + " " + + " " + + " " + + " " + + " daredevil" + + " " + + " " + + " " + + " " + + " defenders" + + " netflix" + + " " + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); + replacements.put("METHOD_BEARER", METHOD_BEARER); + replacements.put("nameId", nameId); + replacements.put("now", now); + replacements.put("PASSWORD_AUTHN_CTX", PASSWORD_AUTHN_CTX); + replacements.put("randomId", randomId()); + replacements.put("requestId", requestId); + replacements.put("sessionindex", sessionindex); + replacements.put("SP_ACS_URL", SP_ACS_URL); + replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); + replacements.put("TRANSIENT", TRANSIENT); + replacements.put("validUntil", validUntil); + + return NamedFormatter.format(xml, replacements); } private String getResponseWithAudienceRestrictions(String... requiredAudiences) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilderTests.java index 1133a71993d19..7d30be36bc9fd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilderTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authc.saml; +import org.elasticsearch.common.util.NamedFormatter; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.hamcrest.Matchers; import org.junit.Before; @@ -20,10 +21,14 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; + public class SamlSpMetadataBuilderTests extends SamlTestCase { private X509Certificate certificate; @@ -67,16 +72,23 @@ public void testBuildMinimalistMetadata() throws Exception { final Element element = new EntityDescriptorMarshaller().marshall(descriptor); final String xml = SamlUtils.toString(element); - assertThat(xml, Matchers.equalTo("" + - "" + - "" + - "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + - "" + - "" + - "" - )); + + final String expectedXml = "" + + "" + + " " + + " urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + + " " + + " " + + ""; + + assertThat(xml, equalTo(normaliseXml(expectedXml))); assertValidXml(xml); } @@ -101,66 +113,87 @@ public void testBuildFullMetadata() throws Exception { final Element element = new EntityDescriptorMarshaller().marshall(descriptor); final String xml = SamlUtils.toString(element); - assertThat(xml, Matchers.equalTo("" + - "" + - "" + - "" + - "" + - "MIIDWDCCAkCgAwIBAgIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA0GCSqGSIb3DQEBCwUAMB0xGzAZ" + System.lineSeparator() + - "BgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDAeFw0xNzExMjkwMjQ3MjZaFw0yMDExMjgwMjQ3MjZa" + System.lineSeparator() + - "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC" + System.lineSeparator() + - "AQoCggEBALHTuPGOieCbD2mZUdYrdH4ofo7qFze6rQUROCLKqf69uBuwvraNWOcwxHUTKVlLMV3d" + System.lineSeparator() + - "dKzYo+yfC44AMXrrV+79xVWsTCNHu9sxQzcDwiEx2OtOOX9MAk6tJQ3svNrMPNXWh8ftwmmY9XdF" + System.lineSeparator() + - "ZwMYUdo6FPjSQj5uQTDmGWRgF08f7VRlk6N92d/fzn9DlDm+TFuaOr17OTSR4B6RTrNwKC29AmXQ" + System.lineSeparator() + - "TwCijCObjLqyMEqP20dZCQeVf2qw8JKUHhW4r6mCLzqmeR+kRTqiHMSWxJddzxDGw6X7fOS7iuzB" + System.lineSeparator() + - "0+TnsKwgu8nYrEXds9MkGf1Yco7WsM43g+Es+LhNHP+es70CAwEAAaOBjjCBizAdBgNVHQ4EFgQU" + System.lineSeparator() + - "ILqVKGhIi8p5Xffsow/IKFLhRbIwWQYDVR0jBFIwUIAUILqVKGhIi8p5Xffsow/IKFLhRbKhIaQf" + System.lineSeparator() + - "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTIIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA8G" + System.lineSeparator() + - "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGhl4V9mp4SWSV2E3HAJ1PX+Vmp6k27K" + System.lineSeparator() + - "d0tkOk1B9fyA13QB30teyiL7RR0vSHRyWFY8rQH1mHD366GKRWLITRG/QPULamGdYXX4h0pFj5ld" + System.lineSeparator() + - "aubLxM/O9vEAxOgmo/lsdkeIq9tLBqY06r/5A/Mcgo63KGi00AFYBoyvqfOu6nRLPnQr+rKVfdNO" + System.lineSeparator() + - "pWeIiFY1i2XTNZ3CZjNPSTwiQMUzrCxKXB9lL0vF6QL2Gj2iBhzNfXi88wf7xaR6XKY1wNuv3HLP" + System.lineSeparator() + - "sL7n+PWby7LRX188dyS1dmKfQcrKL65OssBA5NC8CAYyBiygBmWN+5kVJM5fSb0SwPSoVWrNyz+8" + System.lineSeparator() + - "IUldQE8=" + - "" + - "" + - "" + - "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + - "" + - "" + - "Hydra Kibana" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "Hydra" + - "Hydra" + - "https://hail.hydra/" + - "" + - "" + - "Wolfgang" + - "von Strucker" + - "baron.strucker@supreme.hydra" + - "" + - "" + - "Paul" + - "Ebersol" + - "pne@tech.hydra" + - "" + - "" - )); + + final String expectedCertificate = joinCertificateLines( + "MIIDWDCCAkCgAwIBAgIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA0GCSqGSIb3DQEBCwUAMB0xGzAZ", + "BgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDAeFw0xNzExMjkwMjQ3MjZaFw0yMDExMjgwMjQ3MjZa", + "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC", + "AQoCggEBALHTuPGOieCbD2mZUdYrdH4ofo7qFze6rQUROCLKqf69uBuwvraNWOcwxHUTKVlLMV3d", + "dKzYo+yfC44AMXrrV+79xVWsTCNHu9sxQzcDwiEx2OtOOX9MAk6tJQ3svNrMPNXWh8ftwmmY9XdF", + "ZwMYUdo6FPjSQj5uQTDmGWRgF08f7VRlk6N92d/fzn9DlDm+TFuaOr17OTSR4B6RTrNwKC29AmXQ", + "TwCijCObjLqyMEqP20dZCQeVf2qw8JKUHhW4r6mCLzqmeR+kRTqiHMSWxJddzxDGw6X7fOS7iuzB", + "0+TnsKwgu8nYrEXds9MkGf1Yco7WsM43g+Es+LhNHP+es70CAwEAAaOBjjCBizAdBgNVHQ4EFgQU", + "ILqVKGhIi8p5Xffsow/IKFLhRbIwWQYDVR0jBFIwUIAUILqVKGhIi8p5Xffsow/IKFLhRbKhIaQf", + "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTIIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA8G", + "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGhl4V9mp4SWSV2E3HAJ1PX+Vmp6k27K", + "d0tkOk1B9fyA13QB30teyiL7RR0vSHRyWFY8rQH1mHD366GKRWLITRG/QPULamGdYXX4h0pFj5ld", + "aubLxM/O9vEAxOgmo/lsdkeIq9tLBqY06r/5A/Mcgo63KGi00AFYBoyvqfOu6nRLPnQr+rKVfdNO", + "pWeIiFY1i2XTNZ3CZjNPSTwiQMUzrCxKXB9lL0vF6QL2Gj2iBhzNfXi88wf7xaR6XKY1wNuv3HLP", + "sL7n+PWby7LRX188dyS1dmKfQcrKL65OssBA5NC8CAYyBiygBmWN+5kVJM5fSb0SwPSoVWrNyz+8", + "IUldQE8=" + ); + + final String expectedXml = "" + + "" + + " " + + " " + + " " + + " " + + " %(expectedCertificate)" + + " " + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + " " + + " " + + " Hydra Kibana" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " Hydra" + + " Hydra" + + " https://hail.hydra/" + + " " + + " " + + " Wolfgang" + + " von Strucker" + + " baron.strucker@supreme.hydra" + + " " + + " " + + " Paul" + + " Ebersol" + + " pne@tech.hydra" + + " " + + ""; + + final Map replacements = Collections.singletonMap("expectedCertificate", expectedCertificate); + final String expectedXmlWithCertificate = NamedFormatter.format(expectedXml, replacements); + + assertThat(xml, equalTo(normaliseXml(expectedXmlWithCertificate))); + assertValidXml(xml); } @@ -185,110 +218,151 @@ public void testBuildFullMetadataWithSigningAndTwoEncryptionCerts() throws Excep final Element element = new EntityDescriptorMarshaller().marshall(descriptor); final String xml = SamlUtils.toString(element); - assertThat(xml, Matchers.equalTo("" + - "" + - "" + - "" + - "" + - "MIIDWDCCAkCgAwIBAgIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA0GCSqGSIb3DQEBCwUAMB0xGzAZ" + System.lineSeparator() + - "BgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDAeFw0xNzExMjkwMjQ3MjZaFw0yMDExMjgwMjQ3MjZa" + System.lineSeparator() + - "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC" + System.lineSeparator() + - "AQoCggEBALHTuPGOieCbD2mZUdYrdH4ofo7qFze6rQUROCLKqf69uBuwvraNWOcwxHUTKVlLMV3d" + System.lineSeparator() + - "dKzYo+yfC44AMXrrV+79xVWsTCNHu9sxQzcDwiEx2OtOOX9MAk6tJQ3svNrMPNXWh8ftwmmY9XdF" + System.lineSeparator() + - "ZwMYUdo6FPjSQj5uQTDmGWRgF08f7VRlk6N92d/fzn9DlDm+TFuaOr17OTSR4B6RTrNwKC29AmXQ" + System.lineSeparator() + - "TwCijCObjLqyMEqP20dZCQeVf2qw8JKUHhW4r6mCLzqmeR+kRTqiHMSWxJddzxDGw6X7fOS7iuzB" + System.lineSeparator() + - "0+TnsKwgu8nYrEXds9MkGf1Yco7WsM43g+Es+LhNHP+es70CAwEAAaOBjjCBizAdBgNVHQ4EFgQU" + System.lineSeparator() + - "ILqVKGhIi8p5Xffsow/IKFLhRbIwWQYDVR0jBFIwUIAUILqVKGhIi8p5Xffsow/IKFLhRbKhIaQf" + System.lineSeparator() + - "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTIIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA8G" + System.lineSeparator() + - "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGhl4V9mp4SWSV2E3HAJ1PX+Vmp6k27K" + System.lineSeparator() + - "d0tkOk1B9fyA13QB30teyiL7RR0vSHRyWFY8rQH1mHD366GKRWLITRG/QPULamGdYXX4h0pFj5ld" + System.lineSeparator() + - "aubLxM/O9vEAxOgmo/lsdkeIq9tLBqY06r/5A/Mcgo63KGi00AFYBoyvqfOu6nRLPnQr+rKVfdNO" + System.lineSeparator() + - "pWeIiFY1i2XTNZ3CZjNPSTwiQMUzrCxKXB9lL0vF6QL2Gj2iBhzNfXi88wf7xaR6XKY1wNuv3HLP" + System.lineSeparator() + - "sL7n+PWby7LRX188dyS1dmKfQcrKL65OssBA5NC8CAYyBiygBmWN+5kVJM5fSb0SwPSoVWrNyz+8" + System.lineSeparator() + - "IUldQE8=" + - "" + - "" + - "" + - "" + - "MIID0zCCArugAwIBAgIJALi5bDfjMszLMA0GCSqGSIb3DQEBCwUAMEgxDDAKBgNVBAoTA29yZzEW" + System.lineSeparator() + - "MBQGA1UECxMNZWxhc3RpY3NlYXJjaDEgMB4GA1UEAxMXRWxhc3RpY3NlYXJjaCBUZXN0IE5vZGUw" + System.lineSeparator() + - "HhcNMTUwOTIzMTg1MjU3WhcNMTkwOTIyMTg1MjU3WjBIMQwwCgYDVQQKEwNvcmcxFjAUBgNVBAsT" + System.lineSeparator() + - "DWVsYXN0aWNzZWFyY2gxIDAeBgNVBAMTF0VsYXN0aWNzZWFyY2ggVGVzdCBOb2RlMIIBIjANBgkq" + System.lineSeparator() + - "hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3rGZ1QbsW0+MuyrSLmMfDFKtLBkIFW8V0gRuurFg1PUK" + System.lineSeparator() + - "KNR1Mq2tMVwjjYETAU/UY0iKZOzjgvYPKhDTYBTte/WHR1ZK4CYVv7TQX/gtFQG/ge/c7u0sLch9" + System.lineSeparator() + - "p7fbd+/HZiLS/rBEZDIohvgUvzvnA8+OIYnw4kuxKo/5iboAIS41klMg/lATm8V71LMY68inht71" + System.lineSeparator() + - "/ZkQoAHKgcR9z4yNYvQ1WqKG8DG8KROXltll3sTrKbl5zJhn660es/1ZnR6nvwt6xnSTl/mNHMjk" + System.lineSeparator() + - "fv1bs4rJ/py3qPxicdoSIn/KyojUcgHVF38fuAy2CQTdjVG5fWj9iz+mQvLm3+qsIYQdFwIDAQAB" + System.lineSeparator() + - "o4G/MIG8MAkGA1UdEwQCMAAwHQYDVR0OBBYEFEMMWLWQi/g83PzlHYqAVnty5L7HMIGPBgNVHREE" + System.lineSeparator() + - "gYcwgYSCCWxvY2FsaG9zdIIVbG9jYWxob3N0LmxvY2FsZG9tYWluggpsb2NhbGhvc3Q0ghdsb2Nh" + System.lineSeparator() + - "bGhvc3Q0LmxvY2FsZG9tYWluNIIKbG9jYWxob3N0NoIXbG9jYWxob3N0Ni5sb2NhbGRvbWFpbjaH" + System.lineSeparator() + - "BH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBAMjGGXT8Nt1tbl2GkiKt" + System.lineSeparator() + - "miuGE2Ej66YuZ37WSJViaRNDVHLlg87TCcHek2rdO+6sFqQbbzEfwQ05T7xGmVu7tm54HwKMRugo" + System.lineSeparator() + - "Q3wct0bQC5wEWYN+oMDvSyO6M28mZwWb4VtR2IRyWP+ve5DHwTM9mxWa6rBlGzsQqH6YkJpZojzq" + System.lineSeparator() + - "k/mQTug+Y8aEmVoqRIPMHq9ob+S9qd5lp09+MtYpwPfTPx/NN+xMEooXWW/ARfpGhWPkg/FuCu4z" + System.lineSeparator() + - "1tFmCqHgNcWirzMm3dQpF78muE9ng6OB2MXQwL4VgnVkxmlZNHbkR2v/t8MyZJxCy4g6cTMM3S/U" + System.lineSeparator() + - "Mt5/+aIB2JAuMKyuD+A=" + - "" + - "" + - "" + - "" + - "MIID1zCCAr+gAwIBAgIJALnUl/KSS74pMA0GCSqGSIb3DQEBCwUAMEoxDDAKBgNVBAoTA29yZzEW" + System.lineSeparator() + - "MBQGA1UECxMNZWxhc3RpY3NlYXJjaDEiMCAGA1UEAxMZRWxhc3RpY3NlYXJjaCBUZXN0IENsaWVu" + System.lineSeparator() + - "dDAeFw0xNTA5MjMxODUyNTVaFw0xOTA5MjIxODUyNTVaMEoxDDAKBgNVBAoTA29yZzEWMBQGA1UE" + System.lineSeparator() + - "CxMNZWxhc3RpY3NlYXJjaDEiMCAGA1UEAxMZRWxhc3RpY3NlYXJjaCBUZXN0IENsaWVudDCCASIw" + System.lineSeparator() + - "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKm+P6vDAff0c6BWKGdhnYoNl9HijLIgfU3d9CQ" + System.lineSeparator() + - "cqKtwT+yUW3DPSVjIfaLmDIGj6Hl8jTHWPB7ZP4fzhrPi6m4qlRGclJMECBuNASZFiPDtEDv3mso" + System.lineSeparator() + - "eqOKQet6n7PZvgpWM7hxYZO4P1aMKJtRsFAdvBAdZUnv0spR5G4UZTHzSKmMeanIKFkLaD0XVKiL" + System.lineSeparator() + - "Qu9/z9M6roDQeAEoCJ/8JsanG8ih2ymfPHIZuNyYIOrVekHN2zU6bnVn8/PCeZSjS6h5xYw+Jl5g" + System.lineSeparator() + - "zGI/n+F5CZ+THoH8pM4pGp6xRVzpiH12gvERGwgSIDXdn/+uZZj+4lE7n2ENRSOt5KcOGG99r60C" + System.lineSeparator() + - "AwEAAaOBvzCBvDAJBgNVHRMEAjAAMB0GA1UdDgQWBBSSFhBXNp7AaNrHdlgCV0mCEzt7ajCBjwYD" + System.lineSeparator() + - "VR0RBIGHMIGEgglsb2NhbGhvc3SCFWxvY2FsaG9zdC5sb2NhbGRvbWFpboIKbG9jYWxob3N0NIIX" + System.lineSeparator() + - "bG9jYWxob3N0NC5sb2NhbGRvbWFpbjSCCmxvY2FsaG9zdDaCF2xvY2FsaG9zdDYubG9jYWxkb21h" + System.lineSeparator() + - "aW42hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQANvAkddfLxn4/B" + System.lineSeparator() + - "CY4LY/1ET3d7ZRldjFTyjjHRYJ3CYBXWVahMskLxIcFNca8YjKfXoX8mcK+NQK/dAbGHXqk76yMl" + System.lineSeparator() + - "krKjh1OQiZ1YAX5ryYerGrZ99N3E9wnbn72bW3iumoLlqmTWlHEpMI0Ql6J75BQLTgKHxCPupVA5" + System.lineSeparator() + - "sTbWkKwGjXXAi84rUlzhDJOR8jk3/7ct0iZO8Hk6AWMcNix5Wka3IDGUXuEVevYRlxgVyCxcnZWC" + System.lineSeparator() + - "7JWREpar5aIPQFkY6VCEglxwUyXbHZw5T/u6XaKKnS7gz8RiwRh68ddSQJeEHi5e4onUD7bOCJgf" + System.lineSeparator() + - "siUwdiCkDbfN9Yum8OIpmBRs" + - "" + - "" + - "" + - "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + - "" + - "" + - "Hydra Kibana" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "Hydra" + - "Hydra" + - "https://hail.hydra/" + - "" + - "" + - "Wolfgang" + - "von Strucker" + - "baron.strucker@supreme.hydra" + - "" + - "" + - "Paul" + - "Ebersol" + - "pne@tech.hydra" + - "" + - "" - )); + + final String expectedCertificateOne = joinCertificateLines( + "MIIDWDCCAkCgAwIBAgIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA0GCSqGSIb3DQEBCwUAMB0xGzAZ", + "BgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDAeFw0xNzExMjkwMjQ3MjZaFw0yMDExMjgwMjQ3MjZa", + "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC", + "AQoCggEBALHTuPGOieCbD2mZUdYrdH4ofo7qFze6rQUROCLKqf69uBuwvraNWOcwxHUTKVlLMV3d", + "dKzYo+yfC44AMXrrV+79xVWsTCNHu9sxQzcDwiEx2OtOOX9MAk6tJQ3svNrMPNXWh8ftwmmY9XdF", + "ZwMYUdo6FPjSQj5uQTDmGWRgF08f7VRlk6N92d/fzn9DlDm+TFuaOr17OTSR4B6RTrNwKC29AmXQ", + "TwCijCObjLqyMEqP20dZCQeVf2qw8JKUHhW4r6mCLzqmeR+kRTqiHMSWxJddzxDGw6X7fOS7iuzB", + "0+TnsKwgu8nYrEXds9MkGf1Yco7WsM43g+Es+LhNHP+es70CAwEAAaOBjjCBizAdBgNVHQ4EFgQU", + "ILqVKGhIi8p5Xffsow/IKFLhRbIwWQYDVR0jBFIwUIAUILqVKGhIi8p5Xffsow/IKFLhRbKhIaQf", + "MB0xGzAZBgNVBAMTEkVsYXN0aWNzZWFyY2gtU0FNTIIVANRTZaFrK+Pz19O8TZsb3HSJmAWpMA8G", + "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGhl4V9mp4SWSV2E3HAJ1PX+Vmp6k27K", + "d0tkOk1B9fyA13QB30teyiL7RR0vSHRyWFY8rQH1mHD366GKRWLITRG/QPULamGdYXX4h0pFj5ld", + "aubLxM/O9vEAxOgmo/lsdkeIq9tLBqY06r/5A/Mcgo63KGi00AFYBoyvqfOu6nRLPnQr+rKVfdNO", + "pWeIiFY1i2XTNZ3CZjNPSTwiQMUzrCxKXB9lL0vF6QL2Gj2iBhzNfXi88wf7xaR6XKY1wNuv3HLP", + "sL7n+PWby7LRX188dyS1dmKfQcrKL65OssBA5NC8CAYyBiygBmWN+5kVJM5fSb0SwPSoVWrNyz+8", + "IUldQE8=" + ); + + final String expectedCertificateTwo = joinCertificateLines( + "MIID0zCCArugAwIBAgIJALi5bDfjMszLMA0GCSqGSIb3DQEBCwUAMEgxDDAKBgNVBAoTA29yZzEW", + "MBQGA1UECxMNZWxhc3RpY3NlYXJjaDEgMB4GA1UEAxMXRWxhc3RpY3NlYXJjaCBUZXN0IE5vZGUw", + "HhcNMTUwOTIzMTg1MjU3WhcNMTkwOTIyMTg1MjU3WjBIMQwwCgYDVQQKEwNvcmcxFjAUBgNVBAsT", + "DWVsYXN0aWNzZWFyY2gxIDAeBgNVBAMTF0VsYXN0aWNzZWFyY2ggVGVzdCBOb2RlMIIBIjANBgkq", + "hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3rGZ1QbsW0+MuyrSLmMfDFKtLBkIFW8V0gRuurFg1PUK", + "KNR1Mq2tMVwjjYETAU/UY0iKZOzjgvYPKhDTYBTte/WHR1ZK4CYVv7TQX/gtFQG/ge/c7u0sLch9", + "p7fbd+/HZiLS/rBEZDIohvgUvzvnA8+OIYnw4kuxKo/5iboAIS41klMg/lATm8V71LMY68inht71", + "/ZkQoAHKgcR9z4yNYvQ1WqKG8DG8KROXltll3sTrKbl5zJhn660es/1ZnR6nvwt6xnSTl/mNHMjk", + "fv1bs4rJ/py3qPxicdoSIn/KyojUcgHVF38fuAy2CQTdjVG5fWj9iz+mQvLm3+qsIYQdFwIDAQAB", + "o4G/MIG8MAkGA1UdEwQCMAAwHQYDVR0OBBYEFEMMWLWQi/g83PzlHYqAVnty5L7HMIGPBgNVHREE", + "gYcwgYSCCWxvY2FsaG9zdIIVbG9jYWxob3N0LmxvY2FsZG9tYWluggpsb2NhbGhvc3Q0ghdsb2Nh", + "bGhvc3Q0LmxvY2FsZG9tYWluNIIKbG9jYWxob3N0NoIXbG9jYWxob3N0Ni5sb2NhbGRvbWFpbjaH", + "BH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBAMjGGXT8Nt1tbl2GkiKt", + "miuGE2Ej66YuZ37WSJViaRNDVHLlg87TCcHek2rdO+6sFqQbbzEfwQ05T7xGmVu7tm54HwKMRugo", + "Q3wct0bQC5wEWYN+oMDvSyO6M28mZwWb4VtR2IRyWP+ve5DHwTM9mxWa6rBlGzsQqH6YkJpZojzq", + "k/mQTug+Y8aEmVoqRIPMHq9ob+S9qd5lp09+MtYpwPfTPx/NN+xMEooXWW/ARfpGhWPkg/FuCu4z", + "1tFmCqHgNcWirzMm3dQpF78muE9ng6OB2MXQwL4VgnVkxmlZNHbkR2v/t8MyZJxCy4g6cTMM3S/U", + "Mt5/+aIB2JAuMKyuD+A=" + ); + + final String expectedCertificateThree = joinCertificateLines( + "MIID1zCCAr+gAwIBAgIJALnUl/KSS74pMA0GCSqGSIb3DQEBCwUAMEoxDDAKBgNVBAoTA29yZzEW", + "MBQGA1UECxMNZWxhc3RpY3NlYXJjaDEiMCAGA1UEAxMZRWxhc3RpY3NlYXJjaCBUZXN0IENsaWVu", + "dDAeFw0xNTA5MjMxODUyNTVaFw0xOTA5MjIxODUyNTVaMEoxDDAKBgNVBAoTA29yZzEWMBQGA1UE", + "CxMNZWxhc3RpY3NlYXJjaDEiMCAGA1UEAxMZRWxhc3RpY3NlYXJjaCBUZXN0IENsaWVudDCCASIw", + "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKm+P6vDAff0c6BWKGdhnYoNl9HijLIgfU3d9CQ", + "cqKtwT+yUW3DPSVjIfaLmDIGj6Hl8jTHWPB7ZP4fzhrPi6m4qlRGclJMECBuNASZFiPDtEDv3mso", + "eqOKQet6n7PZvgpWM7hxYZO4P1aMKJtRsFAdvBAdZUnv0spR5G4UZTHzSKmMeanIKFkLaD0XVKiL", + "Qu9/z9M6roDQeAEoCJ/8JsanG8ih2ymfPHIZuNyYIOrVekHN2zU6bnVn8/PCeZSjS6h5xYw+Jl5g", + "zGI/n+F5CZ+THoH8pM4pGp6xRVzpiH12gvERGwgSIDXdn/+uZZj+4lE7n2ENRSOt5KcOGG99r60C", + "AwEAAaOBvzCBvDAJBgNVHRMEAjAAMB0GA1UdDgQWBBSSFhBXNp7AaNrHdlgCV0mCEzt7ajCBjwYD", + "VR0RBIGHMIGEgglsb2NhbGhvc3SCFWxvY2FsaG9zdC5sb2NhbGRvbWFpboIKbG9jYWxob3N0NIIX", + "bG9jYWxob3N0NC5sb2NhbGRvbWFpbjSCCmxvY2FsaG9zdDaCF2xvY2FsaG9zdDYubG9jYWxkb21h", + "aW42hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQANvAkddfLxn4/B", + "CY4LY/1ET3d7ZRldjFTyjjHRYJ3CYBXWVahMskLxIcFNca8YjKfXoX8mcK+NQK/dAbGHXqk76yMl", + "krKjh1OQiZ1YAX5ryYerGrZ99N3E9wnbn72bW3iumoLlqmTWlHEpMI0Ql6J75BQLTgKHxCPupVA5", + "sTbWkKwGjXXAi84rUlzhDJOR8jk3/7ct0iZO8Hk6AWMcNix5Wka3IDGUXuEVevYRlxgVyCxcnZWC", + "7JWREpar5aIPQFkY6VCEglxwUyXbHZw5T/u6XaKKnS7gz8RiwRh68ddSQJeEHi5e4onUD7bOCJgf", + "siUwdiCkDbfN9Yum8OIpmBRs" + ); + + final String expectedXml = "" + + "" + + " " + + " " + + " " + + " " + + " %(expectedCertificateOne)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(expectedCertificateTwo)" + + " " + + " " + + " " + + " " + + " " + + " " + + " %(expectedCertificateThree)" + + " " + + " " + + " " + + " " + + " urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + + " " + + " " + + " Hydra Kibana" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " Hydra" + + " Hydra" + + " https://hail.hydra/" + + " " + + " " + + " Wolfgang" + + " von Strucker" + + " baron.strucker@supreme.hydra" + + " " + + " " + + " Paul" + + " Ebersol" + + " pne@tech.hydra" + + " " + + ""; + + final Map replacements = new HashMap<>(); + replacements.put("expectedCertificateOne", expectedCertificateOne); + replacements.put("expectedCertificateTwo", expectedCertificateTwo); + replacements.put("expectedCertificateThree", expectedCertificateThree); + + final String expectedXmlWithCertificate = NamedFormatter.format(expectedXml, replacements); + + assertThat(xml, equalTo(normaliseXml(expectedXmlWithCertificate))); + assertValidXml(xml); } @@ -307,4 +381,14 @@ public void testAttributeNameIsRequired() { private void assertValidXml(String xml) throws Exception { SamlUtils.validate(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), SamlMetadataCommand.METADATA_SCHEMA); } -} \ No newline at end of file + + private String joinCertificateLines(String... lines) { + return Arrays.stream(lines).collect(Collectors.joining(System.lineSeparator())); + } + + private String normaliseXml(String input) { + // Remove spaces between elements, and compress other spaces. These patterns don't use \s because + // that would match newlines. + return input.replaceAll("> +<", "><").replaceAll(" +", " "); + } +}