Skip to content

Commit e9d39c2

Browse files
author
Gabriel Beims Bräscher
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 82fc9f3 commit e9d39c2

2 files changed

Lines changed: 146 additions & 25 deletions

File tree

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

Lines changed: 58 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
/////////////////////////////////////////////////////
@@ -294,19 +302,15 @@ public String authenticate(final String command, final Map<String, Object[]> par
294302
if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
295303
// Log into the first enabled user account
296304
// Users can switch to other allowed accounts later
297-
for (UserAccountVO possibleUserAccount: possibleUserAccounts) {
305+
for (UserAccountVO possibleUserAccount : possibleUserAccounts) {
298306
if (possibleUserAccount.getAccountState().equals(Account.State.enabled.toString())) {
299307
userAccount = possibleUserAccount;
300308
break;
301309
}
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,15 @@ 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+
}
380+
348381
}

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

Lines changed: 88 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;
@@ -45,8 +46,10 @@
4546
import org.junit.Assert;
4647
import org.junit.Test;
4748
import org.junit.runner.RunWith;
49+
import org.mockito.InjectMocks;
4850
import org.mockito.Mock;
4951
import org.mockito.Mockito;
52+
import org.mockito.Spy;
5053
import org.mockito.runners.MockitoJUnitRunner;
5154
import org.opensaml.common.SAMLVersion;
5255
import org.opensaml.saml2.core.Assertion;
@@ -106,6 +109,10 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
106109
@Mock
107110
HttpServletRequest req;
108111

112+
@Spy
113+
@InjectMocks
114+
private SAML2LoginAPIAuthenticatorCmd cmdSpy;
115+
109116
private Response buildMockResponse() throws Exception {
110117
Response samlMessage = new ResponseBuilder().buildObject();
111118
samlMessage.setID("foo");
@@ -205,4 +212,85 @@ public void testAuthenticate() throws Exception {
205212
public void testGetAPIType() {
206213
Assert.assertTrue(new SAML2LoginAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
207214
}
215+
216+
@Test
217+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlBlank() throws IOException {
218+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", " ", false);
219+
boolean expectServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
220+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, expectServerApiException, 0, 1);
221+
}
222+
223+
@Test
224+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlNull() throws IOException {
225+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", null, false);
226+
boolean expectServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
227+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, expectServerApiException, 0, 1);
228+
}
229+
230+
@Test
231+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlEmpty() throws IOException {
232+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "", false);
233+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
234+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, hasThrownServerApiException, 0, 1);
235+
}
236+
237+
@Test
238+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectExternalEntityNullAndUrlNotConfigured() throws IOException {
239+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, " ", false);
240+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
241+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(true, hasThrownServerApiException, 0, 1);
242+
}
243+
244+
@Test
245+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectExternalEntityNullAndUrlConfigured() throws IOException {
246+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, "some.url", true);
247+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
248+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
249+
}
250+
251+
@Test
252+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlConfigured() throws IOException {
253+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", false);
254+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
255+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
256+
}
257+
258+
@Test
259+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlUserAccountNull() throws IOException {
260+
configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", true);
261+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(null);
262+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 1, 1);
263+
}
264+
265+
@Test
266+
public void whenFailToAuthenticateThrowExceptionOrRedirectToUrlTestSaml2FailedLoginRedirectUrlIsUserAuthorized() throws IOException {
267+
UserAccountVO userAccount = configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl("entity", "some.url", true);
268+
boolean hasThrownServerApiException = runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(userAccount);
269+
verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(false, hasThrownServerApiException, 0, 0);
270+
}
271+
272+
private UserAccountVO configureTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(String entity, String configurationDaoGetValue, Boolean isUserAuthorized)
273+
throws IOException {
274+
Mockito.when(samlAuthManager.isUserAuthorized(Mockito.anyLong(), Mockito.anyString())).thenReturn(isUserAuthorized);
275+
UserAccountVO userAccount = new UserAccountVO();
276+
userAccount.setExternalEntity(entity);
277+
userAccount.setId(0l);
278+
return userAccount;
279+
}
280+
281+
private void verifyTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(boolean expectServerApiException, boolean hasThrownServerApiException, int timesOfSendRedirect,
282+
int timesOfConfigDao) throws IOException {
283+
Mockito.verify(resp, Mockito.times(timesOfSendRedirect)).sendRedirect(Mockito.anyString());
284+
Assert.assertEquals(expectServerApiException, hasThrownServerApiException);
285+
}
286+
287+
private boolean runTestWhenFailToAuthenticateThrowExceptionOrRedirectToUrl(UserAccountVO userAccount) throws IOException {
288+
try {
289+
cmdSpy.whenFailToAuthenticateThrowExceptionOrRedirectToUrl(null, "responseType", resp, new IssuerBuilder().buildObject(), userAccount);
290+
} catch (ServerApiException e) {
291+
return true;
292+
}
293+
return false;
294+
}
295+
208296
}

0 commit comments

Comments
 (0)