keycloak-aplcache

Details

diff --git a/docbook/reference/en/en-US/modules/adapter-config.xml b/docbook/reference/en/en-US/modules/adapter-config.xml
index 979a9d5..448f6ca 100755
--- a/docbook/reference/en/en-US/modules/adapter-config.xml
+++ b/docbook/reference/en/en-US/modules/adapter-config.xml
@@ -113,6 +113,15 @@
                 </listitem>
             </varlistentry>
             <varlistentry>
+                <term>public-client</term>
+                <listitem>
+                    <para>
+                        If set to true, the adapter will not send credentials for the client to Keycloak.
+                        The default value is <emphasis>false</emphasis>.
+                    </para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
                 <term>enable-cors</term>
                 <listitem>
                     <para>
@@ -140,7 +149,19 @@
                     <para>
                         If CORS is enabled, this sets the value of the
                         <literal>Access-Control-Allow-Methods</literal>
-                        header. This should be a JSON list of strings.
+                        header. This should be a comma-separated string.
+                        This is <emphasis>OPTIONAL</emphasis>. If not set, this header is not returned in CORS
+                        responses.
+                    </para>
+                </listitem>
+            </varlistentry>
+            <varlistentry>
+                <term>cors-allowed-headers</term>
+                <listitem>
+                    <para>
+                        If CORS is enabled, this sets the value of the
+                        <literal>Access-Control-Allow-Headers</literal>
+                        header. This should be a comma-separated string.
                         This is <emphasis>OPTIONAL</emphasis>. If not set, this header is not returned in CORS
                         responses.
                     </para>
diff --git a/integration/adapter-core/pom.xml b/integration/adapter-core/pom.xml
index 4fddde8..0190268 100755
--- a/integration/adapter-core/pom.xml
+++ b/integration/adapter-core/pom.xml
@@ -15,6 +15,11 @@
 
     <dependencies>
         <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk16</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.jboss.logging</groupId>
             <artifactId>jboss-logging</artifactId>
             <version>${jboss.logging.version}</version>
@@ -52,6 +57,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
             <version>${keycloak.apache.httpcomponents.version}</version>
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 1f00b91..9c2129c 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 0000000..71c03bd
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..f8ae5a3
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 0000000..2eb6e1f
--- /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 0000000..0c4e3a1
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 964509f..51e564d 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 21b2e6c..b0e6cae 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)

pom.xml 6(+6 -0)

diff --git a/pom.xml b/pom.xml
index 97b5f6d..c99e0b2 100755
--- a/pom.xml
+++ b/pom.xml
@@ -304,6 +304,12 @@
                 <scope>test</scope>
             </dependency>
             <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <scope>test</scope>
+                <version>2.4</version>
+            </dependency>
+            <dependency>
                 <groupId>org.hamcrest</groupId>
                 <artifactId>hamcrest-all</artifactId>
                 <version>1.3</version>
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 a9f0233..4f462e0 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);
 
-        user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
+            // 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 f66c88e..d832f96 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 cc2a614..81a442d 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 63d0d9b..c5f0b37 100755
--- a/testsuite/performance-web/pom.xml
+++ b/testsuite/performance-web/pom.xml
@@ -89,6 +89,11 @@
             <artifactId>resteasy-undertow</artifactId>
             <version>${resteasy.version.latest}</version>
         </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.jmeter</groupId>