Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle incoming AuthnRequests #52018

Merged
merged 12 commits into from
Feb 26, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@
import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.action.TransportSamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.rest.action.RestSamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction;
import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction;
import org.elasticsearch.xpack.idp.rest.RestSamlValidateAuthenticationRequestAction;
import org.elasticsearch.xpack.idp.saml.idp.CloudIdp;
import org.elasticsearch.xpack.idp.saml.support.SamlUtils;
import org.elasticsearch.xpack.idp.saml.support.SamlInit;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;

import java.util.Collection;
Expand All @@ -56,30 +59,30 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin {
public static final Setting<String> IDP_ENTITY_ID = Setting.simpleString("xpack.idp.entity_id", Setting.Property.NodeScope);
public static final Setting<String> IDP_SSO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.redirect", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URI", e);
new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URL", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SSO_POST_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.post", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URI", e);
new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URL", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SLO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.redirect", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URI", e);
new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URL", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SLO_POST_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.post", value -> {
try {
new URI(value);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URI", e);
new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URL", e);
}
}, Setting.Property.NodeScope);
public static final Setting<String> IDP_SIGNING_KEY_ALIAS = Setting.simpleString("xpack.idp.signing.keystore.alias",
Expand All @@ -101,7 +104,7 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
return List.of();
}

SamlUtils.initialize();
SamlInit.initialize();
CloudIdp idp = new CloudIdp(environment, settings);
return List.of();
}
Expand All @@ -111,8 +114,9 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
if (enabled == false) {
return Collections.emptyList();
}
return Collections.singletonList(
new ActionHandler<>(SamlInitiateSingleSignOnAction.INSTANCE, TransportSamlInitiateSingleSignOnAction.class)
return List.of(
new ActionHandler<>(SamlInitiateSingleSignOnAction.INSTANCE, TransportSamlInitiateSingleSignOnAction.class),
new ActionHandler<>(SamlValidateAuthnRequestAction.INSTANCE, TransportSamlValidateAuthnRequestAction.class)
);
}

Expand All @@ -124,7 +128,9 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
if (enabled == false) {
return Collections.emptyList();
}
return Collections.singletonList(new RestSamlInitiateSingleSignOnAction());
return List.of(
new RestSamlInitiateSingleSignOnAction(),
new RestSamlValidateAuthenticationRequestAction());
}

@Override
Expand All @@ -139,4 +145,12 @@ public List<Setting<?>> getSettings() {
settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings());
return Collections.unmodifiableList(settings);
}

private static URL parseURL(String key, String value) {
try {
return new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid value [" + value + "] for [" + key + "]. Not a valid URL", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.action;

import org.elasticsearch.action.ActionType;

public class SamlValidateAuthnRequestAction extends ActionType<SamlValidateAuthnRequestResponse> {

public static final String NAME = "cluster:admin/idp/saml/validate";
public static final SamlValidateAuthnRequestAction INSTANCE = new SamlValidateAuthnRequestAction();

private SamlValidateAuthnRequestAction() {
super(NAME, SamlValidateAuthnRequestResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.action;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;

import static org.elasticsearch.action.ValidateActions.addValidationError;

public class SamlValidateAuthnRequestRequest extends ActionRequest {

private String queryString;

public SamlValidateAuthnRequestRequest(StreamInput in) throws IOException {
super(in);
queryString = in.readString();
}

public SamlValidateAuthnRequestRequest() {
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(queryString)) {
validationException = addValidationError("Authentication request query string must be provided", validationException);
}
return validationException;
}

public String getQueryString() {
return queryString;
}

public void setQueryString(String queryString) {
this.queryString = queryString;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(queryString);
}

@Override
public String toString() {
return getClass().getSimpleName() + "{queryString='" + queryString + "'}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.action;

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;

public class SamlValidateAuthnRequestResponse extends ActionResponse {

private final String spEntityId;
private final boolean forceAuthn;
private final Map<String, Object> authnState;

public SamlValidateAuthnRequestResponse(StreamInput in) throws IOException {
super(in);
this.spEntityId = in.readString();
this.forceAuthn = in.readBoolean();
this.authnState = in.readMap();
}

public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map<String, Object> authnState) {
this.spEntityId = Objects.requireNonNull(spEntityId);
this.forceAuthn = forceAuthn;
this.authnState = Map.copyOf(Objects.requireNonNull(authnState));
}

public String getSpEntityId() {
return spEntityId;
}

public boolean isForceAuthn() {
return forceAuthn;
}

public Map<String, Object> getAuthnState() {
return authnState;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(spEntityId);
out.writeBoolean(forceAuthn);
out.writeMap(authnState);

}

@Override
public String toString() {
return getClass().getSimpleName() + "{ spEntityId='" + getSpEntityId() + "',\n" +
" forceAuthn='" + isForceAuthn() + "',\n" +
" additionalData='" + getAuthnState() + "' }";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider;
import org.elasticsearch.xpack.idp.saml.support.SamlFactory;
import org.elasticsearch.xpack.idp.saml.support.SamlUtils;
import org.opensaml.saml.saml2.core.Response;

import java.io.IOException;
Expand Down Expand Up @@ -72,7 +71,7 @@ protected void doExecute(Task task, SamlInitiateSingleSignOnRequest request,
Clock.systemUTC(), idp);
final Response response = builder.build(user, null);
listener.onResponse(new SamlInitiateSingleSignOnResponse(user.getServiceProvider().getAssertionConsumerService().toString(),
SamlUtils.getXmlContent(response),
samlFactory.getXmlContent(response),
user.getServiceProvider().getEntityId()));
} catch (IOException e) {
listener.onFailure(new IllegalArgumentException(e.getMessage()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.action;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.env.Environment;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.idp.saml.authn.SamlAuthnRequestValidator;
import org.elasticsearch.xpack.idp.saml.idp.CloudIdp;
import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
import org.elasticsearch.xpack.idp.saml.support.SamlFactory;

public class TransportSamlValidateAuthnRequestAction extends HandledTransportAction<SamlValidateAuthnRequestRequest,
SamlValidateAuthnRequestResponse> {

private final Environment env;

@Inject
public TransportSamlValidateAuthnRequestAction(TransportService transportService, ActionFilters actionFilters,
Environment environment) {
super(SamlValidateAuthnRequestAction.NAME, transportService, actionFilters, SamlValidateAuthnRequestRequest::new);
this.env = environment;
}

@Override
protected void doExecute(Task task, SamlValidateAuthnRequestRequest request,
ActionListener<SamlValidateAuthnRequestResponse> listener) {
final SamlIdentityProvider idp = new CloudIdp(env, env.settings());
final SamlFactory samlFactory = new SamlFactory();
final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(samlFactory, idp);
try {
validator.processQueryString(request.getQueryString(), listener);
} catch (Exception e) {
listener.onFailure(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.rest;

import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction;
import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestRequest;
import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

import static org.elasticsearch.rest.RestRequest.Method.POST;

public class RestSamlValidateAuthenticationRequestAction extends BaseRestHandler {

static final ObjectParser<SamlValidateAuthnRequestRequest, Void> PARSER =
new ObjectParser<>("idp_validate_authn_request", SamlValidateAuthnRequestRequest::new);

static {
PARSER.declareString(SamlValidateAuthnRequestRequest::setQueryString, new ParseField("authn_request_query"));
}

@Override
public String getName() {
return "saml_idp_validate_authn_request_action";
}

@Override
public List<Route> routes() {
return Collections.singletonList(new Route(POST, "/_idp/saml/validate"));
}

@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
try (XContentParser parser = request.contentParser()) {
final SamlValidateAuthnRequestRequest validateRequest = PARSER.parse(parser, null);
return channel -> client.execute(SamlValidateAuthnRequestAction.INSTANCE, validateRequest,
new RestBuilderListener<SamlValidateAuthnRequestResponse>(channel) {
@Override
public RestResponse buildResponse(SamlValidateAuthnRequestResponse response, XContentBuilder builder) throws Exception {
builder.startObject();
builder.startObject("service_provider");
builder.field("entity_id", response.getSpEntityId());
builder.endObject();
builder.field("force_authn", response.isForceAuthn());
builder.field("authn_state", response.getAuthnState());
builder.endObject();
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
}
}
Loading