Skip to content

Commit ad658c6

Browse files
Adding API for generating SAML SP metadata (#64517)
* 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]>
1 parent 710500a commit ad658c6

File tree

12 files changed

+327
-10
lines changed

12 files changed

+327
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.saml;
8+
9+
import org.elasticsearch.action.ActionType;
10+
11+
public class SamlSpMetadataAction extends ActionType<SamlSpMetadataResponse> {
12+
public static final String NAME = "cluster:monitor/xpack/security/saml/metadata";
13+
public static final SamlSpMetadataAction INSTANCE = new SamlSpMetadataAction();
14+
15+
private SamlSpMetadataAction() {
16+
super(NAME, SamlSpMetadataResponse::new);
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.saml;
8+
9+
import org.elasticsearch.action.ActionRequest;
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
15+
import java.io.IOException;
16+
17+
import static org.elasticsearch.action.ValidateActions.addValidationError;
18+
19+
public class SamlSpMetadataRequest extends ActionRequest {
20+
21+
private String realmName;
22+
23+
public SamlSpMetadataRequest(StreamInput in) throws IOException {
24+
super(in);
25+
realmName = in.readOptionalString();
26+
}
27+
28+
public SamlSpMetadataRequest(String realmName) {
29+
this.realmName = realmName;
30+
}
31+
32+
@Override
33+
public ActionRequestValidationException validate() {
34+
ActionRequestValidationException validationException = null;
35+
if (Strings.hasText(realmName) == false) {
36+
validationException = addValidationError("Realm name may not be empty", validationException);
37+
}
38+
return validationException;
39+
}
40+
41+
public String getRealmName() {
42+
return realmName;
43+
}
44+
45+
public void setRealmName(String realmName) {
46+
this.realmName = realmName;
47+
}
48+
49+
@Override
50+
public String toString() {
51+
return getClass().getSimpleName() + "{" +
52+
"realmName=" + realmName +
53+
'}';
54+
}
55+
56+
@Override
57+
public void writeTo(StreamOutput out) throws IOException {
58+
super.writeTo(out);
59+
out.writeOptionalString(realmName);
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.saml;
8+
9+
import org.elasticsearch.action.ActionResponse;
10+
import org.elasticsearch.common.io.stream.StreamInput;
11+
import org.elasticsearch.common.io.stream.StreamOutput;
12+
13+
import java.io.IOException;
14+
15+
/**
16+
* Response containing a SAML SP metadata for a specific realm as XML.
17+
*/
18+
public class SamlSpMetadataResponse extends ActionResponse {
19+
public String getXMLString() {
20+
return XMLString;
21+
}
22+
23+
private String XMLString;
24+
25+
public SamlSpMetadataResponse(StreamInput in) throws IOException {
26+
super(in);
27+
XMLString = in.readString();
28+
}
29+
30+
public SamlSpMetadataResponse(String XMLString) {
31+
this.XMLString = XMLString;
32+
}
33+
34+
@Override
35+
public void writeTo(StreamOutput out) throws IOException {
36+
out.writeString(XMLString);
37+
}
38+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.xpack.core.ilm.action.StopILMAction;
2323
import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
2424
import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction;
25+
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
2526
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
2627
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
2728
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
@@ -45,7 +46,7 @@ public class ClusterPrivilegeResolver {
4546
// shared automatons
4647
private static final Set<String> ALL_SECURITY_PATTERN = Set.of("cluster:admin/xpack/security/*");
4748
private static final Set<String> MANAGE_SAML_PATTERN = Set.of("cluster:admin/xpack/security/saml/*",
48-
InvalidateTokenAction.NAME, RefreshTokenAction.NAME);
49+
InvalidateTokenAction.NAME, RefreshTokenAction.NAME, SamlSpMetadataAction.NAME);
4950
private static final Set<String> MANAGE_OIDC_PATTERN = Set.of("cluster:admin/xpack/security/oidc/*");
5051
private static final Set<String> MANAGE_TOKEN_PATTERN = Set.of("cluster:admin/xpack/security/token/*");
5152
private static final Set<String> MANAGE_API_KEY_PATTERN = Set.of("cluster:admin/xpack/security/api_key/*");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action.saml;
8+
9+
import org.elasticsearch.action.ActionRequestValidationException;
10+
import org.elasticsearch.common.io.stream.BytesStreamOutput;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.test.ESTestCase;
13+
14+
import java.io.IOException;
15+
16+
import static org.hamcrest.Matchers.containsString;
17+
18+
public class SamlSpMetadataRequestTests extends ESTestCase {
19+
20+
public void testValidateFailsWhenRealmEmpty() {
21+
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("");
22+
final ActionRequestValidationException validationException = samlSPMetadataRequest.validate();
23+
assertThat(validationException.getMessage(), containsString("Realm name may not be empty"));
24+
}
25+
26+
public void testValidateSerialization() throws IOException {
27+
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1");
28+
try (BytesStreamOutput out = new BytesStreamOutput()) {
29+
samlSPMetadataRequest.writeTo(out);
30+
try (StreamInput in = out.bytes().streamInput()) {
31+
final SamlSpMetadataRequest serialized = new SamlSpMetadataRequest(in);
32+
assertEquals(samlSPMetadataRequest.getRealmName(), serialized.getRealmName());
33+
}
34+
}
35+
}
36+
37+
public void testValidateToString() {
38+
final SamlSpMetadataRequest samlSPMetadataRequest = new SamlSpMetadataRequest("saml1");
39+
assertThat(samlSPMetadataRequest.toString(), containsString("{realmName=saml1}"));
40+
}
41+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction;
109109
import org.elasticsearch.xpack.core.security.action.saml.SamlCompleteLogoutAction;
110110
import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction;
111+
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
111112
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
112113
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
113114
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
@@ -174,6 +175,7 @@
174175
import org.elasticsearch.xpack.security.action.saml.TransportSamlLogoutAction;
175176
import org.elasticsearch.xpack.security.action.saml.TransportSamlCompleteLogoutAction;
176177
import org.elasticsearch.xpack.security.action.saml.TransportSamlPrepareAuthenticationAction;
178+
import org.elasticsearch.xpack.security.action.saml.TransportSamlSpMetadataAction;
177179
import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction;
178180
import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction;
179181
import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction;
@@ -243,6 +245,7 @@
243245
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction;
244246
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlCompleteLogoutAction;
245247
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction;
248+
import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSpMetadataAction;
246249
import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction;
247250
import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction;
248251
import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction;
@@ -781,6 +784,7 @@ public void onIndexModule(IndexModule module) {
781784
new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class),
782785
new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class),
783786
new ActionHandler<>(SamlCompleteLogoutAction.INSTANCE, TransportSamlCompleteLogoutAction.class),
787+
new ActionHandler<>(SamlSpMetadataAction.INSTANCE, TransportSamlSpMetadataAction.class),
784788
new ActionHandler<>(OpenIdConnectPrepareAuthenticationAction.INSTANCE,
785789
TransportOpenIdConnectPrepareAuthenticationAction.class),
786790
new ActionHandler<>(OpenIdConnectAuthenticateAction.INSTANCE, TransportOpenIdConnectAuthenticateAction.class),
@@ -841,6 +845,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
841845
new RestSamlLogoutAction(settings, getLicenseState()),
842846
new RestSamlInvalidateSessionAction(settings, getLicenseState()),
843847
new RestSamlCompleteLogoutAction(settings, getLicenseState()),
848+
new RestSamlSpMetadataAction(settings, getLicenseState()),
844849
new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()),
845850
new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()),
846851
new RestOpenIdConnectLogoutAction(settings, getLicenseState()),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.security.action.saml;
8+
9+
import org.apache.logging.log4j.message.ParameterizedMessage;
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.action.support.ActionFilters;
12+
import org.elasticsearch.action.support.HandledTransportAction;
13+
import org.elasticsearch.common.inject.Inject;
14+
import org.elasticsearch.tasks.Task;
15+
import org.elasticsearch.transport.TransportService;
16+
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
17+
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest;
18+
import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse;
19+
import org.elasticsearch.xpack.security.authc.Realms;
20+
import org.elasticsearch.xpack.security.authc.saml.SamlRealm;
21+
import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder;
22+
import org.elasticsearch.xpack.security.authc.saml.SamlUtils;
23+
import org.elasticsearch.xpack.security.authc.saml.SpConfiguration;
24+
import org.opensaml.saml.saml2.core.AuthnRequest;
25+
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
26+
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
27+
import org.w3c.dom.Element;
28+
29+
import javax.xml.transform.Transformer;
30+
import javax.xml.transform.dom.DOMSource;
31+
import javax.xml.transform.stream.StreamResult;
32+
import java.io.StringWriter;
33+
import java.util.List;
34+
import java.util.Locale;
35+
36+
import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms;
37+
38+
/**
39+
* Transport action responsible for generating a SAML SP Metadata.
40+
*/
41+
public class TransportSamlSpMetadataAction
42+
extends HandledTransportAction<SamlSpMetadataRequest, SamlSpMetadataResponse> {
43+
44+
private final Realms realms;
45+
46+
@Inject
47+
public TransportSamlSpMetadataAction(TransportService transportService, ActionFilters actionFilters, Realms realms) {
48+
super(SamlSpMetadataAction.NAME, transportService, actionFilters, SamlSpMetadataRequest::new
49+
);
50+
this.realms = realms;
51+
}
52+
53+
@Override
54+
protected void doExecute(Task task, SamlSpMetadataRequest request,
55+
ActionListener<SamlSpMetadataResponse> listener) {
56+
List<SamlRealm> realms = findSamlRealms(this.realms, request.getRealmName(), null);
57+
if (realms.isEmpty()) {
58+
listener.onFailure(SamlUtils.samlException("Cannot find any matching realm for [{}]", request.getRealmName()));
59+
} else if (realms.size() > 1) {
60+
listener.onFailure(SamlUtils.samlException("Found multiple matching realms [{}] for [{}]", realms, request.getRealmName()));
61+
} else {
62+
prepareMetadata(realms.get(0), listener);
63+
}
64+
}
65+
66+
private void prepareMetadata(SamlRealm realm, ActionListener<SamlSpMetadataResponse> listener) {
67+
try {
68+
final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller();
69+
final SpConfiguration spConfig = realm.getServiceProvider();
70+
final SamlSpMetadataBuilder builder = new SamlSpMetadataBuilder(Locale.getDefault(), spConfig.getEntityId())
71+
.assertionConsumerServiceUrl(spConfig.getAscUrl())
72+
.singleLogoutServiceUrl(spConfig.getLogoutUrl())
73+
.encryptionCredentials(spConfig.getEncryptionCredentials())
74+
.signingCredential(spConfig.getSigningConfiguration().getCredential())
75+
.authnRequestsSigned(spConfig.getSigningConfiguration().shouldSign(AuthnRequest.DEFAULT_ELEMENT_LOCAL_NAME));
76+
final EntityDescriptor descriptor = builder.build();
77+
final Element element = marshaller.marshall(descriptor);
78+
final StringWriter writer = new StringWriter();
79+
final Transformer serializer = SamlUtils.getHardenedXMLTransformer();
80+
serializer.transform(new DOMSource(element), new StreamResult(writer));
81+
listener.onResponse(new SamlSpMetadataResponse(writer.toString()));
82+
} catch (Exception e) {
83+
logger.error(new ParameterizedMessage(
84+
"Error during SAML SP metadata generation for realm [{}]", realm.name()), e);
85+
listener.onFailure(e);
86+
}
87+
}
88+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java

+4
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ public static SamlRealm create(RealmConfig config, SSLService sslService, Resour
208208
return realm;
209209
}
210210

211+
public SpConfiguration getServiceProvider() {
212+
return serviceProvider;
213+
}
214+
211215
// For testing
212216
SamlRealm(
213217
RealmConfig config,

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlSpMetadataBuilder.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,9 @@ public EntityDescriptor build() throws Exception {
240240
if (organization != null) {
241241
descriptor.setOrganization(buildOrganization());
242242
}
243-
contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c)));
243+
if(contacts.size() > 0) {
244+
contacts.forEach(c -> descriptor.getContactPersons().add(buildContact(c)));
245+
}
244246

245247
return descriptor;
246248
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SigningConfiguration.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
/**
1717
* Encapsulates the rules and credentials for how and when Elasticsearch should sign outgoing SAML messages.
1818
*/
19-
class SigningConfiguration {
19+
public class SigningConfiguration {
2020

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

33-
boolean shouldSign(String elementName) {
33+
public boolean shouldSign(String elementName) {
3434
if (credential == null) {
3535
return false;
3636
}
@@ -45,7 +45,7 @@ byte[] sign(byte[] content, String algo) throws SecurityException {
4545
return XMLSigningUtil.signWithURI(this.credential, algo, content);
4646
}
4747

48-
X509Credential getCredential() {
48+
public X509Credential getCredential() {
4949
return credential;
5050
}
5151
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SpConfiguration.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,23 @@ public SpConfiguration(final String entityId, final String ascUrl, final String
4141
/**
4242
* The SAML identifier (as a URI) for the Sp
4343
*/
44-
String getEntityId() {
44+
public String getEntityId() {
4545
return entityId;
4646
}
4747

48-
String getAscUrl() {
48+
public String getAscUrl() {
4949
return ascUrl;
5050
}
5151

52-
String getLogoutUrl() {
52+
public String getLogoutUrl() {
5353
return logoutUrl;
5454
}
5555

56-
List<X509Credential> getEncryptionCredentials() {
56+
public List<X509Credential> getEncryptionCredentials() {
5757
return encryptionCredentials;
5858
}
5959

60-
SigningConfiguration getSigningConfiguration() {
60+
public SigningConfiguration getSigningConfiguration() {
6161
return signingConfiguration;
6262
}
6363

0 commit comments

Comments
 (0)