|
18 | 18 | import org.elasticsearch.common.util.concurrent.ThreadContext;
|
19 | 19 | import org.elasticsearch.core.Nullable;
|
20 | 20 | import org.elasticsearch.xcontent.ObjectPath;
|
| 21 | +import org.elasticsearch.xcontent.XContentBuilder; |
21 | 22 | import org.elasticsearch.xcontent.json.JsonXContent;
|
22 | 23 | import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationResponse;
|
23 | 24 | import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex;
|
24 | 25 | import org.junit.Before;
|
| 26 | +import org.w3c.dom.Document; |
| 27 | +import org.w3c.dom.Element; |
| 28 | +import org.w3c.dom.NodeList; |
| 29 | +import org.xml.sax.InputSource; |
25 | 30 |
|
26 | 31 | import java.io.IOException;
|
| 32 | +import java.io.StringReader; |
27 | 33 | import java.nio.charset.StandardCharsets;
|
| 34 | +import java.util.ArrayList; |
28 | 35 | import java.util.Base64;
|
29 | 36 | import java.util.List;
|
30 | 37 | import java.util.Map;
|
31 | 38 | import java.util.Set;
|
32 | 39 |
|
| 40 | +import javax.xml.parsers.DocumentBuilder; |
| 41 | +import javax.xml.parsers.DocumentBuilderFactory; |
| 42 | +import javax.xml.xpath.XPath; |
| 43 | +import javax.xml.xpath.XPathConstants; |
| 44 | +import javax.xml.xpath.XPathFactory; |
| 45 | + |
33 | 46 | import static org.hamcrest.Matchers.containsInAnyOrder;
|
34 | 47 | import static org.hamcrest.Matchers.containsString;
|
35 | 48 | import static org.hamcrest.Matchers.equalTo;
|
36 | 49 | import static org.hamcrest.Matchers.hasSize;
|
37 | 50 | import static org.hamcrest.Matchers.instanceOf;
|
| 51 | +import static org.hamcrest.Matchers.is; |
| 52 | +import static org.hamcrest.Matchers.notNullValue; |
38 | 53 |
|
39 | 54 | public class IdentityProviderAuthenticationIT extends IdpRestTestCase {
|
40 | 55 |
|
@@ -74,6 +89,81 @@ public void testRegistrationAndIdpInitiatedSso() throws Exception {
|
74 | 89 | authenticateWithSamlResponse(samlResponse, null);
|
75 | 90 | }
|
76 | 91 |
|
| 92 | + public void testCustomAttributesInIdpInitiatedSso() throws Exception { |
| 93 | + final Map<String, Object> request = Map.ofEntries( |
| 94 | + Map.entry("name", "Test SP With Custom Attributes"), |
| 95 | + Map.entry("acs", SP_ACS), |
| 96 | + Map.entry("privileges", Map.ofEntries(Map.entry("resource", SP_ENTITY_ID), Map.entry("roles", List.of("sso:(\\w+)")))), |
| 97 | + Map.entry( |
| 98 | + "attributes", |
| 99 | + Map.ofEntries( |
| 100 | + Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"), |
| 101 | + Map.entry("name", "https://idp.test.es.elasticsearch.org/attribute/name"), |
| 102 | + Map.entry("email", "https://idp.test.es.elasticsearch.org/attribute/email"), |
| 103 | + Map.entry("roles", "https://idp.test.es.elasticsearch.org/attribute/roles") |
| 104 | + ) |
| 105 | + ) |
| 106 | + ); |
| 107 | + final SamlServiceProviderIndex.DocumentVersion docVersion = createServiceProvider(SP_ENTITY_ID, request); |
| 108 | + checkIndexDoc(docVersion); |
| 109 | + ensureGreen(SamlServiceProviderIndex.INDEX_NAME); |
| 110 | + |
| 111 | + // Create custom attributes |
| 112 | + Map<String, List<String>> attributesMap = Map.of("department", List.of("engineering", "product"), "region", List.of("APJ")); |
| 113 | + |
| 114 | + // Generate SAML response with custom attributes |
| 115 | + final String samlResponse = generateSamlResponseWithAttributes(SP_ENTITY_ID, SP_ACS, null, attributesMap); |
| 116 | + |
| 117 | + // Parse XML directly from samlResponse (it's not base64 encoded at this point) |
| 118 | + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| 119 | + factory.setNamespaceAware(true); // Required for XPath |
| 120 | + DocumentBuilder builder = factory.newDocumentBuilder(); |
| 121 | + Document document = builder.parse(new InputSource(new StringReader(samlResponse))); |
| 122 | + |
| 123 | + // Create XPath evaluator |
| 124 | + XPathFactory xPathFactory = XPathFactory.newInstance(); |
| 125 | + XPath xpath = xPathFactory.newXPath(); |
| 126 | + |
| 127 | + // Validate SAML Response structure |
| 128 | + Element responseElement = (Element) xpath.evaluate("//*[local-name()='Response']", document, XPathConstants.NODE); |
| 129 | + assertThat("SAML Response element should exist", responseElement, notNullValue()); |
| 130 | + |
| 131 | + Element assertionElement = (Element) xpath.evaluate("//*[local-name()='Assertion']", document, XPathConstants.NODE); |
| 132 | + assertThat("SAML Assertion element should exist", assertionElement, notNullValue()); |
| 133 | + |
| 134 | + // Validate department attribute |
| 135 | + NodeList departmentAttributes = (NodeList) xpath.evaluate( |
| 136 | + "//*[local-name()='Attribute' and @Name='department']/*[local-name()='AttributeValue']", |
| 137 | + document, |
| 138 | + XPathConstants.NODESET |
| 139 | + ); |
| 140 | + |
| 141 | + assertThat("Should have two values for department attribute", departmentAttributes.getLength(), is(2)); |
| 142 | + |
| 143 | + // Verify department values |
| 144 | + List<String> departmentValues = new ArrayList<>(); |
| 145 | + for (int i = 0; i < departmentAttributes.getLength(); i++) { |
| 146 | + departmentValues.add(departmentAttributes.item(i).getTextContent()); |
| 147 | + } |
| 148 | + assertThat( |
| 149 | + "Department attribute should contain 'engineering' and 'product'", |
| 150 | + departmentValues, |
| 151 | + containsInAnyOrder("engineering", "product") |
| 152 | + ); |
| 153 | + |
| 154 | + // Validate region attribute |
| 155 | + NodeList regionAttributes = (NodeList) xpath.evaluate( |
| 156 | + "//*[local-name()='Attribute' and @Name='region']/*[local-name()='AttributeValue']", |
| 157 | + document, |
| 158 | + XPathConstants.NODESET |
| 159 | + ); |
| 160 | + |
| 161 | + assertThat("Should have one value for region attribute", regionAttributes.getLength(), is(1)); |
| 162 | + assertThat("Region attribute should contain 'APJ'", regionAttributes.item(0).getTextContent(), equalTo("APJ")); |
| 163 | + |
| 164 | + authenticateWithSamlResponse(samlResponse, null); |
| 165 | + } |
| 166 | + |
77 | 167 | public void testRegistrationAndSpInitiatedSso() throws Exception {
|
78 | 168 | final Map<String, Object> request = Map.ofEntries(
|
79 | 169 | Map.entry("name", "Test SP"),
|
@@ -125,17 +215,37 @@ private SamlPrepareAuthenticationResponse generateSamlAuthnRequest(String realmN
|
125 | 215 | }
|
126 | 216 | }
|
127 | 217 |
|
128 |
| - private String generateSamlResponse(String entityId, String acs, @Nullable Map<String, Object> authnState) throws Exception { |
| 218 | + private String generateSamlResponse(String entityId, String acs, @Nullable Map<String, Object> authnState) throws IOException { |
| 219 | + return generateSamlResponseWithAttributes(entityId, acs, authnState, null); |
| 220 | + } |
| 221 | + |
| 222 | + private String generateSamlResponseWithAttributes( |
| 223 | + String entityId, |
| 224 | + String acs, |
| 225 | + @Nullable Map<String, Object> authnState, |
| 226 | + @Nullable Map<String, List<String>> attributes |
| 227 | + ) throws IOException { |
129 | 228 | final Request request = new Request("POST", "/_idp/saml/init");
|
130 |
| - if (authnState != null && authnState.isEmpty() == false) { |
131 |
| - request.setJsonEntity(Strings.format(""" |
132 |
| - {"entity_id":"%s", "acs":"%s","authn_state":%s} |
133 |
| - """, entityId, acs, Strings.toString(JsonXContent.contentBuilder().map(authnState)))); |
134 |
| - } else { |
135 |
| - request.setJsonEntity(Strings.format(""" |
136 |
| - {"entity_id":"%s", "acs":"%s"} |
137 |
| - """, entityId, acs)); |
| 229 | + |
| 230 | + XContentBuilder builder = JsonXContent.contentBuilder(); |
| 231 | + builder.startObject(); |
| 232 | + builder.field("entity_id", entityId); |
| 233 | + builder.field("acs", acs); |
| 234 | + |
| 235 | + if (authnState != null) { |
| 236 | + builder.field("authn_state"); |
| 237 | + builder.map(authnState); |
| 238 | + } |
| 239 | + |
| 240 | + if (attributes != null) { |
| 241 | + builder.field("attributes"); |
| 242 | + builder.map(attributes); |
138 | 243 | }
|
| 244 | + |
| 245 | + builder.endObject(); |
| 246 | + String jsonEntity = Strings.toString(builder); |
| 247 | + |
| 248 | + request.setJsonEntity(jsonEntity); |
139 | 249 | request.setOptions(
|
140 | 250 | RequestOptions.DEFAULT.toBuilder()
|
141 | 251 | .addHeader("es-secondary-authorization", basicAuthHeaderValue("idp_user", new SecureString("idp-password".toCharArray())))
|
|
0 commit comments