/* * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.keycloak.testsuite.x509; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel; import org.keycloak.events.Details; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage; import javax.ws.rs.core.Response; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USER_ATTRIBUTE; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL; /** * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a> * @version $Revision: 1 $ * @date 8/12/2016 */ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest { @Page protected AppPage appPage; @Page protected X509IdentityConfirmationPage loginConfirmationPage; @Page protected LoginPage loginPage; private void login(X509AuthenticatorConfigModel config, String userId, String username, String attemptedUsername) { AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); loginConfirmationPage.open(); Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost")); Assert.assertEquals(username, loginConfirmationPage.getUsernameText()); Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted")); loginConfirmationPage.confirm(); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, attemptedUsername) .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginAsUserFromCertSubjectEmail() throws Exception { // Login using an e-mail extracted from certificate's subject DN login(createLoginSubjectEmail2UsernameOrEmailConfig(), userId, "test-user@localhost", "test-user@localhost"); } @Test public void loginIgnoreX509IdentityContinueToFormLogin() throws Exception { // Set the X509 authenticator configuration AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); loginConfirmationPage.open(); Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost")); Assert.assertEquals("test-user@localhost", loginConfirmationPage.getUsernameText()); Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted")); loginConfirmationPage.ignore(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginAsUserFromCertSubjectCN() { // Login using a CN extracted from certificate's subject DN login(createLoginSubjectCN2UsernameOrEmailConfig(), userId, "test-user@localhost", "test-user@localhost"); } @Test public void loginAsUserFromCertIssuerCN() { login(createLoginIssuerCNToUsernameOrEmailConfig(), userId2, "keycloak", "Keycloak"); } @Test public void loginAsUserFromCertIssuerCNMappedToUserAttribute() { UserRepresentation user = testRealm().users().get(userId2).toRepresentation(); Assert.assertNotNull(user); user.singleAttribute("x509_certificate_identity", "Red Hat"); this.updateUser(user); events.clear(); login(createLoginIssuerDN_OU2CustomAttributeConfig(), userId2, "keycloak", "Red Hat"); } @Test public void loginDuplicateUsersNotAllowed() { AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginIssuerDN_OU2CustomAttributeConfig().getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); // Set up the users so that the identity extracted from X509 client cert // matches more than a single user to trigger DuplicateModelException. UserRepresentation user = testRealm().users().get(userId2).toRepresentation(); Assert.assertNotNull(user); user.singleAttribute("x509_certificate_identity", "Red Hat"); this.updateUser(user); user = testRealm().users().get(userId).toRepresentation(); Assert.assertNotNull(user); user.singleAttribute("x509_certificate_identity", "Red Hat"); this.updateUser(user); events.clear(); loginPage.open(); Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed.")); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginAttemptedNoConfig() { loginConfirmationPage.open(); loginPage.assertCurrent(); Assert.assertThat(loginPage.getInfoMessage(), containsString("X509 client authentication has not been configured yet")); // Continue with form based login loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginWithX509CertCustomAttributeUserNotFound() { X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN) .setRegularExpression("O=(.*?)(?:,|$)") .setCustomAttributeName("x509_certificate_identity") .setUserIdentityMapperType(USER_ATTRIBUTE); AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); loginConfirmationPage.open(); loginPage.assertCurrent(); // Verify there is an error message Assert.assertNotNull(loginPage.getError()); Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed.")); events.expectLogin() .user((String) null) .session((String) null) .error("user_not_found") .detail(Details.USERNAME, "Red Hat") .removeDetail(Details.CONSENT) .removeDetail(Details.REDIRECT_URI) .assertEvent(); // Continue with form based login loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginWithX509CertCustomAttributeSuccess() { X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN) .setRegularExpression("O=(.*?)(?:,|$)") .setCustomAttributeName("x509_certificate_identity") .setUserIdentityMapperType(USER_ATTRIBUTE); AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); // Update the attribute used to match the user identity to that // extracted from the client certificate UserRepresentation user = findUser("test-user@localhost"); Assert.assertNotNull(user); user.singleAttribute("x509_certificate_identity", "Red Hat"); this.updateUser(user); events.clear(); loginConfirmationPage.open(); Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost")); Assert.assertEquals("test-user@localhost", loginConfirmationPage.getUsernameText()); Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted")); loginConfirmationPage.confirm(); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); } @Test public void loginWithX509CertBadUserOrNotFound() { AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); // Delete user UserRepresentation user = findUser("test-user@localhost"); Assert.assertNotNull(user); Response response = testRealm().users().delete(userId); assertEquals(204, response.getStatus()); response.close(); // TODO causes the test to fail //assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.userResourcePath(userId)); loginConfirmationPage.open(); loginPage.assertCurrent(); // Verify there is an error message Assert.assertNotNull(loginPage.getError()); Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed.")); events.expectLogin() .user((String) null) .session((String) null) .error("user_not_found") .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.CONSENT) .removeDetail(Details.REDIRECT_URI) .assertEvent(); // Continue with form based login loginPage.login("test-user@localhost", "password"); loginPage.assertCurrent(); Assert.assertEquals("test-user@localhost", loginPage.getUsername()); Assert.assertEquals("", loginPage.getPassword()); Assert.assertEquals("Invalid username or password.", loginPage.getError()); } @Test public void loginValidCertificateDisabledUser() { setUserEnabled("test-user@localhost", false); try { AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); loginConfirmationPage.open(); loginPage.assertCurrent(); Assert.assertNotNull(loginPage.getError()); Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed.\nUser is disabled")); events.expectLogin() .user(userId) .session((String) null) .error("user_disabled") .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.CONSENT) .removeDetail(Details.REDIRECT_URI) .assertEvent(); loginPage.login("test-user@localhost", "password"); loginPage.assertCurrent(); // KEYCLOAK-1741 - assert form field values kept Assert.assertEquals("test-user@localhost", loginPage.getUsername()); Assert.assertEquals("", loginPage.getPassword()); // KEYCLOAK-2024 Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError()); events.expectLogin() .user(userId) .session((String) null) .error("user_disabled") .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.CONSENT) .removeDetail(Details.REDIRECT_URI) .assertEvent(); } finally { setUserEnabled("test-user@localhost", true); } } @Test public void loginWithX509WithEmptyRevocationList() { X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setCRLEnabled(true) .setCRLRelativePath(EMPTY_CRL_PATH) .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN_EMAIL) .setUserIdentityMapperType(USERNAME_EMAIL); login(config, userId, "test-user@localhost", "test-user@localhost"); } @Test public void loginCertificateRevoked() { X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setCRLEnabled(true) .setCRLRelativePath(CLIENT_CRL_PATH) .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN_EMAIL) .setUserIdentityMapperType(USERNAME_EMAIL); AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); loginConfirmationPage.open(); loginPage.assertCurrent(); // Verify there is an error message Assert.assertNotNull(loginPage.getError()); Assert.assertThat(loginPage.getError(), containsString("Certificate validation's failed.\nCertificate has been revoked, certificate's subject:")); // Continue with form based login loginPage.login("test-user@localhost", "password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } @Test public void loginNoIdentityConfirmationPage() { X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(false) .setMappingSourceType(SUBJECTDN_EMAIL) .setUserIdentityMapperType(USERNAME_EMAIL); AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig()); String cfgId = createConfig(browserExecution.getId(), cfg); Assert.assertNotNull(cfgId); oauth.openLoginForm(); // X509 authenticator extracts the user identity, maps it to an existing // user and automatically logs the user in without prompting to confirm // the identity. Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); events.expectLogin() .user(userId) .detail(Details.USERNAME, "test-user@localhost") .removeDetail(Details.REDIRECT_URI) .assertEvent(); } }