Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"name_id_format": {
"type": "keyword"
},
"sign_messages": {
"type": "keyword"
},
"authn_expiry_ms": {
"type": "long"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ protected void doExecute(Task task, final PutSamlServiceProviderRequest request,
// the same time, one will fail. That's not ideal, but it's better than having 1 silently overwrite the other.
writeDocument(document, DocWriteRequest.OpType.CREATE, listener);
} else if (matchingDocuments.size() == 1) {
SamlServiceProviderDocument existingDoc = Iterables.get(matchingDocuments, 0);
final SamlServiceProviderDocument existingDoc = Iterables.get(matchingDocuments, 0).getDocument();
assert existingDoc.docId != null : "Loaded document with no doc id";
assert existingDoc.entityId.equals(document.entityId) : "Loaded document with non-matching entity-id";
document.setDocId(existingDoc.docId);
document.setCreated(existingDoc.created);
writeDocument(document, DocWriteRequest.OpType.INDEX, listener);
} else {
logger.warn("Found multiple existing service providers in [{}] with entity id [{}] - [{}]",
index, document.entityId, matchingDocuments.stream().map(d -> d.docId).collect(Collectors.joining(",")));
index, document.entityId, matchingDocuments.stream().map(d -> d.getDocument().docId).collect(Collectors.joining(",")));
listener.onFailure(new IllegalStateException(
"Multiple service providers already exist with entity id [" + document.entityId + "]"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
import java.util.Collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

Expand Down Expand Up @@ -144,19 +148,19 @@ private void validateAuthnRequest(AuthnRequest authnRequest, SamlServiceProvider
parsedQueryString.queryString), listener);
return;
}
final X509Credential spSigningCredential = sp.getSpSigningCredential();
if (spSigningCredential == null) {
final Set<X509Credential> spSigningCredentials = sp.getSpSigningCredentials();
if (spSigningCredentials == null || spSigningCredentials.isEmpty()) {
logAndRespond(
"Unable to validate signature of authentication request, " +
"Service Provider hasn't registered signing credentials",
listener);
return;
}
if (validateSignature(parsedQueryString, spSigningCredential) == false) {
if (validateSignature(parsedQueryString, spSigningCredentials) == false) {
logAndRespond(
new ParameterizedMessage("Unable to validate signature of authentication request [{}] using credentials [{}]",
parsedQueryString.queryString,
samlFactory.describeCredentials(Collections.singletonList(spSigningCredential))), listener);
samlFactory.describeCredentials(spSigningCredentials)), listener);
return;
}
} else if (Strings.hasText(parsedQueryString.sigAlg)) {
Expand Down Expand Up @@ -200,17 +204,25 @@ private Map<String, Object> buildAuthnState(AuthnRequest request, SamlServicePro
return authnState;
}

private boolean validateSignature(ParsedQueryString queryString, X509Credential credential) {
try {
final String queryParam = queryString.reconstructQueryParameters();
Signature sig = Signature.getInstance(samlFactory.getJavaAlorithmNameFromUri(queryString.sigAlg));
sig.initVerify(credential.getEntityCertificate().getPublicKey());
sig.update(queryParam.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(queryString.signature));
} catch (Exception e) {
throw new ElasticsearchSecurityException("Unable to validate signature of authentication request using credentials [{}]",
samlFactory.describeCredentials(Collections.singletonList(credential)), e);
}
private boolean validateSignature(ParsedQueryString queryString, Collection<X509Credential> credentials) {
final String javaSigAlgorithm = samlFactory.getJavaAlorithmNameFromUri(queryString.sigAlg);
final byte[] contentBytes = queryString.reconstructQueryParameters().getBytes(StandardCharsets.UTF_8);
final byte[] signatureBytes = Base64.getDecoder().decode(queryString.signature);
return credentials.stream().anyMatch(credential -> {
try {
Signature sig = Signature.getInstance(javaSigAlgorithm);
sig.initVerify(credential.getEntityCertificate().getPublicKey());
sig.update(contentBytes);
return sig.verify(signatureBytes);
} catch (NoSuchAlgorithmException e) {
throw new ElasticsearchSecurityException("Java signature algorithm [{}] is not available for SAML/XML-Sig algorithm [{}]",
e, javaSigAlgorithm, queryString.sigAlg);
} catch (InvalidKeyException | SignatureException e) {
logger.warn(new ParameterizedMessage("Signature verification failed for credential [{}]",
samlFactory.describeCredentials(Set.of(credential))), e);
return false;
}
});
}

private void getSpFromIssuer(Issuer issuer, ActionListener<SamlServiceProvider> listener) {
Expand Down Expand Up @@ -327,10 +339,14 @@ private ParsedQueryString(String queryString, String samlRequest, String relaySt
this.signature = signature;
}

public String reconstructQueryParameters() throws UnsupportedEncodingException {
return relayState == null ?
"SAMLRequest=" + urlEncode(samlRequest) + "&SigAlg=" + urlEncode(sigAlg) :
"SAMLRequest=" + urlEncode(samlRequest) + "&RelayState=" + urlEncode(relayState) + "&SigAlg=" + urlEncode(sigAlg);
public String reconstructQueryParameters() throws ElasticsearchSecurityException {
try {
return relayState == null ?
"SAMLRequest=" + urlEncode(samlRequest) + "&SigAlg=" + urlEncode(sigAlg) :
"SAMLRequest=" + urlEncode(samlRequest) + "&RelayState=" + urlEncode(relayState) + "&SigAlg=" + urlEncode(sigAlg);
} catch (UnsupportedEncodingException e) {
throw new ElasticsearchSecurityException("Cannot reconstruct query for signature verification", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.idp.saml.authn;

import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.idp.authc.AuthenticationMethod;
import org.elasticsearch.xpack.idp.authc.NetworkControl;
import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
Expand Down Expand Up @@ -172,6 +173,7 @@ private String resolveAuthnClass(Set<AuthenticationMethod> authenticationMethods
private AttributeStatement buildAttributes(UserServiceAuthentication user) {
final SamlServiceProvider serviceProvider = user.getServiceProvider();
final AttributeStatement statement = samlFactory.object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME);
// TODO Add principal, email, name
final Attribute groups = buildAttribute(serviceProvider.getAttributeNames().groups, "groups", user.getGroups());
if (groups != null) {
statement.getAttributes().add(groups);
Expand All @@ -183,7 +185,7 @@ private AttributeStatement buildAttributes(UserServiceAuthentication user) {
}

private Attribute buildAttribute(String formalName, String friendlyName, Collection<String> values) {
if (values.isEmpty()) {
if (values.isEmpty() || Strings.isNullOrEmpty(formalName)) {
return null;
}
final Attribute attribute = samlFactory.object(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings;
import org.elasticsearch.xpack.idp.saml.sp.CloudServiceProvider;
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider;
import org.joda.time.Duration;
import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration;
import org.opensaml.security.x509.X509Credential;
import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter;

import javax.net.ssl.X509KeyManager;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PrivateKey;
import java.util.ArrayList;
Expand Down Expand Up @@ -54,12 +57,16 @@ public class CloudIdp implements SamlIdentityProvider {
private final String entityId;
private final HashMap<String, URL> ssoEndpoints = new HashMap<>();
private final HashMap<String, URL> sloEndpoints = new HashMap<>();
private Map<String, SamlServiceProvider> registeredServiceProviders;
private final ServiceProviderDefaults serviceProviderDefaults;

private final X509Credential signingCredential;
private final X509Credential metadataSigningCredential;

private SamlIdPMetadataBuilder.ContactInfo technicalContact;
private SamlIdPMetadataBuilder.OrganizationInfo organization;

private Map<String, SamlServiceProvider> registeredServiceProviders;

public CloudIdp(Environment env, Settings settings) {
this.entityId = require(settings, IDP_ENTITY_ID);
this.ssoEndpoints.put(SAML2_REDIRECT_BINDING_URI, requiredUrl(settings, IDP_SSO_REDIRECT_ENDPOINT));
Expand All @@ -73,6 +80,7 @@ public CloudIdp(Environment env, Settings settings) {
this.sloEndpoints.put(SAML2_REDIRECT_BINDING_URI, IDP_SLO_REDIRECT_ENDPOINT.get(settings));
}
this.registeredServiceProviders = gatherRegisteredServiceProviders();
this.serviceProviderDefaults = new ServiceProviderDefaults("elastic-cloud", "action:login", TRANSIENT, Duration.standardMinutes(5));
this.signingCredential = buildSigningCredential(env, settings, "xpack.idp.signing.");
this.metadataSigningCredential = buildSigningCredential(env, settings, "xpack.idp.metadata_signing.");
this.technicalContact = buildContactInfo(settings);
Expand Down Expand Up @@ -105,6 +113,11 @@ public void getRegisteredServiceProvider(String spEntityId, ActionListener<SamlS
listener.onResponse(registeredServiceProviders.get(spEntityId));
}

@Override
public ServiceProviderDefaults getServiceProviderDefaults() {
return serviceProviderDefaults;
}

@Override
public X509Credential getSigningCredential() {
return signingCredential;
Expand Down Expand Up @@ -219,9 +232,19 @@ private Map<String, SamlServiceProvider> gatherRegisteredServiceProviders() {
// TODO Fetch all the registered service providers from the index (?) they are persisted.
// For now hardcode something to use.
Map<String, SamlServiceProvider> registeredSps = new HashMap<>();
registeredSps.put("https://sp.some.org",
new CloudServiceProvider("https://sp.some.org", "https://sp.some.org/api/security/v1/saml", Set.of(TRANSIENT), null, false,
false, null));
try {
registeredSps.put("https://sp.some.org",
new CloudServiceProvider("https://sp.some.org", new URL("https://sp.some.org/api/security/v1/saml"), Set.of(TRANSIENT),
Duration.standardMinutes(5), null,
new SamlServiceProvider.AttributeNames(
"https://saml.elasticsearch.org/attributes/principal",
"https://saml.elasticsearch.org/attributes/name",
"https://saml.elasticsearch.org/attributes/email",
"https://saml.elasticsearch.org/attributes/groups"),
null, false, false));
} catch (MalformedURLException e) {
throw new UncheckedIOException(e);
}
return registeredSps;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider;
import org.joda.time.ReadableDuration;
import org.opensaml.security.x509.X509Credential;

import java.net.URL;
Expand All @@ -24,6 +25,8 @@ public interface SamlIdentityProvider {

URL getSingleLogoutEndpoint(String binding);

ServiceProviderDefaults getServiceProviderDefaults();

void getRegisteredServiceProvider(String spEntityId, ActionListener<SamlServiceProvider> listener);

X509Credential getSigningCredential();
Expand All @@ -33,4 +36,21 @@ public interface SamlIdentityProvider {
SamlIdPMetadataBuilder.OrganizationInfo getOrganization();

SamlIdPMetadataBuilder.ContactInfo getTechnicalContact();

final class ServiceProviderDefaults {
public final String applicationName;
public final String loginAction;
public final String nameIdFormat;
public final ReadableDuration authenticationExpiry;

public ServiceProviderDefaults(String applicationName,
String loginAction,
String nameIdFormat,
ReadableDuration authenticationExpiry) {
this.applicationName = applicationName;
this.loginAction = loginAction;
this.nameIdFormat = nameIdFormat;
this.authenticationExpiry = authenticationExpiry;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,47 @@

package org.elasticsearch.xpack.idp.saml.sp;

import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges;
import org.joda.time.Duration;
import org.joda.time.ReadableDuration;
import org.opensaml.security.x509.X509Credential;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.Set;


public class CloudServiceProvider implements SamlServiceProvider {

private final String entityid;
private final String entityId;
private final URL assertionConsumerService;
private final Set<String> allowedNameIdFormats;
private final ReadableDuration authnExpiry;
private final ServiceProviderPrivileges privileges;
private final Set<String> allowedNameIdFormats;
private final X509Credential spSigningCredential;
private final AttributeNames attributeNames;
private final Set<X509Credential> spSigningCredentials;
private final boolean signAuthnRequests;
private final boolean signLogoutRequests;

public CloudServiceProvider(String entityId, String assertionConsumerService, Set<String> allowedNameIdFormats,
ServiceProviderPrivileges privileges, boolean signAuthnRequests, boolean signLogoutRequests,
@Nullable X509Credential spSigningCredential) {
public CloudServiceProvider(String entityId, URL assertionConsumerService, Set<String> allowedNameIdFormats,
ReadableDuration authnExpiry, ServiceProviderPrivileges privileges, AttributeNames attributeNames,
Set<X509Credential> spSigningCredentials, boolean signAuthnRequests, boolean signLogoutRequests) {
if (Strings.isNullOrEmpty(entityId)) {
throw new IllegalArgumentException("Service Provider Entity ID cannot be null or empty");
}
this.entityid = entityId;
try {
this.assertionConsumerService = new URL(assertionConsumerService);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URL for Assertion Consumer Service", e);
}
this.entityId = entityId;
this.assertionConsumerService = assertionConsumerService;
this.allowedNameIdFormats = Set.copyOf(allowedNameIdFormats);
this.authnExpiry = Duration.standardMinutes(5);
this.privileges = new ServiceProviderPrivileges("cloud-idp", "service$" + entityId, "action:sso", Map.of());
this.spSigningCredential = spSigningCredential;
this.authnExpiry = authnExpiry;
this.privileges = privileges;
this.attributeNames = attributeNames;
this.spSigningCredentials = spSigningCredentials == null ? Set.of() : Set.copyOf(spSigningCredentials);
this.signLogoutRequests = signLogoutRequests;
this.signAuthnRequests = signAuthnRequests;

}

@Override
public String getEntityId() {
return entityid;
return entityId;
}

@Override
Expand All @@ -73,12 +66,12 @@ public ReadableDuration getAuthnExpiry() {

@Override
public AttributeNames getAttributeNames() {
return new SamlServiceProvider.AttributeNames();
return attributeNames;
}

@Override
public X509Credential getSpSigningCredential() {
return spSigningCredential;
public Set<X509Credential> getSpSigningCredentials() {
return spSigningCredentials;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

package org.elasticsearch.xpack.idp.saml.sp;

import org.elasticsearch.common.Nullable;
import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges;
import org.joda.time.ReadableDuration;
import org.opensaml.security.x509.X509Credential;
Expand All @@ -27,17 +26,26 @@ public interface SamlServiceProvider {
ReadableDuration getAuthnExpiry();

class AttributeNames {
public final String groups = "https://saml.elasticsearch.org/attributes/groups";
public final String principal;
public final String name;
public final String email;
public final String groups;

public AttributeNames(String principal, String name, String email, String groups) {
this.principal = principal;
this.name = name;
this.email = email;
this.groups = groups;
}
}

AttributeNames getAttributeNames();

@Nullable
X509Credential getSpSigningCredential();
ServiceProviderPrivileges getPrivileges();

Set<X509Credential> getSpSigningCredentials();

boolean shouldSignAuthnRequests();

boolean shouldSignLogoutRequests();

ServiceProviderPrivileges getPrivileges();
}
Loading