diff --git a/docbook/reference/en/en-US/modules/adapter-config.xml b/docbook/reference/en/en-US/modules/adapter-config.xml index 979a9d550f..448f6ca2fb 100755 --- a/docbook/reference/en/en-US/modules/adapter-config.xml +++ b/docbook/reference/en/en-US/modules/adapter-config.xml @@ -112,6 +112,15 @@ + + public-client + + + If set to true, the adapter will not send credentials for the client to Keycloak. + The default value is false. + + + enable-cors @@ -140,7 +149,19 @@ If CORS is enabled, this sets the value of the Access-Control-Allow-Methods - header. This should be a JSON list of strings. + header. This should be a comma-separated string. + This is OPTIONAL. If not set, this header is not returned in CORS + responses. + + + + + cors-allowed-headers + + + If CORS is enabled, this sets the value of the + Access-Control-Allow-Headers + header. This should be a comma-separated string. This is OPTIONAL. If not set, this header is not returned in CORS responses. diff --git a/integration/adapter-core/pom.xml b/integration/adapter-core/pom.xml index 4fddde83f2..0190268ed7 100755 --- a/integration/adapter-core/pom.xml +++ b/integration/adapter-core/pom.xml @@ -14,6 +14,11 @@ + + org.bouncycastle + bcprov-jdk16 + provided + org.jboss.logging jboss-logging @@ -51,6 +56,11 @@ junit test + + commons-io + commons-io + test + org.apache.httpcomponents httpclient diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 1f00b91c07..9c2129ccf1 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -59,6 +59,8 @@ public class KeycloakDeploymentBuilder { deployment.setPublicClient(adapterConfig.isPublicClient()); deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings()); + deployment.setExposeToken(adapterConfig.isExposeToken()); + if (adapterConfig.isCors()) { deployment.setCors(true); deployment.setCorsMaxAge(adapterConfig.getCorsMaxAge()); diff --git a/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java new file mode 100644 index 0000000000..71c03bd581 --- /dev/null +++ b/integration/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -0,0 +1,71 @@ +package org.keycloak.adapters; + +import org.apache.commons.io.FileUtils; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.bouncycastle.util.encoders.Base64; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.keycloak.enums.SslRequired; +import org.keycloak.enums.TokenStore; +import org.keycloak.util.PemUtils; + +import javax.net.ssl.SSLSocketFactory; +import java.io.File; +import java.io.IOException; +import java.security.PublicKey; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Stian Thorgersen + */ +public class KeycloakDeploymentBuilderTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void before() throws IOException { + File dir = folder.newFolder(); + FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("/cacerts.jks"), new File(dir, "cacerts.jks")); + FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("/keystore.jks"), new File(dir, "keystore.jks")); + System.setProperty("testResources", dir.getAbsolutePath()); + } + + @After + public void after() { + System.getProperties().remove("testResources"); + } + + @Test + public void load() throws Exception { + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak.json")); + assertEquals("demo", deployment.getRealm()); + assertEquals("customer-portal", deployment.getResourceName()); + assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), deployment.getRealmKey()); + assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/login", deployment.getAuthUrl().build().toString()); + assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired()); + assertTrue(deployment.isUseResourceRoleMappings()); + assertTrue(deployment.isCors()); + assertEquals(1000, deployment.getCorsMaxAge()); + assertEquals("POST, PUT, DELETE, GET", deployment.getCorsAllowedMethods()); + assertEquals("X-Custom, X-Custom2", deployment.getCorsAllowedHeaders()); + assertTrue(deployment.isBearerOnly()); + assertTrue(deployment.isPublicClient()); + assertTrue(deployment.isEnableBasicAuth()); + assertTrue(deployment.isExposeToken()); + assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret")); + assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal()); + assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/refresh", deployment.getRefreshUrl()); + assertTrue(deployment.isAlwaysRefreshToken()); + assertTrue(deployment.isRegisterNodeAtStartup()); + assertEquals(1000, deployment.getRegisterNodePeriod()); + assertEquals(TokenStore.COOKIE, deployment.getTokenStore()); + assertEquals("email", deployment.getPrincipalAttribute()); + } + +} diff --git a/integration/adapter-core/src/test/resources/cacerts.jks b/integration/adapter-core/src/test/resources/cacerts.jks new file mode 100644 index 0000000000..f8ae5a39a0 Binary files /dev/null and b/integration/adapter-core/src/test/resources/cacerts.jks differ diff --git a/integration/adapter-core/src/test/resources/keycloak.json b/integration/adapter-core/src/test/resources/keycloak.json new file mode 100644 index 0000000000..2eb6e1f109 --- /dev/null +++ b/integration/adapter-core/src/test/resources/keycloak.json @@ -0,0 +1,33 @@ +{ + "realm": "demo", + "resource": "customer-portal", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "https://localhost:8443/auth", + "ssl-required": "external", + "use-resource-role-mappings": true, + "enable-cors": true, + "cors-max-age": 1000, + "cors-allowed-methods": "POST, PUT, DELETE, GET", + "cors-allowed-headers": "X-Custom, X-Custom2", + "bearer-only": true, + "public-client": true, + "enable-basic-auth": true, + "expose-token": true, + "credentials": { + "secret": "234234-234234-234234" + }, + "connection-pool-size": 20, + "disable-trust-manager": true, + "allow-any-hostname": true, + "truststore": "${testResources}/cacerts.jks", + "truststore-password": "changeit", + "client-keystore": "${testResources}/keystore.jks", + "client-keystore-password": "changeit", + "client-key-password": "password", + "auth-server-url-for-backend-requests": "https://backend:8443/auth", + "always-refresh-token": true, + "register-node-at-startup": true, + "register-node-period": 1000, + "token-store": "cookie", + "principal-attribute": "email" +} \ No newline at end of file diff --git a/integration/adapter-core/src/test/resources/keystore.jks b/integration/adapter-core/src/test/resources/keystore.jks new file mode 100644 index 0000000000..0c4e3a189c Binary files /dev/null and b/integration/adapter-core/src/test/resources/keystore.jks differ diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js index 964509fe31..51e564dd27 100755 --- a/integration/js/src/main/resources/keycloak.js +++ b/integration/js/src/main/resources/keycloak.js @@ -51,7 +51,7 @@ var configPromise = loadConfig(config); function processInit() { - var callback = parseCallback(window.location.search); + var callback = parseCallback(window.location.href); if (callback) { window.history.replaceState({}, null, callback.newUrl); @@ -534,6 +534,7 @@ break; default: oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + p[0] + '=' + p[1]; + break; } } @@ -688,8 +689,9 @@ } else if (kc.redirectUri) { return kc.redirectUri; } else { - var redirectUri = location.href.substring(0, location.href.indexOf('#')); + var redirectUri = location.href; if (location.hash) { + redirectUri = redirectUri.substring(0, location.href.indexOf('#')); redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); } return redirectUri; diff --git a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java index 21b2e6cb56..b0e6caec54 100755 --- a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java +++ b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaUserSessionManagement.java @@ -62,8 +62,7 @@ public class CatalinaUserSessionManagement implements SessionListener { public void sessionEvent(SessionEvent event) { // We only care about session destroyed events - if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) - && (!Session.SESSION_PASSIVATED_EVENT.equals(event.getType()))) + if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())) return; // Look up the single session id associated with this session (if any) diff --git a/pom.xml b/pom.xml index 97b5f6d460..c99e0b2cef 100755 --- a/pom.xml +++ b/pom.xml @@ -303,6 +303,12 @@ 4.11 test + + commons-io + commons-io + test + 2.4 + org.hamcrest hamcrest-all diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index a9f0233a53..4f462e0a61 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -22,7 +22,6 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.email.EmailException; @@ -45,7 +44,6 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.LoginProtocol; -import org.keycloak.protocol.oidc.OpenIDConnect; import org.keycloak.protocol.oidc.OpenIDConnectService; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.PasswordToken; @@ -63,7 +61,6 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -73,7 +70,6 @@ import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.util.LinkedList; import java.util.List; -import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -605,16 +601,28 @@ public class LoginActionsService { user.setLastName(formData.getFirst("lastName")); String email = formData.getFirst("email"); + String oldEmail = user.getEmail(); boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; - user.setEmail(email); + if (emailChanged) { + UserModel userByEmail = session.users().getUserByEmail(email, realm); + + // check for duplicated email + if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { + return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(Messages.EMAIL_EXISTS) + .setClientSessionCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PROFILE); + } + + user.setEmail(email); + user.setEmailVerified(false); + } user.removeRequiredAction(RequiredAction.UPDATE_PROFILE); - event.clone().event(EventType.UPDATE_PROFILE).success(); + if (emailChanged) { - user.setEmailVerified(false); event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index f66c88e72b..d832f967ee 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -147,4 +147,21 @@ public class RequiredActionUpdateProfileTest { events.assertEmpty(); } + @Test + public void updateProfileDuplicatedEmail() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "keycloak-user@localhost"); + + updateProfilePage.assertCurrent(); + + Assert.assertEquals("Email already exists", updateProfilePage.getError()); + + events.assertEmpty(); + } + } diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index cc2a6143d3..81a442d7a8 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -29,6 +29,20 @@ "test-app": [ "customer-user" ], "account": [ "view-profile", "manage-account" ] } + }, + { + "username" : "keycloak-user@localhost", + "enabled": true, + "email" : "keycloak-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "applicationRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } } ], "oauthClients" : [ diff --git a/testsuite/performance-web/pom.xml b/testsuite/performance-web/pom.xml index 63d0d9bfd7..c5f0b3707e 100755 --- a/testsuite/performance-web/pom.xml +++ b/testsuite/performance-web/pom.xml @@ -89,6 +89,11 @@ resteasy-undertow ${resteasy.version.latest} + + commons-io + commons-io + provided + org.apache.jmeter