saml: introduce saml2.check.signature (#9219)

Adminstrators should ensure that IDP configuration has signing
certificate for the actual signature check to be performed. In addition
to this, this change introduces a new global setting
`saml2.check.signature` which can deliberately fail a SAML login attempt
when the SAML response has missing signature.

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2024-06-13 11:30:33 +05:30 committed by GitHub
parent 8221be3a8c
commit 78ace3a750
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 19 deletions

View File

@ -144,6 +144,14 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
return responseObject; return responseObject;
} }
protected void checkAndFailOnMissingSAMLSignature(Signature signature) {
if (signature == null && SAML2AuthManager.SAMLCheckSignature.value()) {
s_logger.error("Failing SAML login due to missing signature in the SAML response and signature check is enforced. " +
"Please check and ensure the IDP configuration has signing certificate or relax the saml2.check.signature setting.");
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Signature is missing from the SAML Response. Please contact the Administrator");
}
}
@Override @Override
public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final InetAddress remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final InetAddress remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
try { try {
@ -225,6 +233,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
session.setAttribute(SAMLPluginConstants.SAML_IDPID, issuer.getValue()); session.setAttribute(SAMLPluginConstants.SAML_IDPID, issuer.getValue());
Signature sig = processedSAMLResponse.getSignature(); Signature sig = processedSAMLResponse.getSignature();
checkAndFailOnMissingSAMLSignature(sig);
if (idpMetadata.getSigningCertificate() != null && sig != null) { if (idpMetadata.getSigningCertificate() != null && sig != null) {
BasicX509Credential credential = new BasicX509Credential(); BasicX509Credential credential = new BasicX509Credential();
credential.setEntityCertificate(idpMetadata.getSigningCertificate()); credential.setEntityCertificate(idpMetadata.getSigningCertificate());
@ -238,9 +247,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
params, responseType)); params, responseType));
} }
} }
if (username == null) {
username = SAMLUtils.getValueFromAssertions(processedSAMLResponse.getAssertions(), SAML2AuthManager.SAMLUserAttributeName.value()); username = SAMLUtils.getValueFromAssertions(processedSAMLResponse.getAssertions(), SAML2AuthManager.SAMLUserAttributeName.value());
}
for (Assertion assertion: processedSAMLResponse.getAssertions()) { for (Assertion assertion: processedSAMLResponse.getAssertions()) {
if (assertion!= null && assertion.getSubject() != null && assertion.getSubject().getNameID() != null) { if (assertion!= null && assertion.getSubject() != null && assertion.getSubject().getNameID() != null) {
@ -272,6 +280,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
continue; continue;
} }
Signature encSig = assertion.getSignature(); Signature encSig = assertion.getSignature();
checkAndFailOnMissingSAMLSignature(encSig);
if (idpMetadata.getSigningCertificate() != null && encSig != null) { if (idpMetadata.getSigningCertificate() != null && encSig != null) {
BasicX509Credential sigCredential = new BasicX509Credential(); BasicX509Credential sigCredential = new BasicX509Credential();
sigCredential.setEntityCertificate(idpMetadata.getSigningCertificate()); sigCredential.setEntityCertificate(idpMetadata.getSigningCertificate());

View File

@ -25,51 +25,54 @@ import java.util.Collection;
public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableService { public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableService {
public static final ConfigKey<Boolean> SAMLIsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.enabled", "false", ConfigKey<Boolean> SAMLIsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.enabled", "false",
"Indicates whether SAML SSO plugin is enabled or not", true); "Indicates whether SAML SSO plugin is enabled or not", true);
public static final ConfigKey<String> SAMLServiceProviderID = new ConfigKey<String>("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack", ConfigKey<String> SAMLServiceProviderID = new ConfigKey<String>("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack",
"SAML2 Service Provider Identifier String", true); "SAML2 Service Provider Identifier String", true);
public static final ConfigKey<String> SAMLServiceProviderContactPersonName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers", ConfigKey<String> SAMLServiceProviderContactPersonName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers",
"SAML2 Service Provider Contact Person Name", true); "SAML2 Service Provider Contact Person Name", true);
public static final ConfigKey<String> SAMLServiceProviderContactEmail = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org", ConfigKey<String> SAMLServiceProviderContactEmail = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org",
"SAML2 Service Provider Contact Email Address", true); "SAML2 Service Provider Contact Email Address", true);
public static final ConfigKey<String> SAMLServiceProviderOrgName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack", ConfigKey<String> SAMLServiceProviderOrgName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack",
"SAML2 Service Provider Organization Name", true); "SAML2 Service Provider Organization Name", true);
public static final ConfigKey<String> SAMLServiceProviderOrgUrl = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org", ConfigKey<String> SAMLServiceProviderOrgUrl = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org",
"SAML2 Service Provider Organization URL", true); "SAML2 Service Provider Organization URL", true);
public static final ConfigKey<String> SAMLServiceProviderSingleSignOnURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso", ConfigKey<String> SAMLServiceProviderSingleSignOnURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso",
"SAML2 CloudStack Service Provider Single Sign On URL", true); "SAML2 CloudStack Service Provider Single Sign On URL", true);
public static final ConfigKey<String> SAMLServiceProviderSingleLogOutURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/", ConfigKey<String> SAMLServiceProviderSingleLogOutURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/",
"SAML2 CloudStack Service Provider Single Log Out URL", true); "SAML2 CloudStack Service Provider Single Log Out URL", true);
public static final ConfigKey<String> SAMLCloudStackRedirectionUrl = new ConfigKey<String>("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client", ConfigKey<String> SAMLCloudStackRedirectionUrl = new ConfigKey<String>("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client",
"The CloudStack UI url the SSO should redirected to when successful", true); "The CloudStack UI url the SSO should redirected to when successful", true);
public static final ConfigKey<String> SAMLUserAttributeName = new ConfigKey<String>("Advanced", String.class, "saml2.user.attribute", "uid", ConfigKey<String> SAMLUserAttributeName = new ConfigKey<String>("Advanced", String.class, "saml2.user.attribute", "uid",
"Attribute name to be looked for in SAML response that will contain the username", true); "Attribute name to be looked for in SAML response that will contain the username", true);
public static final ConfigKey<String> SAMLIdentityProviderMetadataURL = new ConfigKey<String>("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php", ConfigKey<String> SAMLIdentityProviderMetadataURL = new ConfigKey<String>("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php",
"SAML2 Identity Provider Metadata XML Url", true); "SAML2 Identity Provider Metadata XML Url", true);
public static final ConfigKey<String> SAMLDefaultIdentityProviderId = new ConfigKey<String>("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no", ConfigKey<String> SAMLDefaultIdentityProviderId = new ConfigKey<String>("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no",
"The default IdP entity ID to use only in case of multiple IdPs", true); "The default IdP entity ID to use only in case of multiple IdPs", true);
public static final ConfigKey<String> SAMLSignatureAlgorithm = new ConfigKey<>(String.class, "saml2.sigalg", "Advanced", "SHA1", ConfigKey<String> SAMLSignatureAlgorithm = new ConfigKey<>(String.class, "saml2.sigalg", "Advanced", "SHA1",
"The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select, "SHA1,SHA256,SHA384,SHA512"); "The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select, "SHA1,SHA256,SHA384,SHA512");
public static final ConfigKey<Boolean> SAMLAppendDomainSuffix = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.append.idpdomain", "false", ConfigKey<Boolean> SAMLAppendDomainSuffix = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.append.idpdomain", "false",
"If enabled, create account/user dialog with SAML SSO enabled will append the IdP domain to the user or account name in the UI dialog", true); "If enabled, create account/user dialog with SAML SSO enabled will append the IdP domain to the user or account name in the UI dialog", true);
public static final ConfigKey<Integer> SAMLTimeout = new ConfigKey<Integer>("Advanced", Integer.class, "saml2.timeout", "1800", ConfigKey<Integer> SAMLTimeout = new ConfigKey<Integer>("Advanced", Integer.class, "saml2.timeout", "1800",
"SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true); "SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true);
ConfigKey<Boolean> SAMLCheckSignature = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.check.signature", "false",
"Whether SAML2 signature must be checked, when enforced and when the SAML response does not have a signature would lead to login exception", true);
public SAMLProviderMetadata getSPMetadata(); public SAMLProviderMetadata getSPMetadata();
public SAMLProviderMetadata getIdPMetadata(String entityId); public SAMLProviderMetadata getIdPMetadata(String entityId);
public Collection<SAMLProviderMetadata> getAllIdPMetadata(); public Collection<SAMLProviderMetadata> getAllIdPMetadata();

View File

@ -535,6 +535,6 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL, SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName, SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId, SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout}; SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature};
} }
} }

View File

@ -271,6 +271,30 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 0, 0); verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 0, 0);
} }
private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException {
Field f = ConfigKey.class.getDeclaredField(name);
f.setAccessible(true);
f.set(configKey, o);
}
@Test
public void testFailOnSAMLSignatureCheckWhenFalse() throws NoSuchFieldException, IllegalAccessException {
overrideDefaultConfigValue(SAML2AuthManager.SAMLCheckSignature, "_defaultValue", "false");
SAML2LoginAPIAuthenticatorCmd cmd = new SAML2LoginAPIAuthenticatorCmd();
try {
cmd.checkAndFailOnMissingSAMLSignature(null);
} catch(Exception e) {
Assert.fail("This shouldn't throw any exception");
}
}
@Test(expected = ServerApiException.class)
public void testFailOnSAMLSignatureCheckWhenTrue() throws NoSuchFieldException, IllegalAccessException {
overrideDefaultConfigValue(SAML2AuthManager.SAMLCheckSignature, "_defaultValue", "true");
SAML2LoginAPIAuthenticatorCmd cmd = new SAML2LoginAPIAuthenticatorCmd();
cmd.checkAndFailOnMissingSAMLSignature(null);
}
private UserAccountVO configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(String entity, String configurationValue, Boolean isUserAuthorized) private UserAccountVO configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(String entity, String configurationValue, Boolean isUserAuthorized)
throws IOException { throws IOException {
Mockito.when(samlAuthManager.isUserAuthorized(nullable(Long.class), nullable(String.class))).thenReturn(isUserAuthorized); Mockito.when(samlAuthManager.isUserAuthorized(nullable(Long.class), nullable(String.class))).thenReturn(isUserAuthorized);