/* * 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.logging.Logger; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.authentication.AuthenticationFlow; import org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory; import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel; import org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory; import org.keycloak.common.util.Encode; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AssertAdminEvents; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; import javax.ws.rs.core.Response; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; 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.ISSUERDN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN_CN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_CN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL; /** * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a> * @version $Revision: 1 $ * @since 10/28/2016 */ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKeycloakTest { public static final String EMPTY_CRL_PATH = "empty.crl"; public static final String CLIENT_CRL_PATH = "intermediate-ca.crl"; protected final Logger log = Logger.getLogger(this.getClass()); static final String REQUIRED = "REQUIRED"; static final String OPTIONAL = "OPTIONAL"; static final String DISABLED = "DISABLED"; static final String ALTERNATIVE = "ALTERNATIVE"; // TODO move to a base class public static final String REALM_NAME = "test"; protected String userId; protected String userId2; protected AuthenticationManagementResource authMgmtResource; protected AuthenticationExecutionInfoRepresentation browserExecution; protected AuthenticationExecutionInfoRepresentation directGrantExecution; @Rule public AssertEvents events = new AssertEvents(this); @Rule public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this); protected boolean isImportAfterEachMethod() { return true; } @Before public void configureFlows() { authMgmtResource = adminClient.realms().realm(REALM_NAME).flows(); AuthenticationFlowRepresentation browserFlow = copyBrowserFlow(); Assert.assertNotNull(browserFlow); AuthenticationFlowRepresentation directGrantFlow = createDirectGrantFlow(); Assert.assertNotNull(directGrantFlow); setBrowserFlow(browserFlow); Assert.assertEquals(testRealm().toRepresentation().getBrowserFlow(), browserFlow.getAlias()); setDirectGrantFlow(directGrantFlow); Assert.assertEquals(testRealm().toRepresentation().getDirectGrantFlow(), directGrantFlow.getAlias()); Assert.assertEquals(0, directGrantFlow.getAuthenticationExecutions().size()); // Add X509 cert authenticator to the direct grant flow directGrantExecution = addAssertExecution(directGrantFlow, ValidateX509CertificateUsernameFactory.PROVIDER_ID, REQUIRED); Assert.assertNotNull(directGrantExecution); directGrantFlow = authMgmtResource.getFlow(directGrantFlow.getId()); Assert.assertNotNull(directGrantFlow.getAuthenticationExecutions()); Assert.assertEquals(1, directGrantFlow.getAuthenticationExecutions().size()); // Add X509 authenticator to the browser flow browserExecution = addAssertExecution(browserFlow, X509ClientCertificateAuthenticatorFactory.PROVIDER_ID, ALTERNATIVE); Assert.assertNotNull(browserExecution); // Raise the priority of the authenticator to position it right before // the Username/password authentication // TODO find a better, more explicit way to specify the position // of authenticator within the flow relative to other authenticators authMgmtResource.raisePriority(browserExecution.getId()); // TODO raising the priority didn't generate the event? //assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRaiseExecutionPath(exec.getId())); UserRepresentation user = findUser("test-user@localhost"); userId = user.getId(); user.singleAttribute("x509_certificate_identity","-"); updateUser(user); } private AuthenticationExecutionInfoRepresentation addAssertExecution(AuthenticationFlowRepresentation flow, String providerId, String requirement) { AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); rep.setPriority(10); rep.setAuthenticator(providerId); rep.setRequirement(requirement); rep.setParentFlow(flow.getId()); Response response = authMgmtResource.addExecution(rep); // TODO the following statement asserts, the actual value is null? //assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authMgmtBasePath() + "/executions"), rep); try { Assert.assertEquals("added execution", 201, response.getStatus()); } finally { response.close(); } List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions(flow.getAlias()); return findExecution(providerId, executionReps); } AuthenticationExecutionInfoRepresentation findExecution(String providerId, List<AuthenticationExecutionInfoRepresentation> reps) { for (AuthenticationExecutionInfoRepresentation exec : reps) { if (providerId.equals(exec.getProviderId())) { return exec; } } return null; } @Override public void configureTestRealm(RealmRepresentation testRealm) { ClientRepresentation app = ClientBuilder.create() .id(KeycloakModelUtils.generateId()) .clientId("resource-owner") .directAccessGrants() .secret("secret") .build(); UserRepresentation user = UserBuilder.create() .id(KeycloakModelUtils.generateId()) .username("Keycloak") .email("localhost@localhost") .enabled(true) .password("password") .build(); userId2 = user.getId(); ClientRepresentation client = findTestApp(testRealm); URI baseUri = URI.create(client.getRedirectUris().get(0)); URI redir = URI.create("https://localhost:" + System.getProperty("app.server.https.port", "8543") + baseUri.getRawPath()); client.getRedirectUris().add(redir.toString()); testRealm.setBruteForceProtected(true); testRealm.setFailureFactor(2); RealmBuilder.edit(testRealm) .user(user) .client(app); } AuthenticationFlowRepresentation createFlow(AuthenticationFlowRepresentation flowRep) { Response response = authMgmtResource.createFlow(flowRep); try { org.keycloak.testsuite.Assert.assertEquals(201, response.getStatus()); } finally { response.close(); } assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authFlowsPath()), flowRep, ResourceType.AUTH_FLOW); for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) { if (flow.getAlias().equalsIgnoreCase(flowRep.getAlias())) { return flow; } } return null; } AuthenticationFlowRepresentation copyFlow(String existingFlow, String newFlow) { // copy that should succeed HashMap<String, String> params = new HashMap<>(); params.put("newName", newFlow); Response response = authMgmtResource.copy(existingFlow, params); assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, Encode.decode(AdminEventPaths.authCopyFlowPath(existingFlow)), params, ResourceType.AUTH_FLOW); try { Assert.assertEquals("Copy flow", 201, response.getStatus()); } finally { response.close(); } for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) { if (flow.getAlias().equalsIgnoreCase(newFlow)) { return flow; } } return null; } AuthenticationFlowRepresentation createDirectGrantFlow() { AuthenticationFlowRepresentation newFlow = newFlow("Copy-of-direct-grant", "desc", AuthenticationFlow.BASIC_FLOW, true, false); return createFlow(newFlow); } AuthenticationFlowRepresentation newFlow(String alias, String description, String providerId, boolean topLevel, boolean builtIn) { AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation(); flow.setAlias(alias); flow.setDescription(description); flow.setProviderId(providerId); flow.setTopLevel(topLevel); flow.setBuiltIn(builtIn); return flow; } AuthenticationFlowRepresentation copyBrowserFlow() { RealmRepresentation realm = testRealm().toRepresentation(); return copyFlow(realm.getBrowserFlow(), "Copy-of-browser"); } void setBrowserFlow(AuthenticationFlowRepresentation flow) { RealmRepresentation realm = testRealm().toRepresentation(); realm.setBrowserFlow(flow.getAlias()); testRealm().update(realm); } void setDirectGrantFlow(AuthenticationFlowRepresentation flow) { RealmRepresentation realm = testRealm().toRepresentation(); realm.setDirectGrantFlow(flow.getAlias()); testRealm().update(realm); } static AuthenticatorConfigRepresentation newConfig(String alias, Map<String,String> params) { AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation(); config.setAlias(alias); config.setConfig(params); return config; } protected String createConfig(String executionId, AuthenticatorConfigRepresentation cfg) { Response resp = authMgmtResource.newExecutionConfig(executionId, cfg); try { Assert.assertEquals(201, resp.getStatus()); } finally { resp.close(); } return ApiUtil.getCreatedId(resp); } protected static X509AuthenticatorConfigModel createLoginSubjectEmail2UsernameOrEmailConfig() { return new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN_EMAIL) .setUserIdentityMapperType(USERNAME_EMAIL); } protected static X509AuthenticatorConfigModel createLoginSubjectCN2UsernameOrEmailConfig() { return new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(SUBJECTDN_CN) .setUserIdentityMapperType(USERNAME_EMAIL); } protected static X509AuthenticatorConfigModel createLoginIssuerCNToUsernameOrEmailConfig() { return new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(ISSUERDN_CN) .setUserIdentityMapperType(USERNAME_EMAIL); } protected static X509AuthenticatorConfigModel createLoginIssuerDN_OU2CustomAttributeConfig() { return new X509AuthenticatorConfigModel() .setConfirmationPageAllowed(true) .setMappingSourceType(ISSUERDN) .setRegularExpression("O=(.*?)(?:,|$)") .setUserIdentityMapperType(USER_ATTRIBUTE) .setCustomAttributeName("x509_certificate_identity"); } protected void setUserEnabled(String userName, boolean enabled) { UserRepresentation user = findUser(userName); Assert.assertNotNull(user); user.setEnabled(enabled); updateUser(user); } }