Skip to content

Commit 2cca668

Browse files
author
gabrascher
committed
Redirect saml2 failed login message to a configurable URL
When a user fails to authenticate with SAML2, it returns an error page showing the content of the attached image. To make it more user-friendly and customizable, one could configure a desirable URL to redirect when such authentication failure happens. This ticket proposes a global settings variable (saml2.failed.login.redirect.url). If null, the SAML2 authentication flow does not change from the current; however, if the user configures an URL then ACS redirects to that URL.
1 parent c670691 commit 2cca668

2 files changed

Lines changed: 168 additions & 25 deletions

File tree

plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616
// under the License.
1717
package org.apache.cloudstack.api.command;
1818

19-
import com.cloud.api.response.ApiResponseSerializer;
20-
import com.cloud.exception.CloudAuthenticationException;
21-
import com.cloud.user.Account;
22-
import com.cloud.user.DomainManager;
23-
import com.cloud.user.UserAccount;
24-
import com.cloud.user.UserAccountVO;
25-
import com.cloud.user.dao.UserAccountDao;
26-
import com.cloud.utils.db.EntityManager;
19+
import java.io.IOException;
20+
import java.net.InetAddress;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import javax.inject.Inject;
25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.HttpServletResponse;
27+
import javax.servlet.http.HttpSession;
28+
import javax.xml.parsers.ParserConfigurationException;
29+
import javax.xml.stream.FactoryConfigurationError;
30+
2731
import org.apache.cloudstack.api.APICommand;
2832
import org.apache.cloudstack.api.ApiConstants;
2933
import org.apache.cloudstack.api.ApiErrorCode;
@@ -35,11 +39,14 @@
3539
import org.apache.cloudstack.api.auth.APIAuthenticator;
3640
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
3741
import org.apache.cloudstack.api.response.LoginCmdResponse;
42+
import org.apache.cloudstack.framework.config.ConfigKey;
43+
import org.apache.cloudstack.framework.config.Configurable;
3844
import org.apache.cloudstack.saml.SAML2AuthManager;
3945
import org.apache.cloudstack.saml.SAMLPluginConstants;
4046
import org.apache.cloudstack.saml.SAMLProviderMetadata;
4147
import org.apache.cloudstack.saml.SAMLTokenVO;
4248
import org.apache.cloudstack.saml.SAMLUtils;
49+
import org.apache.commons.lang.StringUtils;
4350
import org.apache.log4j.Logger;
4451
import org.opensaml.DefaultBootstrap;
4552
import org.opensaml.saml2.core.Assertion;
@@ -62,19 +69,17 @@
6269
import org.opensaml.xml.validation.ValidationException;
6370
import org.xml.sax.SAXException;
6471

65-
import javax.inject.Inject;
66-
import javax.servlet.http.HttpServletRequest;
67-
import javax.servlet.http.HttpServletResponse;
68-
import javax.servlet.http.HttpSession;
69-
import javax.xml.parsers.ParserConfigurationException;
70-
import javax.xml.stream.FactoryConfigurationError;
71-
import java.io.IOException;
72-
import java.net.InetAddress;
73-
import java.util.List;
74-
import java.util.Map;
72+
import com.cloud.api.response.ApiResponseSerializer;
73+
import com.cloud.exception.CloudAuthenticationException;
74+
import com.cloud.user.Account;
75+
import com.cloud.user.DomainManager;
76+
import com.cloud.user.UserAccount;
77+
import com.cloud.user.UserAccountVO;
78+
import com.cloud.user.dao.UserAccountDao;
79+
import com.cloud.utils.db.EntityManager;
7580

7681
@APICommand(name = "samlSso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
77-
public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
82+
public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator, Configurable {
7883
public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName());
7984
private static final String s_name = "loginresponse";
8085

@@ -93,6 +98,9 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
9398
@Inject
9499
private UserAccountDao _userAccountDao;
95100

101+
private static final ConfigKey<String> Saml2FailedLoginRedirectUrl = new ConfigKey<String>("Advanced", String.class, "saml2.failed.login.redirect.url", "",
102+
"The URL to redirect the SAML2 login failed message (the default vaulue is empty).", true);
103+
96104
SAML2AuthManager _samlAuthManager;
97105

98106
/////////////////////////////////////////////////////
@@ -170,7 +178,7 @@ public String authenticate(final String command, final Map<String, Object[]> par
170178
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
171179
"IdP ID (" + idpId + ") has no Single Sign On URL defined please contact "
172180
+ idpMetadata.getContactPersonName() + " <" + idpMetadata.getContactPersonEmail() + ">, cannot proceed.",
173-
params, responseType));
181+
params, responseType));
174182
}
175183
String authnId = SAMLUtils.generateSecureRandomId();
176184
_samlAuthManager.saveToken(authnId, domainPath, idpMetadata.getEntityId());
@@ -302,11 +310,7 @@ public String authenticate(final String command, final Map<String, Object[]> par
302310
}
303311
}
304312

305-
if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
306-
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
307-
"Your authenticated user is not authorized for SAML Single Sign-On, please contact your administrator",
308-
params, responseType));
309-
}
313+
whenFailToAuthenticateThrowExceptionOrRedirectToUrl(params, responseType, resp, issuer, userAccount);
310314

311315
try {
312316
if (_apiServer.verifyUser(userAccount.getId())) {
@@ -329,6 +333,24 @@ public String authenticate(final String command, final Map<String, Object[]> par
329333
params, responseType));
330334
}
331335

336+
/**
337+
* If it fails to authenticate the user, the method gets the value from configuration
338+
* Saml2FailedLoginRedirectUrl; if the user configured an error URL then it redirects to that
339+
* URL, otherwise it throws the ServerApiException
340+
*/
341+
protected void whenFailToAuthenticateThrowExceptionOrRedirectToUrl(final Map<String, Object[]> params, final String responseType, final HttpServletResponse resp, Issuer issuer,
342+
UserAccount userAccount) throws IOException {
343+
if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
344+
String saml2FailedLoginRedirectUrl = Saml2FailedLoginRedirectUrl.value(); //TODO configDao.getValue(Config.Saml2FailedLoginRedirectUrl.toString());
345+
if (StringUtils.isBlank(saml2FailedLoginRedirectUrl)) {
346+
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
347+
"Your authenticated user is not authorized for SAML Single Sign-On, please contact your administrator", params, responseType));
348+
} else {
349+
resp.sendRedirect(saml2FailedLoginRedirectUrl);
350+
}
351+
}
352+
}
353+
332354
@Override
333355
public APIAuthenticationType getAPIType() {
334356
return APIAuthenticationType.LOGIN_API;
@@ -345,4 +367,14 @@ public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
345367
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
346368
}
347369
}
370+
371+
@Override
372+
public String getConfigComponentName() {
373+
return SAML2LoginAPIAuthenticatorCmd.class.getSimpleName();
374+
}
375+
376+
@Override
377+
public ConfigKey<?>[] getConfigKeys() {
378+
return new ConfigKey<?>[] { Saml2FailedLoginRedirectUrl };
379+
}
348380
}

plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import static org.junit.Assert.assertFalse;
2323

24+
import java.io.IOException;
2425
import java.lang.reflect.Field;
2526
import java.net.InetAddress;
2627
import java.security.KeyPair;
@@ -36,6 +37,7 @@
3637
import org.apache.cloudstack.api.BaseCmd;
3738
import org.apache.cloudstack.api.ServerApiException;
3839
import org.apache.cloudstack.api.auth.APIAuthenticationType;
40+
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
3941
import org.apache.cloudstack.saml.SAML2AuthManager;
4042
import org.apache.cloudstack.saml.SAMLPluginConstants;
4143
import org.apache.cloudstack.saml.SAMLProviderMetadata;
@@ -45,8 +47,10 @@
4547
import org.junit.Assert;
4648
import org.junit.Test;
4749
import org.junit.runner.RunWith;
50+
import org.mockito.InjectMocks;
4851
import org.mockito.Mock;
4952
import org.mockito.Mockito;
53+
import org.mockito.Spy;
5054
import org.mockito.runners.MockitoJUnitRunner;
5155
import org.opensaml.common.SAMLVersion;
5256
import org.opensaml.saml2.core.Assertion;
@@ -106,6 +110,13 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
106110
@Mock
107111
HttpServletRequest req;
108112

113+
@Mock
114+
private ConfigurationDao configDao;
115+
116+
@Spy
117+
@InjectMocks
118+
private SAML2LoginAPIAuthenticatorCmd cmdSpy;
119+
109120
private Response buildMockResponse() throws Exception {
110121
Response samlMessage = new ResponseBuilder().buildObject();
111122
samlMessage.setID("foo");
@@ -205,4 +216,104 @@ public void testAuthenticate() throws Exception {
205216
public void testGetAPIType() {
206217
Assert.assertTrue(new SAML2LoginAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
207218
}
219+
220+
@Test
221+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlBlank() throws IOException {
222+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", " ", false);
223+
224+
boolean expectServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
225+
226+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, expectServerApiException, 0, 1);
227+
}
228+
229+
@Test
230+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlNull() throws IOException {
231+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", null, false);
232+
233+
boolean expectServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
234+
235+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, expectServerApiException, 0, 1);
236+
}
237+
238+
@Test
239+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlEmpty() throws IOException {
240+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "", false);
241+
242+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
243+
244+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, hasThrownServerApiException, 0, 1);
245+
}
246+
247+
@Test
248+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectExternalEntityNullAndUrlNotConfigured() throws IOException {
249+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, " ", false);
250+
251+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
252+
253+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, hasThrownServerApiException, 0, 1);
254+
}
255+
256+
@Test
257+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectExternalEntityNullAndUrlConfigured() throws IOException {
258+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, "some.url", true);
259+
260+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
261+
262+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
263+
}
264+
265+
@Test
266+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlConfigured() throws IOException {
267+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", false);
268+
269+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
270+
271+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
272+
}
273+
274+
@Test
275+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlUserAccountNull() throws IOException {
276+
configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", true);
277+
278+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null);
279+
280+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
281+
}
282+
283+
@Test
284+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlIsUserAuthorized() throws IOException {
285+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", true);
286+
287+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
288+
289+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 0, 0);
290+
}
291+
292+
private UserAccountVO configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(String entity, String configurationDaoGetValue, Boolean isUserAuthorized)
293+
throws IOException {
294+
Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn(configurationDaoGetValue);
295+
Mockito.when(samlAuthManager.isUserAuthorized(Mockito.anyLong(), Mockito.anyString())).thenReturn(isUserAuthorized);
296+
297+
UserAccountVO userAccount = new UserAccountVO();
298+
userAccount.setExternalEntity(entity);
299+
userAccount.setId(0l);
300+
return userAccount;
301+
}
302+
303+
private void verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(boolean expectServerApiException, boolean hasThrownServerApiException, int timesOfSendRedirect,
304+
int timesOfConfigDao) throws IOException {
305+
Mockito.verify(resp, Mockito.times(timesOfSendRedirect)).sendRedirect(Mockito.anyString());
306+
Mockito.verify(configDao, Mockito.times(timesOfConfigDao)).getValue(Mockito.anyString());
307+
Assert.assertEquals(expectServerApiException, hasThrownServerApiException);
308+
}
309+
310+
private boolean runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(UserAccountVO userAccount) throws IOException {
311+
try {
312+
cmdSpy.whenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, "responseType", resp, new IssuerBuilder().buildObject(), userAccount);
313+
} catch (ServerApiException e) {
314+
return true;
315+
}
316+
return false;
317+
}
318+
208319
}

0 commit comments

Comments
 (0)