-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Use the same ES cluster as both an SP and an IDP and perform IDP initiated and SP initiated SSO. The REST client plays the role of both the Cloud UI and Kibana in these flows
- Loading branch information
Showing
8 changed files
with
332 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
...est-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.idp; | ||
|
||
import org.apache.http.HttpHost; | ||
import org.elasticsearch.client.Request; | ||
import org.elasticsearch.client.RequestOptions; | ||
import org.elasticsearch.client.Response; | ||
import org.elasticsearch.client.RestClient; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.settings.SecureString; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.util.concurrent.ThreadContext; | ||
import org.elasticsearch.common.xcontent.ObjectPath; | ||
import org.elasticsearch.common.xcontent.json.JsonXContent; | ||
import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationResponse; | ||
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex; | ||
import org.junit.Before; | ||
|
||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; | ||
import static org.hamcrest.Matchers.contains; | ||
import static org.hamcrest.Matchers.containsString; | ||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.hasSize; | ||
import static org.hamcrest.Matchers.instanceOf; | ||
|
||
public class IdentityProviderAuthenticationIT extends IdpRestTestCase { | ||
|
||
// From build.gradle | ||
private final String SP_ENTITY_ID = "ec:123456:abcdefg"; | ||
private final String SP_ACS = "https://sp1.test.es.elasticsearch.org/saml/acs"; | ||
private final String REALM_NAME = "cloud-saml"; | ||
|
||
@Before | ||
public void createUsers() throws IOException { | ||
setUserPassword("kibana", new SecureString("kibana".toCharArray())); | ||
} | ||
|
||
public void testRegistrationAndIdpInitiatedSso() throws Exception { | ||
final Map<String, Object> request = new HashMap<>(); | ||
request.put("name", "Test SP"); | ||
request.put("acs", SP_ACS); | ||
final Map<String, Object> privilegeMap = new HashMap<>(); | ||
privilegeMap.put("resource", SP_ENTITY_ID); | ||
final Map<String, String> roleMap = new HashMap<>(); | ||
roleMap.put("superuser", "role:superuser"); | ||
roleMap.put("viewer", "role:viewer"); | ||
privilegeMap.put("roles", roleMap); | ||
request.put("privileges", privilegeMap); | ||
final Map<String, String> attributeMap = new HashMap<>(); | ||
attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"); | ||
attributeMap.put("name", "https://idp.test.es.elasticsearch.org/attribute/name"); | ||
attributeMap.put("email", "https://idp.test.es.elasticsearch.org/attribute/email"); | ||
attributeMap.put("roles", "https://idp.test.es.elasticsearch.org/attribute/roles"); | ||
request.put("attributes", attributeMap); | ||
final SamlServiceProviderIndex.DocumentVersion docVersion = createServiceProvider(SP_ENTITY_ID, request); | ||
checkIndexDoc(docVersion); | ||
ensureGreen(SamlServiceProviderIndex.INDEX_NAME); | ||
final String samlResponse = generateSamlResponse(SP_ENTITY_ID, SP_ACS, null); | ||
authenticateWithSamlResponse(samlResponse, null); | ||
} | ||
|
||
public void testRegistrationAndSpInitiatedSso() throws Exception { | ||
final Map<String, Object> request = new HashMap<>(); | ||
request.put("name", "Test SP"); | ||
request.put("acs", SP_ACS); | ||
final Map<String, Object> privilegeMap = new HashMap<>(); | ||
privilegeMap.put("resource", SP_ENTITY_ID); | ||
final Map<String, String> roleMap = new HashMap<>(); | ||
roleMap.put("superuser", "role:superuser"); | ||
roleMap.put("viewer", "role:viewer"); | ||
privilegeMap.put("roles", roleMap); | ||
request.put("privileges", privilegeMap); | ||
final Map<String, String> attributeMap = new HashMap<>(); | ||
attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"); | ||
attributeMap.put("name", "https://idp.test.es.elasticsearch.org/attribute/name"); | ||
attributeMap.put("email", "https://idp.test.es.elasticsearch.org/attribute/email"); | ||
attributeMap.put("roles", "https://idp.test.es.elasticsearch.org/attribute/roles"); | ||
request.put("attributes", attributeMap); | ||
final SamlServiceProviderIndex.DocumentVersion docVersion = createServiceProvider(SP_ENTITY_ID, request); | ||
checkIndexDoc(docVersion); | ||
ensureGreen(SamlServiceProviderIndex.INDEX_NAME); | ||
SamlPrepareAuthenticationResponse samlPrepareAuthenticationResponse = generateSamlAuthnRequest(REALM_NAME); | ||
final String requestId = samlPrepareAuthenticationResponse.getRequestId(); | ||
final String query = samlPrepareAuthenticationResponse.getRedirectUrl().split("\\?")[1]; | ||
Map<String, Object> authnState = validateAuthnRequest(SP_ENTITY_ID, query); | ||
final String samlResponse = generateSamlResponse(SP_ENTITY_ID, SP_ACS, authnState); | ||
assertThat(samlResponse, containsString("InResponseTo=\"" + requestId + "\"")); | ||
authenticateWithSamlResponse(samlResponse, requestId); | ||
} | ||
|
||
private Map<String, Object> validateAuthnRequest(String entityId, String authnRequestQuery) throws Exception { | ||
final Request request = new Request("POST", "/_idp/saml/validate"); | ||
request.setJsonEntity("{\"authn_request_query\":\"" + authnRequestQuery + "\"}"); | ||
final Response response = client().performRequest(request); | ||
final Map<String, Object> map = entityAsMap(response); | ||
assertThat(ObjectPath.eval("service_provider.entity_id", map), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("service_provider.entity_id", map), equalTo(entityId)); | ||
assertThat(ObjectPath.eval("authn_state", map), instanceOf(Map.class)); | ||
return ObjectPath.eval("authn_state", map); | ||
} | ||
|
||
private SamlPrepareAuthenticationResponse generateSamlAuthnRequest(String realmName) throws Exception { | ||
final Request request = new Request("POST", "/_security/saml/prepare"); | ||
request.setJsonEntity("{\"realm\":\"" + realmName + "\"}"); | ||
try (RestClient kibanaClient = restClientAsKibana()) { | ||
final Response response = kibanaClient.performRequest(request); | ||
final Map<String, Object> map = entityAsMap(response); | ||
assertThat(ObjectPath.eval("realm", map), equalTo(realmName)); | ||
assertThat(ObjectPath.eval("id", map), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("redirect", map), instanceOf(String.class)); | ||
return new SamlPrepareAuthenticationResponse(realmName, ObjectPath.eval("id", map), ObjectPath.eval("redirect", map)); | ||
} | ||
} | ||
|
||
private String generateSamlResponse(String entityId, String acs, @Nullable Map<String, Object> authnState) throws Exception { | ||
final Request request = new Request("POST", "/_idp/saml/init"); | ||
if (authnState != null && authnState.isEmpty() == false) { | ||
request.setJsonEntity("{\"entity_id\":\"" + entityId + "\", \"acs\":\"" + acs + "\"," + | ||
"\"authn_state\":" + Strings.toString(JsonXContent.contentBuilder().map(authnState)) + "}"); | ||
} else { | ||
request.setJsonEntity("{\"entity_id\":\"" + entityId + "\", \"acs\":\"" + acs + "\"}"); | ||
} | ||
request.setOptions(RequestOptions.DEFAULT.toBuilder() | ||
.addHeader("es-secondary-authorization", basicAuthHeaderValue("idp_user", | ||
new SecureString("idp-password".toCharArray()))) | ||
.build()); | ||
final Response response = client().performRequest(request); | ||
final Map<String, Object> map = entityAsMap(response); | ||
assertThat(ObjectPath.eval("service_provider.entity_id", map), equalTo(entityId)); | ||
assertThat(ObjectPath.eval("post_url", map), equalTo(acs)); | ||
assertThat(ObjectPath.eval("saml_response", map), instanceOf(String.class)); | ||
return (String) ObjectPath.eval("saml_response", map); | ||
} | ||
|
||
private void authenticateWithSamlResponse(String samlResponse, @Nullable String id) throws Exception { | ||
final String encodedResponse = Base64.getEncoder().encodeToString(samlResponse.getBytes(StandardCharsets.UTF_8)); | ||
final Request request = new Request("POST", "/_security/saml/authenticate"); | ||
if (Strings.hasText(id)) { | ||
request.setJsonEntity("{\"content\":\"" + encodedResponse + "\", \"realm\":\"" + REALM_NAME + "\", \"ids\":[\"" + id + "\"]}"); | ||
} else { | ||
request.setJsonEntity("{\"content\":\"" + encodedResponse + "\", \"realm\":\"" + REALM_NAME + "\"}"); | ||
} | ||
final String accessToken; | ||
try (RestClient kibanaClient = restClientAsKibana()) { | ||
final Response response = kibanaClient.performRequest(request); | ||
final Map<String, Object> map = entityAsMap(response); | ||
assertThat(ObjectPath.eval("username", map), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("username", map), equalTo("idp_user")); | ||
assertThat(ObjectPath.eval("realm", map), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("realm", map), equalTo(REALM_NAME)); | ||
assertThat(ObjectPath.eval("access_token", map), instanceOf(String.class)); | ||
accessToken = ObjectPath.eval("access_token", map); | ||
assertThat(ObjectPath.eval("refresh_token", map), instanceOf(String.class)); | ||
} | ||
try (RestClient accessTokenClient = restClientWithToken(accessToken)) { | ||
final Request authenticateRequest = new Request("GET", "/_security/_authenticate"); | ||
final Response authenticateResponse = accessTokenClient.performRequest(authenticateRequest); | ||
final Map<String, Object> authMap = entityAsMap(authenticateResponse); | ||
assertThat(ObjectPath.eval("username", authMap), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("username", authMap), equalTo("idp_user")); | ||
assertThat(ObjectPath.eval("metadata.saml_nameid_format", authMap), instanceOf(String.class)); | ||
assertThat(ObjectPath.eval("metadata.saml_nameid_format", authMap), | ||
equalTo("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")); | ||
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), instanceOf(List.class)); | ||
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), hasSize(1)); | ||
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), contains("viewer")); | ||
} | ||
} | ||
|
||
private RestClient restClientWithToken(String accessToken) throws IOException { | ||
return buildClient( | ||
Settings.builder().put(ThreadContext.PREFIX + ".Authorization", "Bearer " + accessToken).build(), | ||
getClusterHosts().toArray(new HttpHost[getClusterHosts().size()])); | ||
} | ||
|
||
private RestClient restClientAsKibana() throws IOException { | ||
return buildClient( | ||
Settings.builder().put(ThreadContext.PREFIX + ".Authorization", basicAuthHeaderValue("kibana", | ||
new SecureString("kibana".toCharArray()))).build(), | ||
getClusterHosts().toArray(new HttpHost[getClusterHosts().size()])); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.