Skip to content

Commit c6b6114

Browse files
authored
saml: Fix SAML SSO plugin redirect URL (apache#6457)
This PR fixes the issue apache#6427 -> SAML request must be appended to an IdP URL as a query param with an ampersand, if the URL already contains a question mark, as opposed to always assume that IdP URLs don't have any query params. Google's IdP URL for instance looks like this: https://accounts.google.com/o/saml2/idp?idpid=<ID>, therefore the expected redirect URL would be https://accounts.google.com/o/saml2/idp?idpid=<ID>&SAMLRequest=<SAMLRequest> This code change is backwards compatible with the current behaviour.
1 parent f1bce69 commit c6b6114

File tree

4 files changed

+76
-7
lines changed

4 files changed

+76
-7
lines changed

plugins/user-authenticators/saml2/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,11 @@
5353
<version>${cs.xercesImpl.version}</version>
5454
<scope>test</scope>
5555
</dependency>
56+
<dependency>
57+
<groupId>org.assertj</groupId>
58+
<artifactId>assertj-core</artifactId>
59+
<version>${cs.assertj.version}</version>
60+
<scope>test</scope>
61+
</dependency>
5662
</dependencies>
5763
</project>

plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ public static String buildAuthnRequestUrl(final String authnId, final SAMLProvid
150150
if (spMetadata.getKeyPair() != null) {
151151
privateKey = spMetadata.getKeyPair().getPrivate();
152152
}
153-
redirectUrl = idpMetadata.getSsoUrl() + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey, signatureAlgorithm);
153+
String appendOperator = idpMetadata.getSsoUrl().contains("?") ? "&" : "?";
154+
redirectUrl = idpMetadata.getSsoUrl() + appendOperator + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey, signatureAlgorithm);
154155
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) {
155156
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
156157
}

plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/SAMLUtilsTest.java

+67-6
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@
1919

2020
package org.apache.cloudstack;
2121

22-
import java.security.KeyPair;
23-
import java.security.PrivateKey;
24-
import java.security.PublicKey;
25-
import java.util.regex.Pattern;
26-
22+
import junit.framework.TestCase;
23+
import org.apache.cloudstack.saml.SAML2AuthManager;
24+
import org.apache.cloudstack.saml.SAMLProviderMetadata;
2725
import org.apache.cloudstack.saml.SAMLUtils;
2826
import org.apache.cloudstack.utils.security.CertUtils;
2927
import org.junit.Test;
3028
import org.opensaml.saml2.core.AuthnRequest;
3129
import org.opensaml.saml2.core.LogoutRequest;
3230

33-
import junit.framework.TestCase;
31+
import java.net.URI;
32+
import java.security.KeyPair;
33+
import java.security.PrivateKey;
34+
import java.security.PublicKey;
35+
import java.util.regex.Pattern;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
3438

3539
public class SAMLUtilsTest extends TestCase {
3640

@@ -60,6 +64,63 @@ public void testBuildAuthnRequestObject() throws Exception {
6064
assertEquals(req.getIssuer().getValue(), spId);
6165
}
6266

67+
@Test
68+
public void testBuildAuthnRequestUrlWithoutQueryParam() throws Exception {
69+
String urlScheme = "http";
70+
71+
String spDomain = "sp.domain.example";
72+
String spUrl = urlScheme + "://" + spDomain;
73+
String spId = "serviceProviderId";
74+
75+
String idpDomain = "idp.domain.example";
76+
String idpUrl = urlScheme + "://" + idpDomain;
77+
String idpId = "identityProviderId";
78+
79+
String authnId = SAMLUtils.generateSecureRandomId();
80+
81+
SAMLProviderMetadata spMetadata = new SAMLProviderMetadata();
82+
spMetadata.setEntityId(spId);
83+
spMetadata.setSsoUrl(spUrl);
84+
85+
SAMLProviderMetadata idpMetadata = new SAMLProviderMetadata();
86+
idpMetadata.setSsoUrl(idpUrl);
87+
idpMetadata.setEntityId(idpId);
88+
89+
URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value()));
90+
assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("SAMLRequest");
91+
assertEquals(urlScheme, redirectUrl.getScheme());
92+
assertEquals(idpDomain, redirectUrl.getHost());
93+
}
94+
95+
@Test
96+
public void testBuildAuthnRequestUrlWithQueryParam() throws Exception {
97+
String urlScheme = "http";
98+
99+
String spDomain = "sp.domain.example";
100+
String spUrl = urlScheme + "://" + spDomain;
101+
String spId = "cloudstack";
102+
103+
String idpDomain = "idp.domain.example";
104+
String idpQueryParam = "idpid=CX1298373";
105+
String idpUrl = urlScheme + "://" + idpDomain + "?" + idpQueryParam;
106+
String idpId = "identityProviderId";
107+
108+
String authnId = SAMLUtils.generateSecureRandomId();
109+
110+
SAMLProviderMetadata spMetadata = new SAMLProviderMetadata();
111+
spMetadata.setEntityId(spId);
112+
spMetadata.setSsoUrl(spUrl);
113+
114+
SAMLProviderMetadata idpMetadata = new SAMLProviderMetadata();
115+
idpMetadata.setSsoUrl(idpUrl);
116+
idpMetadata.setEntityId(idpId);
117+
118+
URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value()));
119+
assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("idpid").hasParameter("SAMLRequest");
120+
assertEquals(urlScheme, redirectUrl.getScheme());
121+
assertEquals(idpDomain, redirectUrl.getHost());
122+
}
123+
63124
@Test
64125
public void testBuildLogoutRequest() throws Exception {
65126
String logoutUrl = "http://logoutUrl";

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
<cs.testng.version>7.1.0</cs.testng.version>
119119
<cs.wiremock.version>2.27.2</cs.wiremock.version>
120120
<cs.xercesImpl.version>2.12.2</cs.xercesImpl.version>
121+
<cs.assertj.version>3.23.1</cs.assertj.version>
121122

122123
<!-- Dependencies versions -->
123124
<cs.amqp-client.version>5.10.0</cs.amqp-client.version>

0 commit comments

Comments
 (0)