mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-17 10:25:15 +00:00
When the SAML authentication is not successful, we return a SAML Response with a status that indicates a failure. This commit adds an error message in the REST API response along with the SAML Response XML string so that the caller of the API can identify that this is an unsuccessful response without needing to parse the XML.
This commit is contained in:
parent
d75571ff0f
commit
1cff6897f3
@ -98,7 +98,7 @@ public class WildcardServiceProviderRestIT extends IdpRestTestCase {
|
||||
|
||||
final Map<String, Object> map = entityAsMap(response);
|
||||
assertThat(map, notNullValue());
|
||||
assertThat(map.keySet(), containsInAnyOrder("post_url", "saml_response", "service_provider"));
|
||||
assertThat(map.keySet(), containsInAnyOrder("post_url", "saml_response", "saml_status", "service_provider", "error"));
|
||||
assertThat(map.get("post_url"), equalTo(acs));
|
||||
assertThat(map.get("saml_response"), instanceOf(String.class));
|
||||
|
||||
|
@ -8,7 +8,6 @@ package org.elasticsearch.xpack.idp.action;
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.ValidationException;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.xpack.idp.saml.support.SamlAuthenticationState;
|
||||
@ -42,15 +41,6 @@ public class SamlInitiateSingleSignOnRequest extends ActionRequest {
|
||||
if (Strings.isNullOrEmpty(assertionConsumerService)) {
|
||||
validationException = addValidationError("acs is missing", validationException);
|
||||
}
|
||||
if (samlAuthenticationState != null) {
|
||||
final ValidationException authnStateException = samlAuthenticationState.validate();
|
||||
if (authnStateException != null && authnStateException.validationErrors().isEmpty() == false) {
|
||||
if (validationException == null) {
|
||||
validationException = new ActionRequestValidationException();
|
||||
}
|
||||
validationException.addValidationErrors(authnStateException.validationErrors());
|
||||
}
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
package org.elasticsearch.xpack.idp.action;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
@ -16,18 +17,25 @@ public class SamlInitiateSingleSignOnResponse extends ActionResponse {
|
||||
private String postUrl;
|
||||
private String samlResponse;
|
||||
private String entityId;
|
||||
private String samlStatus;
|
||||
private String error;
|
||||
|
||||
public SamlInitiateSingleSignOnResponse(StreamInput in) throws IOException {
|
||||
super(in);
|
||||
this.entityId = in.readString();
|
||||
this.postUrl = in.readString();
|
||||
this.samlResponse = in.readString();
|
||||
this.entityId = in.readString();
|
||||
this.samlStatus = in.readString();
|
||||
this.error = in.readOptionalString();
|
||||
}
|
||||
|
||||
public SamlInitiateSingleSignOnResponse(String postUrl, String samlResponse, String entityId) {
|
||||
public SamlInitiateSingleSignOnResponse(String entityId, String postUrl, String samlResponse, String samlStatus,
|
||||
@Nullable String error) {
|
||||
this.entityId = entityId;
|
||||
this.postUrl = postUrl;
|
||||
this.samlResponse = samlResponse;
|
||||
this.entityId = entityId;
|
||||
this.samlStatus = samlStatus;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getPostUrl() {
|
||||
@ -42,10 +50,20 @@ public class SamlInitiateSingleSignOnResponse extends ActionResponse {
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public String getSamlStatus() {
|
||||
return samlStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
out.writeString(entityId);
|
||||
out.writeString(postUrl);
|
||||
out.writeString(samlResponse);
|
||||
out.writeString(entityId);
|
||||
out.writeString(samlStatus);
|
||||
out.writeOptionalString(error);
|
||||
}
|
||||
}
|
||||
|
@ -62,50 +62,53 @@ public class TransportSamlInitiateSingleSignOnAction
|
||||
request.getAssertionConsumerService(),
|
||||
false,
|
||||
ActionListener.wrap(
|
||||
sp -> {
|
||||
if (null == sp) {
|
||||
final String message = "Service Provider with Entity ID [" + request.getSpEntityId() + "] and ACS ["
|
||||
+ request.getAssertionConsumerService() + "] is not known to this Identity Provider";
|
||||
logger.debug(message);
|
||||
possiblyReplyWithSamlFailure(authenticationState, StatusCode.RESPONDER, new IllegalArgumentException(message),
|
||||
listener);
|
||||
return;
|
||||
}
|
||||
final SecondaryAuthentication secondaryAuthentication = SecondaryAuthentication.readFromContext(securityContext);
|
||||
if (secondaryAuthentication == null) {
|
||||
possiblyReplyWithSamlFailure(authenticationState,
|
||||
StatusCode.REQUESTER,
|
||||
new ElasticsearchSecurityException("Request is missing secondary authentication", RestStatus.FORBIDDEN),
|
||||
listener);
|
||||
return;
|
||||
}
|
||||
buildUserFromAuthentication(secondaryAuthentication, sp, ActionListener.wrap(
|
||||
user -> {
|
||||
if (user == null) {
|
||||
possiblyReplyWithSamlFailure(authenticationState,
|
||||
StatusCode.REQUESTER,
|
||||
new ElasticsearchSecurityException("User [{}] is not permitted to access service [{}]",
|
||||
RestStatus.FORBIDDEN, secondaryAuthentication.getUser(), sp),
|
||||
listener);
|
||||
return;
|
||||
}
|
||||
final SuccessfulAuthenticationResponseMessageBuilder builder =
|
||||
new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, Clock.systemUTC(), identityProvider);
|
||||
try {
|
||||
final Response response = builder.build(user, authenticationState);
|
||||
listener.onResponse(new SamlInitiateSingleSignOnResponse(
|
||||
user.getServiceProvider().getAssertionConsumerService().toString(),
|
||||
samlFactory.getXmlContent(response),
|
||||
user.getServiceProvider().getEntityId()));
|
||||
} catch (ElasticsearchException e) {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
},
|
||||
e -> possiblyReplyWithSamlFailure(authenticationState, StatusCode.RESPONDER, e, listener)
|
||||
));
|
||||
},
|
||||
e -> possiblyReplyWithSamlFailure(authenticationState, StatusCode.RESPONDER, e, listener)
|
||||
));
|
||||
sp -> {
|
||||
if (null == sp) {
|
||||
final String message = "Service Provider with Entity ID [" + request.getSpEntityId() + "] and ACS ["
|
||||
+ request.getAssertionConsumerService() + "] is not known to this Identity Provider";
|
||||
possiblyReplyWithSamlFailure(authenticationState, request.getSpEntityId(), request.getAssertionConsumerService(),
|
||||
StatusCode.RESPONDER, new IllegalArgumentException(message), listener);
|
||||
return;
|
||||
}
|
||||
final SecondaryAuthentication secondaryAuthentication = SecondaryAuthentication.readFromContext(securityContext);
|
||||
if (secondaryAuthentication == null) {
|
||||
possiblyReplyWithSamlFailure(authenticationState, request.getSpEntityId(), request.getAssertionConsumerService(),
|
||||
StatusCode.REQUESTER,
|
||||
new ElasticsearchSecurityException("Request is missing secondary authentication", RestStatus.FORBIDDEN),
|
||||
listener);
|
||||
return;
|
||||
}
|
||||
buildUserFromAuthentication(secondaryAuthentication, sp, ActionListener.wrap(
|
||||
user -> {
|
||||
if (user == null) {
|
||||
possiblyReplyWithSamlFailure(authenticationState, request.getSpEntityId(),
|
||||
request.getAssertionConsumerService(), StatusCode.REQUESTER,
|
||||
new ElasticsearchSecurityException("User [{}] is not permitted to access service [{}]",
|
||||
RestStatus.FORBIDDEN, secondaryAuthentication.getUser().principal(), sp.getEntityId()),
|
||||
listener);
|
||||
return;
|
||||
}
|
||||
final SuccessfulAuthenticationResponseMessageBuilder builder =
|
||||
new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, Clock.systemUTC(), identityProvider);
|
||||
try {
|
||||
final Response response = builder.build(user, authenticationState);
|
||||
listener.onResponse(new SamlInitiateSingleSignOnResponse(
|
||||
user.getServiceProvider().getEntityId(),
|
||||
user.getServiceProvider().getAssertionConsumerService().toString(),
|
||||
samlFactory.getXmlContent(response),
|
||||
StatusCode.SUCCESS,
|
||||
null));
|
||||
} catch (ElasticsearchException e) {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
},
|
||||
e -> possiblyReplyWithSamlFailure(authenticationState, request.getSpEntityId(),
|
||||
request.getAssertionConsumerService(), StatusCode.RESPONDER, e, listener)
|
||||
));
|
||||
},
|
||||
e -> possiblyReplyWithSamlFailure(authenticationState, request.getSpEntityId(), request.getAssertionConsumerService(),
|
||||
StatusCode.RESPONDER, e, listener)
|
||||
));
|
||||
}
|
||||
|
||||
private void buildUserFromAuthentication(SecondaryAuthentication secondaryAuthentication, SamlServiceProvider serviceProvider,
|
||||
@ -129,20 +132,23 @@ public class TransportSamlInitiateSingleSignOnAction
|
||||
);
|
||||
}
|
||||
|
||||
private void possiblyReplyWithSamlFailure(SamlAuthenticationState authenticationState, String statusCode, Exception e,
|
||||
private void possiblyReplyWithSamlFailure(SamlAuthenticationState authenticationState, String spEntityId,
|
||||
String acsUrl, String statusCode, Exception e,
|
||||
ActionListener<SamlInitiateSingleSignOnResponse> listener) {
|
||||
logger.debug("Failed to generate a successful SAML response: ", e);
|
||||
if (authenticationState != null) {
|
||||
final FailedAuthenticationResponseMessageBuilder builder =
|
||||
new FailedAuthenticationResponseMessageBuilder(samlFactory, Clock.systemUTC(), identityProvider)
|
||||
.setInResponseTo(authenticationState.getAuthnRequestId())
|
||||
.setAcsUrl(authenticationState.getRequestedAcsUrl())
|
||||
.setAcsUrl(acsUrl)
|
||||
.setPrimaryStatusCode(statusCode);
|
||||
final Response response = builder.build();
|
||||
//TODO: Log and indicate SAML Response status is failure in the response
|
||||
listener.onResponse(new SamlInitiateSingleSignOnResponse(
|
||||
authenticationState.getRequestedAcsUrl(),
|
||||
spEntityId,
|
||||
acsUrl,
|
||||
samlFactory.getXmlContent(response),
|
||||
authenticationState.getEntityId()));
|
||||
statusCode,
|
||||
e.getMessage()));
|
||||
} else {
|
||||
listener.onFailure(e);
|
||||
}
|
||||
|
@ -181,7 +181,6 @@ public class SamlAuthnRequestValidator {
|
||||
checkDestination(authnRequest);
|
||||
final String acs = checkAcs(authnRequest, sp, authnState);
|
||||
validateNameIdPolicy(authnRequest, sp, authnState);
|
||||
authnState.put(SamlAuthenticationState.Fields.ENTITY_ID.getPreferredName(), sp.getEntityId());
|
||||
authnState.put(SamlAuthenticationState.Fields.AUTHN_REQUEST_ID.getPreferredName(), authnRequest.getID());
|
||||
final SamlValidateAuthnRequestResponse response = new SamlValidateAuthnRequestResponse(sp.getEntityId(), acs,
|
||||
authnRequest.isForceAuthn(), authnState);
|
||||
@ -268,7 +267,6 @@ public class SamlAuthnRequestValidator {
|
||||
throw new ElasticsearchSecurityException("The registered ACS URL for this Service Provider is [{}] but the authentication " +
|
||||
"request contained [{}]", RestStatus.BAD_REQUEST, sp.getAssertionConsumerService(), acs);
|
||||
}
|
||||
authnState.put(SamlAuthenticationState.Fields.ACS_URL.getPreferredName(), acs);
|
||||
return acs;
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,8 @@ public class RestSamlInitiateSingleSignOnAction extends IdpBaseRestHandler {
|
||||
builder.startObject();
|
||||
builder.field("post_url", response.getPostUrl());
|
||||
builder.field("saml_response", response.getSamlResponse());
|
||||
builder.field("saml_status", response.getSamlStatus());
|
||||
builder.field("error", response.getError());
|
||||
builder.startObject("service_provider");
|
||||
builder.field("entity_id", response.getEntityId());
|
||||
builder.endObject();
|
||||
|
@ -8,7 +8,6 @@ package org.elasticsearch.xpack.idp.saml.support;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.ValidationException;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.io.stream.Writeable;
|
||||
@ -27,8 +26,6 @@ import java.util.Objects;
|
||||
* these.
|
||||
*/
|
||||
public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
private String entityId;
|
||||
private String requestedAcsUrl;
|
||||
@Nullable
|
||||
private String requestedNameidFormat;
|
||||
@Nullable
|
||||
@ -39,8 +36,6 @@ public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
}
|
||||
|
||||
public SamlAuthenticationState(StreamInput in) throws IOException {
|
||||
entityId = in.readString();
|
||||
requestedAcsUrl = in.readString();
|
||||
requestedNameidFormat = in.readOptionalString();
|
||||
authnRequestId = in.readOptionalString();
|
||||
}
|
||||
@ -61,39 +56,10 @@ public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
this.authnRequestId = authnRequestId;
|
||||
}
|
||||
|
||||
public String getEntityId() {
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public void setEntityId(String entityId) {
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
public String getRequestedAcsUrl() {
|
||||
return requestedAcsUrl;
|
||||
}
|
||||
|
||||
public void setRequestedAcsUrl(String requestedAcsUrl) {
|
||||
this.requestedAcsUrl = requestedAcsUrl;
|
||||
}
|
||||
|
||||
public ValidationException validate() {
|
||||
final ValidationException validation = new ValidationException();
|
||||
if (Strings.isNullOrEmpty(entityId)) {
|
||||
validation.addValidationError("field [" + Fields.ENTITY_ID + "] is required, but was [" + entityId + "]");
|
||||
}
|
||||
if (Strings.isNullOrEmpty(requestedAcsUrl)) {
|
||||
validation.addValidationError("field [" + Fields.ACS_URL + "] is required, but was [" + requestedAcsUrl + "]");
|
||||
}
|
||||
return validation;
|
||||
}
|
||||
|
||||
public static final ObjectParser<SamlAuthenticationState, SamlAuthenticationState> PARSER
|
||||
= new ObjectParser<>("saml_authn_state", true, SamlAuthenticationState::new);
|
||||
|
||||
static {
|
||||
PARSER.declareString(SamlAuthenticationState::setEntityId, Fields.ENTITY_ID);
|
||||
PARSER.declareString(SamlAuthenticationState::setRequestedAcsUrl, Fields.ACS_URL);
|
||||
PARSER.declareStringOrNull(SamlAuthenticationState::setRequestedNameidFormat, Fields.NAMEID_FORMAT);
|
||||
PARSER.declareStringOrNull(SamlAuthenticationState::setAuthnRequestId, Fields.AUTHN_REQUEST_ID);
|
||||
}
|
||||
@ -103,8 +69,6 @@ public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
builder.startObject();
|
||||
builder.field(Fields.NAMEID_FORMAT.getPreferredName(), requestedNameidFormat);
|
||||
builder.field(Fields.AUTHN_REQUEST_ID.getPreferredName(), authnRequestId);
|
||||
builder.field(Fields.ENTITY_ID.getPreferredName(), entityId);
|
||||
builder.field(Fields.ACS_URL.getPreferredName(), requestedAcsUrl);
|
||||
return builder.endObject();
|
||||
}
|
||||
|
||||
@ -116,14 +80,10 @@ public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
public interface Fields {
|
||||
ParseField NAMEID_FORMAT = new ParseField("nameid_format");
|
||||
ParseField AUTHN_REQUEST_ID = new ParseField("authn_request_id");
|
||||
ParseField ENTITY_ID = new ParseField("entity_id");
|
||||
ParseField ACS_URL = new ParseField("acs_url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
out.writeString(entityId);
|
||||
out.writeString(requestedAcsUrl);
|
||||
out.writeOptionalString(requestedNameidFormat);
|
||||
out.writeOptionalString(authnRequestId);
|
||||
}
|
||||
@ -138,14 +98,12 @@ public class SamlAuthenticationState implements Writeable, ToXContentObject {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SamlAuthenticationState that = (SamlAuthenticationState) o;
|
||||
return entityId.equals(that.entityId) &&
|
||||
requestedAcsUrl.equals(that.requestedAcsUrl) &&
|
||||
Objects.equals(requestedNameidFormat, that.requestedNameidFormat) &&
|
||||
return Objects.equals(requestedNameidFormat, that.requestedNameidFormat) &&
|
||||
Objects.equals(authnRequestId, that.authnRequestId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(entityId, requestedAcsUrl, requestedNameidFormat, authnRequestId);
|
||||
return Objects.hash(requestedNameidFormat, authnRequestId);
|
||||
}
|
||||
}
|
||||
|
@ -150,15 +150,11 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
|
||||
assertThat(serviceProvider, hasKey("entity_id"));
|
||||
assertThat(serviceProvider.get("entity_id"), equalTo(entityId));
|
||||
assertThat(serviceProvider, hasKey("acs"));
|
||||
assertThat(serviceProvider.get("acs"), equalTo(authnRequest.getAssertionConsumerServiceURL()));
|
||||
assertThat(serviceProvider.get("acs"), equalTo(acsUrl));
|
||||
assertThat(validateResponseObject.evaluate("force_authn"), equalTo(forceAuthn));
|
||||
Map<String, String> authnState = validateResponseObject.evaluate("authn_state");
|
||||
assertThat(authnState, hasKey("nameid_format"));
|
||||
assertThat(authnState.get("nameid_format"), equalTo(nameIdFormat));
|
||||
assertThat(authnState, hasKey("entity_id"));
|
||||
assertThat(authnState.get("entity_id"), equalTo(entityId));
|
||||
assertThat(authnState, hasKey("acs_url"));
|
||||
assertThat(authnState.get("acs_url"), equalTo(acsUrl));
|
||||
assertThat(authnState, hasKey("authn_request_id"));
|
||||
final String expectedInResponeTo = authnState.get("authn_request_id");
|
||||
|
||||
@ -194,6 +190,62 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
|
||||
assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME);
|
||||
}
|
||||
|
||||
public void testSpInitiatedSsoFailsForUserWithNoAccess() throws Exception {
|
||||
String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
|
||||
String entityId = SP_ENTITY_ID;
|
||||
registerServiceProvider(entityId, acsUrl);
|
||||
ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
|
||||
// Validate incoming authentication request
|
||||
Request validateRequest = new Request("POST", "/_idp/saml/validate");
|
||||
validateRequest.setOptions(REQUEST_OPTIONS_AS_CONSOLE_USER);
|
||||
final String nameIdFormat = TRANSIENT;
|
||||
final String relayString = randomBoolean() ? randomAlphaOfLength(8) : null;
|
||||
final boolean forceAuthn = true;
|
||||
final AuthnRequest authnRequest = buildAuthnRequest(entityId, new URL(acsUrl),
|
||||
new URL("https://idp.org/sso/redirect"), nameIdFormat, forceAuthn);
|
||||
final String query = getQueryString(authnRequest, relayString, false, null);
|
||||
validateRequest.setJsonEntity("{\"authn_request_query\":\"" + query + "\"}");
|
||||
Response validateResponse = getRestClient().performRequest(validateRequest);
|
||||
ObjectPath validateResponseObject = ObjectPath.createFromResponse(validateResponse);
|
||||
Map<String, String> serviceProvider = validateResponseObject.evaluate("service_provider");
|
||||
assertThat(serviceProvider, hasKey("entity_id"));
|
||||
assertThat(serviceProvider.get("entity_id"), equalTo(entityId));
|
||||
assertThat(serviceProvider, hasKey("acs"));
|
||||
assertThat(serviceProvider.get("acs"), equalTo(acsUrl));
|
||||
assertThat(validateResponseObject.evaluate("force_authn"), equalTo(forceAuthn));
|
||||
Map<String, String> authnState = validateResponseObject.evaluate("authn_state");
|
||||
assertThat(authnState, hasKey("nameid_format"));
|
||||
assertThat(authnState.get("nameid_format"), equalTo(nameIdFormat));
|
||||
assertThat(authnState, hasKey("authn_request_id"));
|
||||
final String expectedInResponeTo = authnState.get("authn_request_id");
|
||||
|
||||
// User login a.k.a exchange the user credentials for an API Key - user can authenticate but shouldn't have access this SP
|
||||
final String apiKeyCredentials = getApiKeyFromCredentials(SAMPLE_USER_NAME,
|
||||
new SecureString(SAMPLE_USER_PASSWORD.toCharArray()));
|
||||
// Make a request to init an SSO flow with the API Key as secondary authentication
|
||||
Request initRequest = new Request("POST", "/_idp/saml/init");
|
||||
initRequest.setOptions(RequestOptions.DEFAULT.toBuilder()
|
||||
.addHeader("Authorization", basicAuthHeaderValue(CONSOLE_USER_NAME,
|
||||
new SecureString(CONSOLE_USER_PASSWORD.toCharArray())))
|
||||
.addHeader("es-secondary-authorization", "ApiKey " + apiKeyCredentials)
|
||||
.build());
|
||||
XContentBuilder authnStateBuilder = jsonBuilder();
|
||||
authnStateBuilder.map(authnState);
|
||||
initRequest.setJsonEntity("{ \"entity_id\":\"" + entityId + "\", \"acs\":\"" + acsUrl + "\"," +
|
||||
"\"authn_state\":" + Strings.toString(authnStateBuilder) + "}");
|
||||
Response initResponse = getRestClient().performRequest(initRequest);
|
||||
ObjectPath initResponseObject = ObjectPath.createFromResponse(initResponse);
|
||||
assertThat(initResponseObject.evaluate("post_url").toString(), equalTo(acsUrl));
|
||||
final String body = initResponseObject.evaluate("saml_response").toString();
|
||||
assertThat(body, containsString("<saml2p:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Requester\"/>"));
|
||||
assertThat(body, containsString("InResponseTo=\"" + expectedInResponeTo + "\""));
|
||||
Map<String, String> sp = initResponseObject.evaluate("service_provider");
|
||||
assertThat(sp, hasKey("entity_id"));
|
||||
assertThat(sp.get("entity_id"), equalTo(entityId));
|
||||
assertThat(initResponseObject.evaluate("error"),
|
||||
equalTo("User [" + SAMPLE_USER_NAME + "] is not permitted to access service [" + entityId + "]"));
|
||||
}
|
||||
|
||||
public void testSpInitiatedSsoFailsForUnknownSp() throws Exception {
|
||||
String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
|
||||
String entityId = SP_ENTITY_ID;
|
||||
|
@ -28,10 +28,12 @@ import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider;
|
||||
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderResolver;
|
||||
import org.elasticsearch.xpack.idp.saml.sp.ServiceProviderDefaults;
|
||||
import org.elasticsearch.xpack.idp.saml.sp.WildcardServiceProviderResolver;
|
||||
import org.elasticsearch.xpack.idp.saml.support.SamlAuthenticationState;
|
||||
import org.elasticsearch.xpack.idp.saml.support.SamlFactory;
|
||||
import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase;
|
||||
import org.joda.time.Duration;
|
||||
import org.mockito.Mockito;
|
||||
import org.opensaml.saml.saml2.core.StatusCode;
|
||||
import org.opensaml.security.x509.X509Credential;
|
||||
|
||||
import java.net.URL;
|
||||
@ -49,12 +51,12 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT;
|
||||
|
||||
public class TransportSamlInitiateSingleSignOnRequestTests extends IdpSamlTestCase {
|
||||
public class TransportSamlInitiateSingleSignOnActionTests extends IdpSamlTestCase {
|
||||
|
||||
public void testGetResponseForRegisteredSp() throws Exception {
|
||||
final SamlInitiateSingleSignOnRequest request = new SamlInitiateSingleSignOnRequest();
|
||||
request.setSpEntityId("https://sp.some.org");
|
||||
|
||||
request.setAssertionConsumerService("https://sp.some.org/api/security/v1/saml");
|
||||
final PlainActionFuture<SamlInitiateSingleSignOnResponse> future = new PlainActionFuture<>();
|
||||
final TransportSamlInitiateSingleSignOnAction action = setupTransportAction(true);
|
||||
action.doExecute(mock(Task.class), request, future);
|
||||
@ -69,10 +71,10 @@ public class TransportSamlInitiateSingleSignOnRequestTests extends IdpSamlTestCa
|
||||
assertThat(response.getSamlResponse(), containsString("https://saml.elasticsearch.org/attributes/roles"));
|
||||
}
|
||||
|
||||
public void testGetResponseWithoutSecondaryAuthentication() throws Exception {
|
||||
public void testGetResponseWithoutSecondaryAuthenticationInIdpInitiated() throws Exception {
|
||||
final SamlInitiateSingleSignOnRequest request = new SamlInitiateSingleSignOnRequest();
|
||||
request.setSpEntityId("https://sp.some.org");
|
||||
|
||||
request.setAssertionConsumerService("https://sp.some.org/api/security/v1/saml");
|
||||
final PlainActionFuture<SamlInitiateSingleSignOnResponse> future = new PlainActionFuture<>();
|
||||
final TransportSamlInitiateSingleSignOnAction action = setupTransportAction(false);
|
||||
action.doExecute(mock(Task.class), request, future);
|
||||
@ -81,10 +83,10 @@ public class TransportSamlInitiateSingleSignOnRequestTests extends IdpSamlTestCa
|
||||
assertThat(e.getCause().getMessage(), containsString("Request is missing secondary authentication"));
|
||||
}
|
||||
|
||||
public void testGetResponseForNotRegisteredSp() throws Exception {
|
||||
public void testGetResponseForNotRegisteredSpInIdpInitiated() throws Exception {
|
||||
final SamlInitiateSingleSignOnRequest request = new SamlInitiateSingleSignOnRequest();
|
||||
request.setSpEntityId("https://sp2.other.org");
|
||||
|
||||
request.setAssertionConsumerService("https://sp2.some.org/api/security/v1/saml");
|
||||
final PlainActionFuture<SamlInitiateSingleSignOnResponse> future = new PlainActionFuture<>();
|
||||
final TransportSamlInitiateSingleSignOnAction action = setupTransportAction(true);
|
||||
action.doExecute(mock(Task.class), request, future);
|
||||
@ -94,6 +96,27 @@ public class TransportSamlInitiateSingleSignOnRequestTests extends IdpSamlTestCa
|
||||
assertThat(e.getCause().getMessage(), containsString("is not known to this Identity Provider"));
|
||||
}
|
||||
|
||||
public void testGetResponseWithoutSecondaryAuthenticationInSpInitiatedFlow() throws Exception {
|
||||
final SamlInitiateSingleSignOnRequest request = new SamlInitiateSingleSignOnRequest();
|
||||
request.setSpEntityId("https://sp.some.org");
|
||||
request.setAssertionConsumerService("https://sp.some.org/saml/acs");
|
||||
final String requestId = randomAlphaOfLength(12);
|
||||
final SamlAuthenticationState samlAuthenticationState = new SamlAuthenticationState();
|
||||
samlAuthenticationState.setAuthnRequestId(requestId);
|
||||
request.setSamlAuthenticationState(samlAuthenticationState);
|
||||
|
||||
final PlainActionFuture<SamlInitiateSingleSignOnResponse> future = new PlainActionFuture<>();
|
||||
final TransportSamlInitiateSingleSignOnAction action = setupTransportAction(false);
|
||||
action.doExecute(mock(Task.class), request, future);
|
||||
|
||||
final SamlInitiateSingleSignOnResponse response = future.get();
|
||||
assertThat(response.getError(), equalTo("Request is missing secondary authentication"));
|
||||
assertThat(response.getSamlStatus(), equalTo(StatusCode.REQUESTER));
|
||||
assertThat(response.getPostUrl(), equalTo("https://sp.some.org/saml/acs"));
|
||||
assertThat(response.getEntityId(), equalTo("https://sp.some.org"));
|
||||
assertThat(response.getSamlResponse(), containsString("InResponseTo=\"" + requestId + "\""));
|
||||
}
|
||||
|
||||
private TransportSamlInitiateSingleSignOnAction setupTransportAction(boolean withSecondaryAuth) throws Exception {
|
||||
final Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
@ -83,7 +83,7 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase {
|
||||
SamlValidateAuthnRequestResponse response = future.actionGet();
|
||||
assertThat(response.isForceAuthn(), equalTo(false));
|
||||
assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org"));
|
||||
assertThat(response.getAuthnState().size(), equalTo(4));
|
||||
assertThat(response.getAuthnState().size(), equalTo(2));
|
||||
assertThat(response.getAuthnState().get("authn_request_id"), equalTo(authnRequest.getID()));
|
||||
assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT));
|
||||
}
|
||||
@ -98,7 +98,7 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase {
|
||||
SamlValidateAuthnRequestResponse response = future.actionGet();
|
||||
assertThat(response.isForceAuthn(), equalTo(false));
|
||||
assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org"));
|
||||
assertThat(response.getAuthnState().size(), equalTo(4));
|
||||
assertThat(response.getAuthnState().size(), equalTo(2));
|
||||
assertThat(response.getAuthnState().get("authn_request_id"), equalTo(authnRequest.getID()));
|
||||
assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT));
|
||||
}
|
||||
@ -112,7 +112,7 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase {
|
||||
SamlValidateAuthnRequestResponse response = future.actionGet();
|
||||
assertThat(response.isForceAuthn(), equalTo(false));
|
||||
assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org"));
|
||||
assertThat(response.getAuthnState().size(), equalTo(4));
|
||||
assertThat(response.getAuthnState().size(), equalTo(2));
|
||||
assertThat(response.getAuthnState().get("authn_request_id"), equalTo(authnRequest.getID()));
|
||||
assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT));
|
||||
}
|
||||
@ -126,7 +126,7 @@ public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase {
|
||||
SamlValidateAuthnRequestResponse response = future.actionGet();
|
||||
assertThat(response.isForceAuthn(), equalTo(false));
|
||||
assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org"));
|
||||
assertThat(response.getAuthnState().size(), equalTo(4));
|
||||
assertThat(response.getAuthnState().size(), equalTo(2));
|
||||
assertThat(response.getAuthnState().get("authn_request_id"), equalTo(authnRequest.getID()));
|
||||
assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT));
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
package org.elasticsearch.xpack.idp.saml.support;
|
||||
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.common.ValidationException;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
|
||||
import org.elasticsearch.common.xcontent.DeprecationHandler;
|
||||
@ -17,30 +16,15 @@ import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.test.VersionUtils;
|
||||
import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.opensaml.saml.saml2.core.NameID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.hamcrest.Matchers.emptyIterable;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
public class SamlAuthenticationStateTests extends IdpSamlTestCase {
|
||||
|
||||
public void testValidation() {
|
||||
final SamlAuthenticationState state = new SamlAuthenticationState();
|
||||
final ValidationException validationException = state.validate();
|
||||
assertThat(validationException, notNullValue());
|
||||
assertThat(validationException.validationErrors(), not(emptyIterable()));
|
||||
assertThat(validationException.validationErrors(), Matchers.containsInAnyOrder(
|
||||
"field [entity_id] is required, but was [null]",
|
||||
"field [acs_url] is required, but was [null]"
|
||||
));
|
||||
}
|
||||
|
||||
public void testXContentRoundTrip() throws Exception {
|
||||
final SamlAuthenticationState state1 = generateAuthnState();
|
||||
final SamlAuthenticationState state2 = assertXContentRoundTrip(state1);
|
||||
@ -67,8 +51,6 @@ public class SamlAuthenticationStateTests extends IdpSamlTestCase {
|
||||
|
||||
private SamlAuthenticationState generateAuthnState() {
|
||||
SamlAuthenticationState authnState = new SamlAuthenticationState();
|
||||
authnState.setRequestedAcsUrl("https://" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8) + "/saml/acs");
|
||||
authnState.setEntityId("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8));
|
||||
authnState.setAuthnRequestId(randomAlphaOfLength(12));
|
||||
authnState.setRequestedNameidFormat(randomFrom(NameID.TRANSIENT, NameID.PERSISTENT, NameID.EMAIL));
|
||||
return authnState;
|
||||
@ -76,8 +58,6 @@ public class SamlAuthenticationStateTests extends IdpSamlTestCase {
|
||||
|
||||
private SamlAuthenticationState generateMinimalAuthnState() {
|
||||
SamlAuthenticationState authnState = new SamlAuthenticationState();
|
||||
authnState.setRequestedAcsUrl("https://" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8) + "/saml/acs");
|
||||
authnState.setEntityId("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8));
|
||||
authnState.setAuthnRequestId(null);
|
||||
authnState.setRequestedNameidFormat(null);
|
||||
return authnState;
|
||||
|
Loading…
x
Reference in New Issue
Block a user