Skip to content

Commit

Permalink
Adding API for generating SAML SP metadata (#64517) (#64731)
Browse files Browse the repository at this point in the history
* Adding API for generating SAML SP metadata
Resolve #49018

* Adding API for generating SAML SP metadata
Resolves #49018

* Adding API for generating SAML SP metadata
Resolves #49018

* Adding API for generating SAML SP metadata
Resolves #49018

* Adding API for generating SAML SP metadata
Resolves #49018

* Adding API for generating SAML SP metadata
Resolves #49018

* Adding API for generating SAML SP metadata
Resolves #49018

Co-authored-by: Elastic Machine <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
BigPandaToo and elasticmachine authored Nov 6, 2020
1 parent be1a568 commit c6bfc96
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 10 deletions.
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.core.security.action.saml;

import org.elasticsearch.action.ActionType;

public class SamlSpMetadataAction extends ActionType<SamlSpMetadataResponse> {
public static final String NAME = "cluster:monitor/xpack/security/saml/metadata";
public static final SamlSpMetadataAction INSTANCE = new SamlSpMetadataAction();

private SamlSpMetadataAction() {
super(NAME, SamlSpMetadataResponse::new);
}
}
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.core.security.action.saml;

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 SamlSpMetadataRequest extends ActionRequest {

private String realmName;

public SamlSpMetadataRequest(StreamInput in) throws IOException {
super(in);
realmName = in.readOptionalString();
}

public SamlSpMetadataRequest(String realmName) {
this.realmName = realmName;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.hasText(realmName) == false) {
validationException = addValidationError("Realm name may not be empty", validationException);
}
return validationException;
}

public String getRealmName() {
return realmName;
}

public void setRealmName(String realmName) {
this.realmName = realmName;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"realmName=" + realmName +
'}';
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeOptionalString(realmName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.core.security.action.saml;

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

import java.io.IOException;

/**
* Response containing a SAML SP metadata for a specific realm as XML.
*/
public class SamlSpMetadataResponse extends ActionResponse {
public String getXMLString() {
return XMLString;
}

private String XMLString;

public SamlSpMetadataResponse(StreamInput in) throws IOException {
super(in);
XMLString = in.readString();
}

public SamlSpMetadataResponse(String XMLString) {
this.XMLString = XMLString;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(XMLString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.elasticsearch.xpack.core.ilm.action.StopILMAction;
import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction;
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
Expand All @@ -46,7 +47,7 @@ public class ClusterPrivilegeResolver {
private static final Set<String> ALL_SECURITY_PATTERN = Collections.singleton("cluster:admin/xpack/security/*");
private static final Set<String> MANAGE_SAML_PATTERN = Collections.unmodifiableSet(
Sets.newHashSet("cluster:admin/xpack/security/saml/*",
InvalidateTokenAction.NAME, RefreshTokenAction.NAME));
InvalidateTokenAction.NAME, RefreshTokenAction.NAME, SamlSpMetadataAction.NAME));
private static final Set<String> MANAGE_OIDC_PATTERN = Collections.singleton("cluster:admin/xpack/security/oidc/*");
private static final Set<String> MANAGE_TOKEN_PATTERN = Collections.singleton("cluster:admin/xpack/security/token/*");
private static final Set<String> MANAGE_API_KEY_PATTERN = Collections.singleton("cluster:admin/xpack/security/api_key/*");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.core.security.action.saml;

import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.test.ESTestCase;

import java.io.IOException;

import static org.hamcrest.Matchers.containsString;

public class SamlSpMetadataRequestTests extends ESTestCase {

public void testValidateFailsWhenRealmEmpty() {
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("");
final ActionRequestValidationException validationException = samlSPMetadataRequest.validate();
assertThat(validationException.getMessage(), containsString("Realm name may not be empty"));
}

public void testValidateSerialization() throws IOException {
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1");
try (BytesStreamOutput out = new BytesStreamOutput()) {
samlSPMetadataRequest.writeTo(out);
try (StreamInput in = out.bytes().streamInput()) {
final SamlSpMetadataRequest serialized = new SamlSpMetadataRequest(in);
assertEquals(samlSPMetadataRequest.getRealmName(), serialized.getRealmName());
}
}
}

public void testValidateToString() {
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1");
assertThat(samlSPMetadataRequest.toString(), containsString("{realmName=saml1}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction;
import org.elasticsearch.xpack.core.security.action.saml.SamlCompleteLogoutAction;
import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
Expand Down Expand Up @@ -175,6 +176,7 @@
import org.elasticsearch.xpack.security.action.saml.TransportSamlLogoutAction;
import org.elasticsearch.xpack.security.action.saml.TransportSamlCompleteLogoutAction;
import org.elasticsearch.xpack.security.action.saml.TransportSamlPrepareAuthenticationAction;
import org.elasticsearch.xpack.security.action.saml.TransportSamlSpMetadataAction;
import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction;
import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction;
import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction;
Expand Down Expand Up @@ -244,6 +246,7 @@
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction;
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlCompleteLogoutAction;
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction;
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSpMetadataAction;
import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction;
import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction;
import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction;
Expand Down Expand Up @@ -822,6 +825,7 @@ public void onIndexModule(IndexModule module) {
new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class),
new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class),
new ActionHandler<>(SamlCompleteLogoutAction.INSTANCE, TransportSamlCompleteLogoutAction.class),
new ActionHandler<>(SamlSpMetadataAction.INSTANCE, TransportSamlSpMetadataAction.class),
new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE,
TransportOpenIdConnectPrepareAuthenticationAction.class),
new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class),
Expand Down Expand Up @@ -884,6 +888,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
new RestSamlLogoutAction(settings, getLicenseState()),
new RestSamlInvalidateSessionAction(settings, getLicenseState()),
new RestSamlCompleteLogoutAction(settings, getLicenseState()),
new RestSamlSpMetadataAction(settings, getLicenseState()),
new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()),
new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()),
new RestOpenIdConnectLogoutAction(settings, getLicenseState()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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.security.action.saml;

import org.apache.logging.log4j.message.ParameterizedMessage;
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.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest;
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse;
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.saml.SamlRealm;
import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder;
import org.elasticsearch.xpack.security.authc.saml.SamlUtils;
import org.elasticsearch.xpack.security.authc.saml.SpConfiguration;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
import org.w3c.dom.Element;

import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.util.List;
import java.util.Locale;

import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms;

/**
* Transport action responsible for generating a SAML SP Metadata.
*/
public class TransportSamlSpMetadataAction
extends HandledTransportAction<SamlSpMetadataRequest, SamlSpMetadataResponse> {

private final Realms realms;

@Inject
public TransportSamlSpMetadataAction(TransportService transportService, ActionFilters actionFilters, Realms realms) {
super(SamlSpMetadataAction.NAME, transportService, actionFilters, SamlSpMetadataRequest::new
);
this.realms = realms;
}

@Override
protected void doExecute(Task task, SamlSpMetadataRequest request,
ActionListener<SamlSpMetadataResponse> listener) {
List<SamlRealm> realms = findSamlRealms(this.realms, request.getRealmName(), null);
if (realms.isEmpty()) {
listener.onFailure(SamlUtils.samlException("Cannot find any matching realm for [{}]", request.getRealmName()));
} else if (realms.size() > 1) {
listener.onFailure(SamlUtils.samlException("Found multiple matching realms [{}] for [{}]", realms, request.getRealmName()));
} else {
prepareMetadata(realms.get(0), listener);
}
}

private void prepareMetadata(SamlRealm realm, ActionListener<SamlSpMetadataResponse> listener) {
try {
final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller();
final SpConfiguration spConfig = realm.getServiceProvider();
final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(Locale.getDefault(), spConfig.getEntityId())
.assertionConsumerServiceUrl(spConfig.getAscUrl())
.singleLogoutServiceUrl(spConfig.getLogoutUrl())
.encryptionCredentials(spConfig.getEncryptionCredentials())
.signingCredential(spConfig.getSigningConfiguration().getCredential())
.authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME));
final EntityDescriptor descriptor = builder.build();
final Element element = marshaller.marshall(descriptor);
final StringWriter writer = new StringWriter();
final Transformer serializer = SamlUtils.getHardenedXMLTransformer();
serializer.transform(new DOMSource(element), new StreamResult(writer));
listener.onResponse(new SamlSpMetadataResponse(writer.toString()));
} catch (Exception e) {
logger.error(new ParameterizedMessage(
"Error during SAML SP metadata generation for realm [{}]", realm.name()), e);
listener.onFailure(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ public static SamlRealm create(RealmConfig config, SSLService sslService, Resour
return realm;
}

public SpConfiguration getServiceProvider() {
return serviceProvider;
}

// For testing
SamlRealm(
RealmConfig config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ public EntityDescriptor build() throws Exception {
if (organization != null) {
descriptor.setOrganization(buildOrganization());
}
contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c)));
if(contacts.size() > 0) {
contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c)));
}

return descriptor;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/**
* Encapsulates the rules and credentials for how and when Elasticsearch should sign outgoing SAML messages.
*/
class SigningConfiguration {
public class SigningConfiguration {

private final Set<String> messageTypes;
private final X509Credential credential;
Expand All @@ -30,7 +30,7 @@ boolean shouldSign(SAMLObject object) {
return shouldSign(object.getElementQName().getLocalPart());
}

boolean shouldSign(String elementName) {
public boolean shouldSign(String elementName) {
if (credential == null) {
return false;
}
Expand All @@ -45,7 +45,7 @@ byte[] sign(byte[] content, String algo) throws SecurityException {
return XMLSigningUtil.signWithURI(this.credential, algo, content);
}

X509Credential getCredential() {
public X509Credential getCredential() {
return credential;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,23 @@ public SpConfiguration(final String entityId, final String ascUrl, final String
/**
* The SAML identifier (as a URI) for the Sp
*/
String getEntityId() {
public String getEntityId() {
return entityId;
}

String getAscUrl() {
public String getAscUrl() {
return ascUrl;
}

String getLogoutUrl() {
public String getLogoutUrl() {
return logoutUrl;
}

List<X509Credential> getEncryptionCredentials() {
public List<X509Credential> getEncryptionCredentials() {
return encryptionCredentials;
}

SigningConfiguration getSigningConfiguration() {
public SigningConfiguration getSigningConfiguration() {
return signingConfiguration;
}

Expand Down
Loading

0 comments on commit c6bfc96

Please sign in to comment.