From 99ce0a36e687105a1c6a082f1a8294b4dafd0a85 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 16 Jun 2014 18:37:33 +0200 Subject: [PATCH 1/8] Refactoring current performance test --- pom.xml | 4 +- testsuite/integration/pom.xml | 4 +- testsuite/performance/README.md | 33 ++++++++------- testsuite/performance/pom.xml | 40 ++++++++++++++++++- .../BaseJMeterPerformanceTest.java | 33 ++++++++++++++- .../performance/ReadUsersWorker.java | 4 +- .../src/test/jmeter/keycloak_perf_test.jmx | 6 +-- .../src/test/jmeter/system.properties | 12 ++++-- 8 files changed, 107 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 66927cb8ce..9d6f31113c 100755 --- a/pom.xml +++ b/pom.xml @@ -360,7 +360,7 @@ org.apache.jmeter ApacheJMeter_java - 2.9 + 2.10 dom4j @@ -513,7 +513,7 @@ com.lazerycode.jmeter jmeter-maven-plugin - 1.8.1 + 1.9.0 com.lazerycode.jmeter diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index a1d7c50c19..052f6b1945 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -2,10 +2,10 @@ - keycloak-parent + keycloak-testsuite-pom org.keycloak 1.0-beta-4-SNAPSHOT - ../../pom.xml + ../pom.xml 4.0.0 diff --git a/testsuite/performance/README.md b/testsuite/performance/README.md index 4e6a842eb6..1969b87d18 100644 --- a/testsuite/performance/README.md +++ b/testsuite/performance/README.md @@ -1,19 +1,16 @@ Configuration of performance test ================================= -- At this moment it's src/test/jmeter/keycloak_perf_test.jmx where you can configure among other things: --- "ThreadGroup.num_threads" -- number of worker threads --- "LoopController.loops" -- Number of loops per each thread. - - src/test/jmeter/system.properties -- System properties including configuration of providers. Allow to specify: +-- Number of worker threads and loops to be used by JMeter performance test -- which model to use -- which test to run -- configuration of individual tests. Properties for each test documented in the file Running performance tests ========================= -cd KEYCLOAK_HOME/testsuite -mvn clean install -DskipTests=true -Pperformance-tests +cd KEYCLOAK_HOME/testsuite/performance +mvn clean verify -DskipTests=true -Pperformance-tests Results: - Log is in: testsuite/performance/target/jmeter/logs/keycloak_perf_test.jmx.log @@ -22,18 +19,26 @@ Results: Example for running test ======================== -Run: -mvn clean install -DskipTests=true -Pperformance-tests +1) Run: +mvn clean verify -DskipTests=true -Pperformance-tests with OOTB configuration (Assumption is mongo running on 27017 as it's using mongo by default). This will create 10 new realms. -Then change keycloak_perf_test.jmx to have - "ThreadGroup.num_threads" to 10 and - "LoopController.loops" to 100 -And change "keycloak.perf.workerClass" to "org.keycloak.testsuite.performance.CreateUsersWorker" in system.properties +2) Then change src/test/jmeter/system.properties to have + "Tkeycloak.jmeter.numThreads" to 10 and + "keycloak.jmeter.loops" to 100 + "keycloak.perf.workerClass" to "org.keycloak.testsuite.performance.CreateUsersWorker" Then run again: -mvn clean install -DskipTests=true -Pperformance-tests -This will create 1000 new users (10 worker threads and each worker doing 100 iterations. Each worker is creating users in separate realm) +mvn clean verify -DskipTests=true -Pperformance-tests +This will create 1000 new users (10 worker threads and each worker doing 100 iterations. Each worker is creating users in separate realm. So 100 users like "user1", "user2", ... "user100" in each realm) + +3) Then change src/test/jmeter/system.properties to have + "keycloak.perf.workerClass" to "org.keycloak.testsuite.performance.ReadUsersWorker" + +Then run again: +mvn clean verify -DskipTests=true -Pperformance-tests + This will read all 1000 previously created users and each user is read 5 times. There are 1000 iterations in total and each iteration is doing 5 read users. + TODO: Easier configuration without need to edit config files, more user friendly, easier to configure and run test diff --git a/testsuite/performance/pom.xml b/testsuite/performance/pom.xml index 6dbe26e2ba..1ba9c0fc8c 100755 --- a/testsuite/performance/pom.xml +++ b/testsuite/performance/pom.xml @@ -3,10 +3,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - keycloak-parent + keycloak-testsuite-pom org.keycloak 1.0-beta-4-SNAPSHOT - ../../pom.xml + ../pom.xml 4.0.0 @@ -73,9 +73,31 @@ net.iharder base64 + + org.bouncycastle + bcprov-jdk16 + org.apache.jmeter ApacheJMeter_java + + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcmail-jdk15on + + + org.bouncycastle + bcmail-jdk15 + + + org.apache.tika + tika-parsers + + @@ -161,6 +183,10 @@ org.slf4j slf4j-simple + + commons-io + commons-io + @@ -168,6 +194,16 @@ jboss-logging ${jboss.logging.version} + + commons-io + commons-io + 2.4 + + + org.bouncycastle + bcprov-jdk16 + ${bouncycastle.version} + + + org.jboss.resteasy + jaxrs-api + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version.latest} + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + + + org.jboss.resteasy + resteasy-client + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-crypto + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-multipart-provider + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-jackson-provider + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-undertow + ${resteasy.version.latest} + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.codehaus.mojo + exec-maven-plugin + + ${project.basedir} + + + + + + diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java new file mode 100644 index 0000000000..55cc44f9b2 --- /dev/null +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java @@ -0,0 +1,87 @@ +package org.keycloak.testsuite.performance.web; + +import java.io.InputStream; + +import javax.servlet.DispatcherType; + +import io.undertow.servlet.Servlets; +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.FilterInfo; +import io.undertow.servlet.api.ServletInfo; +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; +import org.jboss.resteasy.spi.ResteasyDeployment; +import org.keycloak.services.filters.ClientConnectionFilter; +import org.keycloak.services.filters.KeycloakSessionServletFilter; +import org.keycloak.test.tools.KeycloakTestApplication; +import org.keycloak.testutils.KeycloakServer; + +/** + * @author Marek Posolda + */ +public class KeycloakPerfServer { + + private KeycloakServer keycloakServer; + + public static void main(String[] args) throws Throwable { + KeycloakServer keycloakServer = KeycloakServer.bootstrapKeycloakServer(args); + System.out.println("Keycloak server bootstrapped"); + + ProviderSessionFactoryHolder.setProviderSessionFactory(keycloakServer.getProviderSessionFactory()); + new KeycloakPerfServer(keycloakServer).start(); + } + + public KeycloakPerfServer(KeycloakServer keycloakServer) { + this.keycloakServer = keycloakServer; + } + + public void start() { + importPerfRealm(); + deployPerfTools(); + deployPerfApp(); + } + + protected void importPerfRealm() { + InputStream perfRealmStream = KeycloakPerfServer.class.getClassLoader().getResourceAsStream("perfrealm.json"); + keycloakServer.importRealm(perfRealmStream); + } + + protected void deployPerfTools() { + ResteasyDeployment deployment = new ResteasyDeployment(); + deployment.setApplicationClass(KeycloakToolsApplication.class.getName()); + + UndertowJaxrsServer server = keycloakServer.getServer(); + + DeploymentInfo di = server.undertowDeployment(deployment, ""); + di.setClassLoader(KeycloakTestApplication.class.getClassLoader()); + di.setContextPath("/keycloak-tools"); + di.setDeploymentName("KeycloakTools"); + + FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class); + di.addFilter(filter); + di.addFilterUrlMapping("SessionFilter", "/perf/*", DispatcherType.REQUEST); + + FilterInfo connectionFilter = Servlets.filter("ClientConnectionFilter", ClientConnectionFilter.class); + di.addFilter(connectionFilter); + di.addFilterUrlMapping("ClientConnectionFilter", "/perf/*", DispatcherType.REQUEST); + + server.deploy(di); + + System.out.println("Keycloak tools deployed"); + } + + protected void deployPerfApp() { + DeploymentInfo deploymentInfo = new DeploymentInfo(); + deploymentInfo.setClassLoader(getClass().getClassLoader()); + deploymentInfo.setDeploymentName("PerfApp"); + deploymentInfo.setContextPath("/perf-app"); + + ServletInfo servlet = new ServletInfo("PerfAppServlet", PerfAppServlet.class); + servlet.addMapping("/*"); + + deploymentInfo.addServlet(servlet); + + keycloakServer.getServer().deploy(deploymentInfo); + + System.out.println("PerfApp deployed"); + } +} diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java new file mode 100644 index 0000000000..94798d9b45 --- /dev/null +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java @@ -0,0 +1,42 @@ +package org.keycloak.testsuite.performance.web; + +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; + +import org.jboss.resteasy.core.Dispatcher; +import org.keycloak.provider.ProviderSessionFactory; +import org.keycloak.test.tools.PerfTools; + +/** + * Modified version of {@link org.keycloak.test.tools.KeycloakTestApplication}, which shares ProviderSessionFactory with KeycloakApplication + * + * @author Marek Posolda + */ +public class KeycloakToolsApplication extends Application { + + protected ProviderSessionFactory providerSessionFactory; + protected Set> classes = new HashSet>(); + protected Set singletons = new HashSet(); + + public KeycloakToolsApplication(@Context ServletContext context, @Context Dispatcher dispatcher) { + this.providerSessionFactory = ProviderSessionFactoryHolder.getProviderSessionFactory(); + context.setAttribute(ProviderSessionFactory.class.getName(), this.providerSessionFactory); + singletons.add(new PerfTools(providerSessionFactory)); + } + + @Override + public Set> getClasses() { + return classes; + } + + @Override + public Set getSingletons() { + return singletons; + } + + +} diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java new file mode 100644 index 0000000000..cc38ae6373 --- /dev/null +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java @@ -0,0 +1,39 @@ +package org.keycloak.testsuite.performance.web; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Marek Posolda + */ +public class PerfAppServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String resourcePath = "perf-app-resources" + req.getPathInfo(); + System.out.println("Resource path: " + resourcePath); + + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath); + if (inputStream == null) { + resp.getWriter().println("Not found: " + resourcePath); + } else { + OutputStream servletOutputStream = resp.getOutputStream(); + + byte[] buf = new byte[1024]; + int bytesRead = 0; + while (bytesRead != -1) { + bytesRead = inputStream.read(buf); + if (bytesRead != -1) { + servletOutputStream.write(buf, 0, bytesRead); + } + } + servletOutputStream.flush(); + } + } +} diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java new file mode 100644 index 0000000000..ea1420bbd1 --- /dev/null +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java @@ -0,0 +1,21 @@ +package org.keycloak.testsuite.performance.web; + +import org.keycloak.provider.ProviderSessionFactory; + +/** + * Static holder to allow sharing ProviderSessionFactory among different JAX-RS applications + * + * @author Marek Posolda + */ +public class ProviderSessionFactoryHolder { + + private static ProviderSessionFactory providerSessionFactory; + + public static ProviderSessionFactory getProviderSessionFactory() { + return providerSessionFactory; + } + + public static void setProviderSessionFactory(ProviderSessionFactory providerSessionFactory) { + ProviderSessionFactoryHolder.providerSessionFactory = providerSessionFactory; + } +} diff --git a/testsuite/performance-web/src/main/resources/perf-app-resources/index.html b/testsuite/performance-web/src/main/resources/perf-app-resources/index.html new file mode 100644 index 0000000000..c5b7c532c1 --- /dev/null +++ b/testsuite/performance-web/src/main/resources/perf-app-resources/index.html @@ -0,0 +1,110 @@ + + + + + + +
+ + + + + + + + + + + + +
+ +

Result

+

+
+

Events

+

+
+
+
+
+
\ No newline at end of file
diff --git a/testsuite/performance-web/src/main/resources/perfrealm.json b/testsuite/performance-web/src/main/resources/perfrealm.json
new file mode 100644
index 0000000000..aaa7fc46c6
--- /dev/null
+++ b/testsuite/performance-web/src/main/resources/perfrealm.json
@@ -0,0 +1,108 @@
+{
+    "id": "perf-realm",
+    "realm": "perf-realm",
+    "enabled": true,
+    "sslNotRequired": true,
+    "registrationAllowed": true,
+    "resetPasswordAllowed": true,
+    "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+    "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+    "requiredCredentials": [ "password" ],
+    "defaultRoles": [ "user" ],
+    "smtpServer": {
+        "from": "auto@keycloak.org",
+        "host": "localhost",
+        "port":"3025"
+    },
+    "users" : [
+        {
+            "username" : "test-user@localhost",
+            "enabled": true,
+            "email" : "test-user@localhost",
+            "credentials" : [
+                { "type" : "password",
+                  "value" : "password" }
+            ]
+        }
+    ],
+    "oauthClients" : [
+        {
+            "name" : "third-party",
+            "enabled": true,
+            "redirectUris": [
+                "http://localhost:8081/app/*"
+            ],
+            "secret": "password"
+        }
+    ],
+    "roleMappings": [
+        {
+            "username": "test-user@localhost",
+            "roles": ["user"]
+        }
+    ],
+    "scopeMappings": [
+        {
+            "client": "third-party",
+            "roles": ["user"]
+        },
+        {
+            "client": "perf-app",
+            "roles": ["user"]
+        }
+    ],
+    "applications": [
+        {
+            "name": "perf-app",
+            "enabled": true,
+            "baseUrl": "http://localhost:8081/perf-app",
+            "redirectUris": [
+                "http://localhost:8081/perf-app/*"
+            ],
+            "adminUrl": "http://localhost:8081/perf-app/logout",
+            "secret": "password"
+         }
+    ],
+    "roles" : {
+        "realm" : [
+            {
+                "name": "user",
+                "description": "Have User privileges"
+            },
+            {
+                "name": "admin",
+                "description": "Have Administrator privileges"
+            }
+        ],
+        "application" : {
+            "perf-app" : [
+                {
+                    "name": "customer-user",
+                    "description": "Have Customer User privileges"
+                },
+                {
+                    "name": "customer-admin",
+                    "description": "Have Customer Admin privileges"
+                }
+            ]
+        }
+
+    },
+
+    "applicationRoleMappings": {
+        "perf-app": [
+            {
+                "username": "test-user@localhost",
+                "roles": ["customer-user"]
+            }
+        ]
+    },
+    "applicationScopeMappings": {
+        "perf-app": [
+            {
+                "client": "third-party",
+                "roles": ["customer-user"]
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index 36085e7d3a..7961ca557e 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -28,6 +28,7 @@
         integration
         performance
         tools
+        performance-web
     
 
 

From a1d4d8e13fa7a235b128369c17023f303169e921 Mon Sep 17 00:00:00 2001
From: mposolda 
Date: Wed, 25 Jun 2014 14:29:38 +0200
Subject: [PATCH 3/8] Work in progress...

---
 testsuite/performance-web/pom.xml             |  14 +
 .../performance/web/KeycloakPerfServer.java   |   6 +-
 .../performance/web/OAuthClient.java          | 376 ++++++++++++++++++
 .../performance/web/PerfAppServlet.java       | 112 +++++-
 .../resources/perf-app-resources/index.ftl    |  51 +++
 .../{index.html => js-console.html}           |   0
 6 files changed, 540 insertions(+), 19 deletions(-)
 create mode 100644 testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/OAuthClient.java
 create mode 100644 testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl
 rename testsuite/performance-web/src/main/resources/perf-app-resources/{index.html => js-console.html} (100%)

diff --git a/testsuite/performance-web/pom.xml b/testsuite/performance-web/pom.xml
index 7ca6bacccc..8cf6f19b75 100644
--- a/testsuite/performance-web/pom.xml
+++ b/testsuite/performance-web/pom.xml
@@ -17,11 +17,25 @@
             org.keycloak
             keycloak-testsuite-integration
             ${project.version}
+            
+                
+                    org.jboss.spec.javax.servlet
+                    jboss-servlet-api_3.0_spec
+                
+            
         
         
             org.keycloak
             keycloak-testsuite-tools
             ${project.version}
+            war
+        
+
+        
+        
+            org.jboss.spec.javax.servlet
+            jboss-servlet-api_3.1_spec
+            1.0.0.Final
         
 
         
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
index 55cc44f9b2..b15101955a 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
@@ -4,9 +4,11 @@ import java.io.InputStream;
 
 import javax.servlet.DispatcherType;
 
+import io.undertow.server.handlers.resource.ClassPathResourceManager;
 import io.undertow.servlet.Servlets;
 import io.undertow.servlet.api.DeploymentInfo;
 import io.undertow.servlet.api.FilterInfo;
+import io.undertow.servlet.api.MimeMapping;
 import io.undertow.servlet.api.ServletInfo;
 import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
 import org.jboss.resteasy.spi.ResteasyDeployment;
@@ -76,10 +78,12 @@ public class KeycloakPerfServer {
         deploymentInfo.setContextPath("/perf-app");
 
         ServletInfo servlet = new ServletInfo("PerfAppServlet", PerfAppServlet.class);
-        servlet.addMapping("/*");
+        servlet.addMapping("/perf-servlet");
 
         deploymentInfo.addServlet(servlet);
 
+        deploymentInfo.setResourceManager(new ClassPathResourceManager(getClass().getClassLoader()));
+
         keycloakServer.getServer().deploy(deploymentInfo);
 
         System.out.println("PerfApp deployed");
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/OAuthClient.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/OAuthClient.java
new file mode 100644
index 0000000000..bb6e0c1535
--- /dev/null
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/OAuthClient.java
@@ -0,0 +1,376 @@
+package org.keycloak.testsuite.performance.web;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.jboss.resteasy.security.PemUtils;
+import org.json.JSONObject;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.RSATokenVerifier;
+import org.keycloak.VerificationException;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.resources.TokenService;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * TODO: Remove from here and instead merge with org.keycloak.testsuite.OAuthClient
+ *
+ *@author Stian Thorgersen
+ */
+public class OAuthClient {
+
+    private String baseUrl = "http://localhost:8081/auth";
+
+    private String realm = "perf-realm";
+
+    private String responseType = OAuth2Constants.CODE;
+
+    private String grantType = "authorization_code";
+
+    private String clientId = "perf-app";
+
+    private String redirectUri = "http://localhost:8081/perf-app/perf-servlet";
+
+    private String state = "123";
+
+    private PublicKey realmPublicKey;
+
+    public OAuthClient() {
+        try {
+            JSONObject realmJson = new JSONObject(IOUtils.toString(getClass().getResourceAsStream("/perfrealm.json")));
+            realmPublicKey = PemUtils.decodePublicKey(realmJson.getString("publicKey"));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to retrieve realm public key", e);
+        }
+    }
+
+    public AccessTokenResponse doAccessTokenRequest(String code, String password) {
+        HttpClient client = new DefaultHttpClient();
+        HttpPost post = new HttpPost(getAccessTokenUrl());
+
+        List parameters = new LinkedList();
+        if (grantType != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, grantType));
+        }
+        if (code != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
+        }
+        if (redirectUri != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
+        }
+        if (clientId != null && password != null) {
+            String authorization = BasicAuthHelper.createHeader(clientId, password);
+            post.setHeader("Authorization", authorization);
+        }
+        else if (clientId != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
+        }
+
+        UrlEncodedFormEntity formEntity = null;
+        try {
+            formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        post.setEntity(formEntity);
+
+        try {
+            return new AccessTokenResponse(client.execute(post));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to retrieve access token", e);
+        }
+    }
+
+    public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username,  String password) throws Exception {
+        HttpClient client = new DefaultHttpClient();
+        HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
+
+        String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+        post.setHeader("Authorization", authorization);
+
+        List parameters = new LinkedList();
+        parameters.add(new BasicNameValuePair("username", username));
+        parameters.add(new BasicNameValuePair("password", password));
+
+        UrlEncodedFormEntity formEntity;
+        try {
+            formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        post.setEntity(formEntity);
+
+        return new AccessTokenResponse(client.execute(post));
+    }
+
+    public HttpResponse doLogout(String redirectUri, String sessionState) throws IOException {
+        HttpClient client = new DefaultHttpClient();
+        HttpGet get = new HttpGet(getLogoutUrl(redirectUri, sessionState));
+
+        return client.execute(get);
+    }
+
+    public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
+        HttpClient client = new DefaultHttpClient();
+        HttpPost post = new HttpPost(getRefreshTokenUrl());
+
+        List parameters = new LinkedList();
+        if (grantType != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, grantType));
+        }
+        if (refreshToken != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
+        }
+        if (clientId != null && password != null) {
+            String authorization = BasicAuthHelper.createHeader(clientId, password);
+            post.setHeader("Authorization", authorization);
+        }
+        else if (clientId != null) {
+            parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
+        }
+
+        UrlEncodedFormEntity formEntity;
+        try {
+            formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        post.setEntity(formEntity);
+
+        try {
+            return new AccessTokenResponse(client.execute(post));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to retrieve access token", e);
+        }
+    }
+
+    public AccessToken verifyToken(String token) {
+        try {
+            return RSATokenVerifier.verifyToken(token, realmPublicKey, realm);
+        } catch (VerificationException e) {
+            throw new RuntimeException("Failed to verify token", e);
+        }
+    }
+
+    public void verifyCode(String code) {
+        if (!RSAProvider.verify(new JWSInput(code), realmPublicKey)) {
+            throw new RuntimeException("Failed to verify code");
+        }
+    }
+
+    public RefreshToken verifyRefreshToken(String refreshToken) {
+        try {
+            JWSInput jws = new JWSInput(refreshToken);
+            if (!RSAProvider.verify(jws, realmPublicKey)) {
+                throw new RuntimeException("Invalid refresh token");
+            }
+            return jws.readJsonContent(RefreshToken.class);
+        } catch (Exception e) {
+            throw new RuntimeException("Invalid refresh token", e);
+        }
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public String getRedirectUri() {
+        return redirectUri;
+    }
+
+    public String getLoginFormUrl() {
+        UriBuilder b = TokenService.loginPageUrl(UriBuilder.fromUri(baseUrl));
+        if (responseType != null) {
+            b.queryParam(OAuth2Constants.RESPONSE_TYPE, responseType);
+        }
+        if (clientId != null) {
+            b.queryParam(OAuth2Constants.CLIENT_ID, clientId);
+        }
+        if (redirectUri != null) {
+            b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+        }
+        if (state != null) {
+            b.queryParam(OAuth2Constants.STATE, state);
+        }
+        return b.build(realm).toString();
+    }
+
+    public String getAccessTokenUrl() {
+        UriBuilder b = TokenService.accessCodeToTokenUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realm).toString();
+    }
+
+    public String getLogoutUrl(String redirectUri, String sessionState) {
+        UriBuilder b = TokenService.logoutUrl(UriBuilder.fromUri(baseUrl));
+        if (redirectUri != null) {
+            b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
+        }
+        if (sessionState != null) {
+            b.queryParam("session_state", sessionState);
+        }
+        return b.build(realm).toString();
+    }
+
+    public String getResourceOwnerPasswordCredentialGrantUrl() {
+        UriBuilder b = TokenService.grantAccessTokenUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realm).toString();
+    }
+
+    public String getRefreshTokenUrl() {
+        UriBuilder b = TokenService.refreshUrl(UriBuilder.fromUri(baseUrl));
+        return b.build(realm).toString();
+    }
+
+    public OAuthClient realm(String realm) {
+        this.realm = realm;
+        return this;
+    }
+    public OAuthClient realmPublicKey(PublicKey key) {
+        this.realmPublicKey = key;
+        return this;
+    }
+
+    public OAuthClient clientId(String clientId) {
+        this.clientId = clientId;
+        return this;
+    }
+
+    public OAuthClient redirectUri(String redirectUri) {
+        this.redirectUri = redirectUri;
+        return this;
+    }
+
+    public OAuthClient responseType(String responseType) {
+        this.responseType = responseType;
+        return this;
+    }
+
+    public OAuthClient state(String state) {
+        this.state = state;
+        return this;
+    }
+
+    public String getRealm() {
+        return realm;
+    }
+
+    public static class AuthorizationCodeResponse {
+
+        private String code;
+        private String state;
+        private String error;
+
+        public AuthorizationCodeResponse(OAuthClient client, HttpServletRequest req) {
+            code = req.getParameter(OAuth2Constants.CODE);
+            state = req.getParameter(OAuth2Constants.STATE);
+            error = req.getParameter(OAuth2Constants.ERROR);
+        }
+
+        public String getCode() {
+            return code;
+        }
+
+        public String getState() {
+            return state;
+        }
+
+        public String getError() {
+            return error;
+        }
+
+    }
+
+    public static class AccessTokenResponse {
+        private int statusCode;
+
+        private String accessToken;
+        private String tokenType;
+        private int expiresIn;
+        private String refreshToken;
+        private String idToken;
+        private String sessionState;
+
+        private String error;
+
+        public AccessTokenResponse(HttpResponse response) throws Exception {
+            statusCode = response.getStatusLine().getStatusCode();
+            if (!"application/json".equals(response.getHeaders("Content-Type")[0].getValue())) {
+                throw new RuntimeException("Invalid content type");
+            }
+
+            String s = IOUtils.toString(response.getEntity().getContent());
+            JSONObject responseJson = new JSONObject(s);
+
+            if (statusCode == 200) {
+                accessToken = responseJson.getString("access_token");
+                tokenType = responseJson.getString("token_type");
+                expiresIn = responseJson.getInt("expires_in");
+                idToken = responseJson.optString("id_token");
+                sessionState = responseJson.optString("session-state");
+
+                if (responseJson.has(OAuth2Constants.REFRESH_TOKEN)) {
+                    refreshToken = responseJson.getString(OAuth2Constants.REFRESH_TOKEN);
+                }
+            } else {
+                error = responseJson.getString(OAuth2Constants.ERROR);
+            }
+        }
+
+        public String getAccessToken() {
+            return accessToken;
+        }
+
+        public String getError() {
+            return error;
+        }
+
+        public int getExpiresIn() {
+            return expiresIn;
+        }
+
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        public String getRefreshToken() {
+            return refreshToken;
+        }
+
+        public String getTokenType() {
+            return tokenType;
+        }
+
+        public String getIdToken() {
+            return idToken;
+        }
+
+        public String getSessionState() {
+            return sessionState;
+        }
+    }
+
+
+}
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
index cc38ae6373..6de11f9b45 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
@@ -1,39 +1,115 @@
 package org.keycloak.testsuite.performance.web;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+
 /**
  * @author Marek Posolda
  */
 public class PerfAppServlet extends HttpServlet {
 
+    private Template indexTemplate;
+    private OAuthClient oauthClient;
+
+    @Override
+    public void init() throws ServletException {
+        try {
+            Configuration cfg = new Configuration();
+            cfg.setTemplateLoader(new ClassTemplateLoader(getClass(), "/"));
+            indexTemplate = cfg.getTemplate("perf-app-resources/index.ftl");
+
+            oauthClient = new OAuthClient();
+        } catch (IOException ioe) {
+            throw new ServletException(ioe);
+        }
+    }
+
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-        String resourcePath = "perf-app-resources" + req.getPathInfo();
-        System.out.println("Resource path: " + resourcePath);
+        resp.setContentType("text/html");
+        String action = req.getParameter("action");
 
-        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
-        if (inputStream == null) {
-            resp.getWriter().println("Not found: " + resourcePath);
-        } else {
-            OutputStream servletOutputStream = resp.getOutputStream();
-
-            byte[] buf = new byte[1024];
-            int bytesRead = 0;
-            while (bytesRead != -1) {
-                bytesRead = inputStream.read(buf);
-                if (bytesRead != -1) {
-                    servletOutputStream.write(buf, 0, bytesRead);
-                }
+        if (action != null) {
+            if (action.equals("code")) {
+                keycloakLoginRedirect(req, resp);
+                return;
+            } else if (action.equals("exchangeCode")) {
+                exchangeCodeForToken(req, resp);
+            } else if (action.equals("refresh")) {
+                refreshToken(req, resp);
+            } else if (action.equals("logout")) {
+                logoutRedirect(req, resp);
+                return;
             }
-            servletOutputStream.flush();
+        }
+
+        String code = req.getParameter("code");
+        if (code != null) {
+            req.getSession().setAttribute("code", code);
+        }
+
+        String freemarkerRedirect = freemarkerRedirect(req, resp);
+        resp.getWriter().println(freemarkerRedirect);
+        resp.getWriter().flush();
+    }
+
+    protected void keycloakLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String loginUrl = oauthClient.getLoginFormUrl();
+        resp.sendRedirect(loginUrl);
+    }
+
+    protected void exchangeCodeForToken(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String code = (String)req.getSession().getAttribute("code");
+        OAuthClient.AccessTokenResponse atResponse = oauthClient.doAccessTokenRequest(code, "password");
+
+        String accessToken = atResponse.getAccessToken();
+        String refreshToken = atResponse.getRefreshToken();
+        req.getSession().setAttribute("accessToken", accessToken);
+        req.getSession().setAttribute("refreshToken", refreshToken);
+    }
+
+    protected void refreshToken(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        String refreshToken = (String)req.getSession().getAttribute("refreshToken");
+        OAuthClient.AccessTokenResponse atResponse = oauthClient.doRefreshTokenRequest(refreshToken, "password");
+
+        String accessToken = atResponse.getAccessToken();
+        refreshToken = atResponse.getRefreshToken();
+        req.getSession().setAttribute("accessToken", accessToken);
+        req.getSession().setAttribute("refreshToken", refreshToken);
+    }
+
+    protected void logoutRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+    }
+
+    private String freemarkerRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        Map attributes = new HashMap();
+        attributes.put("requestURI", req.getRequestURI());
+        attributes.put("code",  req.getSession().getAttribute("code"));
+        attributes.put("accessToken",  req.getSession().getAttribute("accessToken"));
+        attributes.put("refreshToken",  req.getSession().getAttribute("refreshToken"));
+
+        try {
+            Writer out = new StringWriter();
+            indexTemplate.process(attributes, out);
+            return out.toString();
+        } catch (TemplateException te) {
+            throw new ServletException(te);
         }
     }
 }
diff --git a/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl b/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl
new file mode 100644
index 0000000000..b8e92eb2f9
--- /dev/null
+++ b/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl
@@ -0,0 +1,51 @@
+
+
+  
+    PerfTest
+    
+  
+  
+
+    

Login and get code | Exchange code | Refresh token | Logout +

+ +

+ <#if code??> + Code: ${code} +


+ + + <#if accessToken??> + accessToken: ${accessToken} +
+

+        
+ + + + + <#if refreshToken??> + refreshToken: ${refreshToken} +
+

+        
+ + + + +

+

+ + \ No newline at end of file diff --git a/testsuite/performance-web/src/main/resources/perf-app-resources/index.html b/testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html similarity index 100% rename from testsuite/performance-web/src/main/resources/perf-app-resources/index.html rename to testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html From 4ebc575afcf9f3e7abe00bd67fcd43ee95632393 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 26 Jun 2014 10:16:34 +0200 Subject: [PATCH 4/8] Change perf test to parse accessToken and refreshToken in Servlet. Support for logout --- .../performance/web/KeycloakPerfServer.java | 2 +- .../performance/web/PerfAppServlet.java | 56 +++++++-- .../resources/perf-app-resources/index.ftl | 28 ++--- .../perf-app-resources/js-console.html | 110 ------------------ .../src/main/resources/perfrealm.json | 2 +- 5 files changed, 65 insertions(+), 133 deletions(-) delete mode 100644 testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java index b15101955a..9a64680c52 100644 --- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java @@ -78,7 +78,7 @@ public class KeycloakPerfServer { deploymentInfo.setContextPath("/perf-app"); ServletInfo servlet = new ServletInfo("PerfAppServlet", PerfAppServlet.class); - servlet.addMapping("/perf-servlet"); + servlet.addMapping("/perf-servlet/*"); deploymentInfo.addServlet(servlet); diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java index 6de11f9b45..a781b31218 100644 --- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java +++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java @@ -15,8 +15,10 @@ import freemarker.cache.ClassTemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; +import org.keycloak.adapters.AdapterConstants; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; +import org.keycloak.util.Time; /** * @author Marek Posolda @@ -43,6 +45,7 @@ public class PerfAppServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); String action = req.getParameter("action"); + String actionDone = null; if (action != null) { if (action.equals("code")) { @@ -50,8 +53,10 @@ public class PerfAppServlet extends HttpServlet { return; } else if (action.equals("exchangeCode")) { exchangeCodeForToken(req, resp); + actionDone = "Token retrieved"; } else if (action.equals("refresh")) { refreshToken(req, resp); + actionDone = "Token refreshed"; } else if (action.equals("logout")) { logoutRedirect(req, resp); return; @@ -61,13 +66,22 @@ public class PerfAppServlet extends HttpServlet { String code = req.getParameter("code"); if (code != null) { req.getSession().setAttribute("code", code); + actionDone = "Code retrieved"; } - String freemarkerRedirect = freemarkerRedirect(req, resp); + String freemarkerRedirect = freemarkerRedirect(req, resp, actionDone); resp.getWriter().println(freemarkerRedirect); resp.getWriter().flush(); } + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getRequestURI().endsWith(AdapterConstants.K_LOGOUT)) { + // System.out.println("Logout callback triggered"); + resp.setStatus(204); + } + } + protected void keycloakLoginRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String loginUrl = oauthClient.getLoginFormUrl(); resp.sendRedirect(loginUrl); @@ -77,32 +91,60 @@ public class PerfAppServlet extends HttpServlet { String code = (String)req.getSession().getAttribute("code"); OAuthClient.AccessTokenResponse atResponse = oauthClient.doAccessTokenRequest(code, "password"); - String accessToken = atResponse.getAccessToken(); - String refreshToken = atResponse.getRefreshToken(); - req.getSession().setAttribute("accessToken", accessToken); - req.getSession().setAttribute("refreshToken", refreshToken); + updateTokensInSession(req, atResponse); } protected void refreshToken(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String refreshToken = (String)req.getSession().getAttribute("refreshToken"); OAuthClient.AccessTokenResponse atResponse = oauthClient.doRefreshTokenRequest(refreshToken, "password"); + updateTokensInSession(req, atResponse); + } + + private void updateTokensInSession(HttpServletRequest req, OAuthClient.AccessTokenResponse atResponse) { String accessToken = atResponse.getAccessToken(); - refreshToken = atResponse.getRefreshToken(); + String refreshToken = atResponse.getRefreshToken(); + AccessToken accessTokenParsed = oauthClient.verifyToken(accessToken); + RefreshToken refreshTokenParsed = oauthClient.verifyRefreshToken(refreshToken); req.getSession().setAttribute("accessToken", accessToken); req.getSession().setAttribute("refreshToken", refreshToken); + req.getSession().setAttribute("accessTokenParsed", accessTokenParsed); + req.getSession().setAttribute("refreshTokenParsed", refreshTokenParsed); } protected void logoutRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String sessionState = null; + AccessToken accessTokenParsed = (AccessToken)req.getSession().getAttribute("accessTokenParsed"); + if (accessTokenParsed != null) { + sessionState = accessTokenParsed.getSessionState(); + } + // Invalidate http session + req.getSession(false).invalidate(); + + String logoutURL = oauthClient.getLogoutUrl(oauthClient.getRedirectUri(), sessionState); + resp.sendRedirect(logoutURL); } - private String freemarkerRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + private String freemarkerRedirect(HttpServletRequest req, HttpServletResponse resp, String actionDone) throws ServletException, IOException { + AccessToken accessTokenParsed = (AccessToken)req.getSession().getAttribute("accessTokenParsed"); + RefreshToken refreshTokenParsed = (RefreshToken)req.getSession().getAttribute("refreshTokenParsed"); + Map attributes = new HashMap(); attributes.put("requestURI", req.getRequestURI()); attributes.put("code", req.getSession().getAttribute("code")); attributes.put("accessToken", req.getSession().getAttribute("accessToken")); attributes.put("refreshToken", req.getSession().getAttribute("refreshToken")); + attributes.put("accessTokenParsed", accessTokenParsed); + attributes.put("refreshTokenParsed", refreshTokenParsed); + attributes.put("actionDone", actionDone); + + if (accessTokenParsed != null) { + attributes.put("accessTokenExpiration", Time.toDate(accessTokenParsed.getExpiration()).toString()); + } + if (refreshTokenParsed != null) { + attributes.put("refreshTokenExpiration", Time.toDate(refreshTokenParsed.getExpiration()).toString()); + } try { Writer out = new StringWriter(); diff --git a/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl b/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl index b8e92eb2f9..74af2bece7 100644 --- a/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl +++ b/testsuite/performance-web/src/main/resources/perf-app-resources/index.ftl @@ -19,30 +19,30 @@

<#if code??> - Code: ${code} + Code Available
+ Code=${code}


<#if accessToken??> - accessToken: ${accessToken} -
-

+        Access Token Available
+ AccessToken=${accessToken}
+ Username=${accessTokenParsed.preferredUsername}
+ SessionState=${accessTokenParsed.sessionState}
+ Expiration=${accessTokenExpiration}

- - <#if refreshToken??> - refreshToken: ${refreshToken} -
-

+        Refresh token available
+ RefreshToken=${refreshToken}
+ Expiration=${refreshTokenExpiration}

+ - + <#if actionDone??> + RequestAction=${actionDone} +

diff --git a/testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html b/testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html deleted file mode 100644 index c5b7c532c1..0000000000 --- a/testsuite/performance-web/src/main/resources/perf-app-resources/js-console.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - -
- - - - - - - - - - - - -
- -

Result

-

-
-

Events

-

-
-
-
-
-
\ No newline at end of file
diff --git a/testsuite/performance-web/src/main/resources/perfrealm.json b/testsuite/performance-web/src/main/resources/perfrealm.json
index aaa7fc46c6..2dadcc2be6 100644
--- a/testsuite/performance-web/src/main/resources/perfrealm.json
+++ b/testsuite/performance-web/src/main/resources/perfrealm.json
@@ -59,7 +59,7 @@
             "redirectUris": [
                 "http://localhost:8081/perf-app/*"
             ],
-            "adminUrl": "http://localhost:8081/perf-app/logout",
+            "adminUrl": "http://localhost:8081/perf-app/perf-servlet",
             "secret": "password"
          }
     ],

From 9ebe2ff45e278fd8f6c088e11f965ee485b82108 Mon Sep 17 00:00:00 2001
From: mposolda 
Date: Thu, 26 Jun 2014 15:35:58 +0200
Subject: [PATCH 5/8] Added support for get-users-count and
 create-available-users into PerfTools

---
 testsuite/performance-web/README.md           | 13 ++++++++
 .../src/main/resources/perfrealm.json         |  8 ++---
 .../org/keycloak/test/tools/PerfTools.java    | 30 +++++++++++++++++++
 3 files changed, 47 insertions(+), 4 deletions(-)
 create mode 100644 testsuite/performance-web/README.md

diff --git a/testsuite/performance-web/README.md b/testsuite/performance-web/README.md
new file mode 100644
index 0000000000..9db534f718
--- /dev/null
+++ b/testsuite/performance-web/README.md
@@ -0,0 +1,13 @@
+Adding users
+------------
+
+Adding 1000 new users (will start from last added user, so you don't need to explicitly check how many users to create are needed: 
+http://localhost:8081/keycloak-tools/perf/perf-realm/create-available-users?prefix=user&count=1000&batch=100&roles=user
+
+Checking users count: 
+http://localhost:8081/keycloak-tools/perf/perf-realm/get-users-count?prefix=user
+
+Switching to Mongo
+------------------
+Start with: (TODO)
+-Dkeycloak.model.provider=mongo -Dkeycloak.model.mongo.db=keycloak-perf
\ No newline at end of file
diff --git a/testsuite/performance-web/src/main/resources/perfrealm.json b/testsuite/performance-web/src/main/resources/perfrealm.json
index 2dadcc2be6..1fab9a52b4 100644
--- a/testsuite/performance-web/src/main/resources/perfrealm.json
+++ b/testsuite/performance-web/src/main/resources/perfrealm.json
@@ -16,9 +16,9 @@
     },
     "users" : [
         {
-            "username" : "test-user@localhost",
+            "username" : "test@localhost",
             "enabled": true,
-            "email" : "test-user@localhost",
+            "email" : "test@localhost",
             "credentials" : [
                 { "type" : "password",
                   "value" : "password" }
@@ -37,7 +37,7 @@
     ],
     "roleMappings": [
         {
-            "username": "test-user@localhost",
+            "username": "test@localhost",
             "roles": ["user"]
         }
     ],
@@ -92,7 +92,7 @@
     "applicationRoleMappings": {
         "perf-app": [
             {
-                "username": "test-user@localhost",
+                "username": "test@localhost",
                 "roles": ["customer-user"]
             }
         ]
diff --git a/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
index 2e08ed77e9..60a5baabfd 100644
--- a/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
+++ b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
@@ -17,9 +17,12 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
+
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -99,6 +102,33 @@ public class PerfTools {
         }
     }
 
+
+    @GET
+    @Path("{realm}/get-users-count")
+    public Response getUsersCountReq(@PathParam("realm") String realmName, @QueryParam("prefix") String prefix) {
+        int usersCount = getUsersCount(realmName, prefix);
+        return Response.ok(String.valueOf(usersCount)).build();
+    }
+
+    // Same as createUsers, but dynamically compute "start" (Next available user)
+    @GET
+    @Path("{realm}/create-available-users")
+    public Response createAvailableUsers(@PathParam("realm") String realmName, @QueryParam("count") Integer count, @QueryParam("batch") Integer batch, @QueryParam("prefix") String prefix, @QueryParam("roles") String roles) throws InterruptedException {
+        int start = getUsersCount(realmName, prefix);
+        return createUsers(realmName, count, batch, start, prefix, roles);
+    }
+
+    private int getUsersCount(String realmName, String prefix) {
+        RealmModel realm = session.getRealmByName(realmName);
+
+        // TODO: method for count on model
+        if (prefix == null) {
+            return realm.getUsers().size();
+        } else {
+            return realm.searchForUser(prefix).size();
+        }
+    }
+
     @GET
     @Path("export")
     public void export(@QueryParam("dir") String dir) {

From de774a305887327a42737fcf287f0a5332aaf064 Mon Sep 17 00:00:00 2001
From: mposolda 
Date: Thu, 26 Jun 2014 18:02:53 +0200
Subject: [PATCH 6/8] Add classes classifier to performance-tools war, so
 classes can be reused in performance-web module

---
 testsuite/performance-web/pom.xml | 2 +-
 testsuite/tools/pom.xml           | 6 ++++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/testsuite/performance-web/pom.xml b/testsuite/performance-web/pom.xml
index 8cf6f19b75..dd696dbf95 100644
--- a/testsuite/performance-web/pom.xml
+++ b/testsuite/performance-web/pom.xml
@@ -28,7 +28,7 @@
             org.keycloak
             keycloak-testsuite-tools
             ${project.version}
-            war
+            classes
         
 
         
diff --git a/testsuite/tools/pom.xml b/testsuite/tools/pom.xml
index 3d3d2c3256..c630624317 100755
--- a/testsuite/tools/pom.xml
+++ b/testsuite/tools/pom.xml
@@ -296,6 +296,12 @@
     
         keycloak-tools
         
+            
+                maven-war-plugin
+                
+                    true
+                
+            
             
                 org.jboss.as.plugins
                 jboss-as-maven-plugin

From b6fd58e644cc9e9503d7b0ee7e5dcc5dadf12247 Mon Sep 17 00:00:00 2001
From: mposolda 
Date: Fri, 27 Jun 2014 12:20:15 +0200
Subject: [PATCH 7/8] Support for configuring number of worker threads in
 undertow

---
 .../keycloak/testutils/KeycloakServer.java    | 18 +++++++++++++++-
 .../performance/web/KeycloakPerfServer.java   |  3 +--
 .../web/KeycloakSessionFactoryHolder.java     | 21 +++++++++++++++++++
 .../web/KeycloakToolsApplication.java         | 10 ++++-----
 .../web/ProviderSessionFactoryHolder.java     | 21 -------------------
 .../org/keycloak/test/tools/PerfTools.java    |  4 ++--
 6 files changed, 46 insertions(+), 31 deletions(-)
 create mode 100644 testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakSessionFactoryHolder.java
 delete mode 100644 testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java

diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
index 6f2aaf9536..c401f0a349 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
@@ -59,6 +59,7 @@ public class KeycloakServer {
     public static class KeycloakServerConfig {
         private String host = "localhost";
         private int port = 8081;
+        private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8;
         private String resourcesHome;
 
         public String getHost() {
@@ -84,6 +85,14 @@ public class KeycloakServer {
         public void setResourcesHome(String resourcesHome) {
             this.resourcesHome = resourcesHome;
         }
+
+        public int getWorkerThreads() {
+            return workerThreads;
+        }
+
+        public void setWorkerThreads(int workerThreads) {
+            this.workerThreads = workerThreads;
+        }
     }
 
     public static  T loadJson(InputStream is, Class type) {
@@ -141,6 +150,10 @@ public class KeycloakServer {
             config.setResourcesHome(dir.getAbsolutePath());
         }
 
+        if (System.getProperties().containsKey("undertowWorkerThreads")) {
+            int undertowWorkerThreads = Integer.parseInt(System.getProperty("undertowWorkerThreads"));
+            config.setWorkerThreads(undertowWorkerThreads);
+        }
 
         final KeycloakServer keycloak = new KeycloakServer(config);
         keycloak.sysout = true;
@@ -244,7 +257,10 @@ public class KeycloakServer {
         ResteasyDeployment deployment = new ResteasyDeployment();
         deployment.setApplicationClass(KeycloakApplication.class.getName());
 
-        Builder builder = Undertow.builder().addListener(config.getPort(), config.getHost());
+        Builder builder = Undertow.builder()
+                .addHttpListener(config.getPort(), config.getHost())
+                .setWorkerThreads(config.getWorkerThreads())
+                .setIoThreads(config.getWorkerThreads() / 8);
 
         server = new UndertowJaxrsServer().start(builder);
 
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
index 9a64680c52..2ad2fa2459 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
@@ -8,7 +8,6 @@ import io.undertow.server.handlers.resource.ClassPathResourceManager;
 import io.undertow.servlet.Servlets;
 import io.undertow.servlet.api.DeploymentInfo;
 import io.undertow.servlet.api.FilterInfo;
-import io.undertow.servlet.api.MimeMapping;
 import io.undertow.servlet.api.ServletInfo;
 import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
 import org.jboss.resteasy.spi.ResteasyDeployment;
@@ -28,7 +27,7 @@ public class KeycloakPerfServer {
         KeycloakServer keycloakServer = KeycloakServer.bootstrapKeycloakServer(args);
         System.out.println("Keycloak server bootstrapped");
 
-        ProviderSessionFactoryHolder.setProviderSessionFactory(keycloakServer.getProviderSessionFactory());
+        KeycloakSessionFactoryHolder.setKeycloakSessionFactory(keycloakServer.getSessionFactory());
         new KeycloakPerfServer(keycloakServer).start();
     }
 
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakSessionFactoryHolder.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakSessionFactoryHolder.java
new file mode 100644
index 0000000000..adc3e19716
--- /dev/null
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakSessionFactoryHolder.java
@@ -0,0 +1,21 @@
+package org.keycloak.testsuite.performance.web;
+
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * Static holder to allow sharing ProviderSessionFactory among different JAX-RS applications
+ *
+ * @author Marek Posolda
+ */
+public class KeycloakSessionFactoryHolder {
+
+    private static KeycloakSessionFactory keycloakSessionFactory;
+
+    public static KeycloakSessionFactory getKeycloakSessionFactory() {
+        return keycloakSessionFactory;
+    }
+
+    public static void setKeycloakSessionFactory(KeycloakSessionFactory keycloakSessionFactory) {
+        KeycloakSessionFactoryHolder.keycloakSessionFactory = keycloakSessionFactory;
+    }
+}
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java
index 94798d9b45..e0612c1b39 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakToolsApplication.java
@@ -8,7 +8,7 @@ import javax.ws.rs.core.Application;
 import javax.ws.rs.core.Context;
 
 import org.jboss.resteasy.core.Dispatcher;
-import org.keycloak.provider.ProviderSessionFactory;
+import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.test.tools.PerfTools;
 
 /**
@@ -18,14 +18,14 @@ import org.keycloak.test.tools.PerfTools;
  */
 public class KeycloakToolsApplication extends Application {
 
-    protected ProviderSessionFactory providerSessionFactory;
+    protected KeycloakSessionFactory keycloakSessionFactory;
     protected Set> classes = new HashSet>();
     protected Set singletons = new HashSet();
 
     public KeycloakToolsApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
-        this.providerSessionFactory = ProviderSessionFactoryHolder.getProviderSessionFactory();
-        context.setAttribute(ProviderSessionFactory.class.getName(), this.providerSessionFactory);
-        singletons.add(new PerfTools(providerSessionFactory));
+        this.keycloakSessionFactory = KeycloakSessionFactoryHolder.getKeycloakSessionFactory();
+        context.setAttribute(KeycloakSessionFactory.class.getName(), this.keycloakSessionFactory);
+        singletons.add(new PerfTools(keycloakSessionFactory));
     }
 
     @Override
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java
deleted file mode 100644
index ea1420bbd1..0000000000
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/ProviderSessionFactoryHolder.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.keycloak.testsuite.performance.web;
-
-import org.keycloak.provider.ProviderSessionFactory;
-
-/**
- * Static holder to allow sharing ProviderSessionFactory among different JAX-RS applications
- *
- * @author Marek Posolda
- */
-public class ProviderSessionFactoryHolder {
-
-    private static ProviderSessionFactory providerSessionFactory;
-
-    public static ProviderSessionFactory getProviderSessionFactory() {
-        return providerSessionFactory;
-    }
-
-    public static void setProviderSessionFactory(ProviderSessionFactory providerSessionFactory) {
-        ProviderSessionFactoryHolder.providerSessionFactory = providerSessionFactory;
-    }
-}
diff --git a/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
index 60a5baabfd..32a1fdcae6 100644
--- a/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
+++ b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
@@ -113,9 +113,9 @@ public class PerfTools {
     // Same as createUsers, but dynamically compute "start" (Next available user)
     @GET
     @Path("{realm}/create-available-users")
-    public Response createAvailableUsers(@PathParam("realm") String realmName, @QueryParam("count") Integer count, @QueryParam("batch") Integer batch, @QueryParam("prefix") String prefix, @QueryParam("roles") String roles) throws InterruptedException {
+    public void createAvailableUsers(@PathParam("realm") String realmName, @QueryParam("count") Integer count, @QueryParam("batch") Integer batch, @QueryParam("prefix") String prefix, @QueryParam("roles") String roles) throws InterruptedException {
         int start = getUsersCount(realmName, prefix);
-        return createUsers(realmName, count, batch, start, prefix, roles);
+        createUsers(realmName, count, batch, start, prefix, roles);
     }
 
     private int getUsersCount(String realmName, String prefix) {

From cb9f1590910060281e57e8bba77594ac21971b7f Mon Sep 17 00:00:00 2001
From: mposolda 
Date: Fri, 27 Jun 2014 18:14:42 +0200
Subject: [PATCH 8/8] Added JMeter. Added KeycloakPerfServer into maven.

---
 testsuite/performance-web/README.md           | 109 ++++-
 testsuite/performance-web/pom.xml             | 128 ++++++
 .../performance/web/KeycloakPerfServer.java   |   7 +
 .../performance/web/PerfAppServlet.java       |   8 +-
 .../src/test/jmeter/jmeter.properties         |  20 +
 .../test/jmeter/keycloak_web_perf_test.jmx    | 387 ++++++++++++++++++
 6 files changed, 644 insertions(+), 15 deletions(-)
 create mode 100644 testsuite/performance-web/src/test/jmeter/jmeter.properties
 create mode 100644 testsuite/performance-web/src/test/jmeter/keycloak_web_perf_test.jmx

diff --git a/testsuite/performance-web/README.md b/testsuite/performance-web/README.md
index 9db534f718..950e90cc12 100644
--- a/testsuite/performance-web/README.md
+++ b/testsuite/performance-web/README.md
@@ -1,13 +1,106 @@
+Keycloak Web Performance Testsuite
+==================================
+To run web performance testsuite, you need to:
+1) Run Keycloak server
+2) Add some users into your Keycloak
+3) Run JMeter performance tests
+
+Keycloak server
+---------------
+In this project you can run:
+
+```shell
+mvn exec:java -Pkeycloak-perf-server
+````
+
+which will execute embedded Undertow server with:
+ * Keycloak server
+ * Performance-tools for mass adding of new users 
+ * Simple web application for testing performance  
+It will also automatically import realm "perf-realm" into Keycloak from file src/main/resources/perfrealm.json 
+
+Note that by default it will use in-memory H2 database, which means that all changes (for example all added users) are discarded after server restart. For performance testing, it's recommended to use some database like PostgreSQL or MySQL
+
+To run server with PostgreSQL you may use command like this (change host,port,dbName and credentials according your DB configuration):
+```shell
+mvn exec:java -Pkeycloak-perf-server -Dhibernate.connection.url=jdbc:postgresql://localhost:5432/keycloak_perf -Dhibernate.connection.driver_class=org.postgresql.Driver -Dhibernate.connection.username=postgres -Dhibernate.connection.password=postgres -Dhibernate.connection.autocommit=false
+````
+
+To run server with MySQL you may use command like this (change host,port,dbName and credentials according your DB configuration):
+```shell
+mvn exec:java -Pkeycloak-perf-server -Dhibernate.connection.url=jdbc:mysql://localhost/keycloak_perf -Dhibernate.connection.driver_class=com.mysql.jdbc.Driver -Dhibernate.connection.username=portal -Dhibernate.connection.password=portal -Dhibernate.connection.autocommit=false
+````
+
+To run server with Mongo you may use command like this (change host,port,dbName and credentials according your DB configuration):
+```shell
+mvn exec:java -Pkeycloak-perf-server -Dkeycloak.model.provider=mongo -Dkeycloak.model.mongo.db=keycloak-perf
+````
+
+To enable cache, you can add additional property:
+```shell
+-Dkeycloak.model.cache.provider=simple
+````
+
 Adding users
-------------
+-----------------
 
-Adding 1000 new users (will start from last added user, so you don't need to explicitly check how many users to create are needed: 
-http://localhost:8081/keycloak-tools/perf/perf-realm/create-available-users?prefix=user&count=1000&batch=100&roles=user
+Performance test is using users with prefix "user" (so users like "user-0", "user-1", ... , "user-123456" etc). So you first need to add some of these users into your database.
 
-Checking users count: 
+For checking users count, you can open this URL: 
+```shell
 http://localhost:8081/keycloak-tools/perf/perf-realm/get-users-count?prefix=user
+````
 
-Switching to Mongo
-------------------
-Start with: (TODO)
--Dkeycloak.model.provider=mongo -Dkeycloak.model.mongo.db=keycloak-perf
\ No newline at end of file
+For adding 10000 new users into your database (will start from last added user, so you don't need to explicitly check how many users to create are needed:
+```shell 
+http://localhost:8081/keycloak-tools/perf/perf-realm/create-available-users?prefix=user&count=10000&batch=100&roles=user
+````
+
+Seeing progress of job for creating users
+```shell 
+http://localhost:8081/keycloak-tools/perf/jobs
+````
+
+Note that with default H2 are all data automatically cleared after server restart. So it's recommended to use different DB like PostgreSQL or Mongo.
+
+
+Execute performance test
+------------------------
+
+When server is started and some users are created, you can run performance test. It's possible to run it from Command line with:
+
+```shell 
+mvn verify -Pperformance-test
+````
+
+By default, test is using Keycloak on localhost:8081 and 50 concurrent clients (threads) and each client doing 50 test iterations. Each iterations is:
+- Login user into KC and retrieve code
+- Exchange code for accessToken
+- Refresh token 2 times
+- Logout user
+
+Each client is using separate username, so actually you need at least 50 users created into your DB (users "user-0", "user-1", ... "user-49" . See above)
+
+ATM it's possible to adjust behaviour with properties:
+* host --- Keycloak host ("localhost" by default)
+* port --- Keycloak port ("8081" by default)
+* userPrefix --- prefix of users ("user" by default)
+* concurrentUsers --- Number of concurrent clients (50 by default). RampUp time is configured to start 5 new users each second, so with 50 users are all clients started in like 10 seconds.
+* iterationsPerUser --- Number of iterations per each client (50 by default). So by default we have 50*50 = 2500 iterations in total per whole test
+* refreshTokenRequestsPerIteration --- Number of refresh token iterations (2 by default)
+
+You can change configuration by adding some properties into command line for example to start just with 10 concurrent clients and 10 iterations per client (so just 100 test iterations) you can use:
+```shell 
+mvn verify -Pperformance-test -DconcurrentUsers=10 -DiterationsPerUser=10 
+````
+ 
+After triggering test are results in file target/jmeter/results/aggregatedRequests-durations--keycloak_web_perf_test.html
+
+Execute performance test from JMeter GUI
+----------------------------------------
+You can run
+```shell 
+mvn jmeter:gui
+````
+
+and then open file src/test/jmeter/keycloak_web_perf_test.jmx and trigger test from JMeter GUI. It may be good as you can see the progress of whole test during execution.
diff --git a/testsuite/performance-web/pom.xml b/testsuite/performance-web/pom.xml
index dd696dbf95..e2d61db79a 100644
--- a/testsuite/performance-web/pom.xml
+++ b/testsuite/performance-web/pom.xml
@@ -88,6 +88,30 @@
             resteasy-undertow
             ${resteasy.version.latest}
         
+
+        
+            org.apache.jmeter
+            ApacheJMeter_java
+            
+                
+                    org.bouncycastle
+                    bcprov-jdk15on
+                
+                
+                    org.bouncycastle
+                    bcmail-jdk15on
+                
+                
+                    org.bouncycastle
+                    bcmail-jdk15
+                
+                
+                    org.apache.tika
+                    tika-parsers
+                
+            
+        
+
     
 
     
@@ -117,4 +141,108 @@
         
     
 
+    
+
+        
+            keycloak-perf-server
+            
+                
+                    
+                        org.codehaus.mojo
+                        exec-maven-plugin
+                        
+                            org.keycloak.testsuite.performance.web.KeycloakPerfServer
+                        
+                    
+                
+            
+        
+
+        
+            performance-test
+
+            
+                localhost
+                8081
+                user
+                50
+                50
+                2
+            
+
+            
+                
+
+                    
+                        com.lazerycode.jmeter
+                        jmeter-maven-plugin
+                        
+                            false
+                            
+                                ${host}
+                                ${port}
+                                ${userPrefix}
+                                ${concurrentUsers}
+                                ${iterationsPerUser}
+                                ${refreshTokenRequestsPerIteration}
+                            
+                        
+                        
+                            
+                                jmeter-tests
+                                verify
+                                
+                                    jmeter
+                                
+                            
+                        
+                        
+                        
+                    
+
+                    
+                        com.lazerycode.jmeter
+                        jmeter-analysis-maven-plugin
+                        
+                            
+                                jmeter-tests-analyze
+                                verify
+                                
+                                    analyze
+                                
+                                
+                                    ${project.build.directory}/jmeter/results/*.jtl
+                                    ${project.build.directory}/jmeter/results
+                                    false
+
+                                    
+                                        
+                                            aggregatedRequests
+                                            * request
+                                        
+                                        
+                                            codes
+                                            **/perf-app/perf-servlet?code=*
+                                        
+                                    
+
+                                    
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                    
+                                
+                            
+                        
+                    
+
+                
+            
+        
+
+    
+
 
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
index 2ad2fa2459..5dde02ee22 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/KeycloakPerfServer.java
@@ -24,6 +24,13 @@ public class KeycloakPerfServer {
     private KeycloakServer keycloakServer;
 
     public static void main(String[] args) throws Throwable {
+        // TODO: should be better than programmatic setup here, but don't copy persistence.xml again...
+        System.setProperty("hibernate.hbm2ddl.auto", "update");
+
+        if (System.getProperty("undertowWorkerThreads") == null) {
+            System.setProperty("undertowWorkerThreads", "256");
+        }
+
         KeycloakServer keycloakServer = KeycloakServer.bootstrapKeycloakServer(args);
         System.out.println("Keycloak server bootstrapped");
 
diff --git a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
index a781b31218..52cf1fa10b 100644
--- a/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
+++ b/testsuite/performance-web/src/main/java/org/keycloak/testsuite/performance/web/PerfAppServlet.java
@@ -113,16 +113,10 @@ public class PerfAppServlet extends HttpServlet {
     }
 
     protected void logoutRedirect(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-        String sessionState = null;
-        AccessToken accessTokenParsed = (AccessToken)req.getSession().getAttribute("accessTokenParsed");
-        if (accessTokenParsed != null) {
-            sessionState = accessTokenParsed.getSessionState();
-        }
-
         // Invalidate http session
         req.getSession(false).invalidate();
 
-        String logoutURL = oauthClient.getLogoutUrl(oauthClient.getRedirectUri(), sessionState);
+        String logoutURL = oauthClient.getLogoutUrl(oauthClient.getRedirectUri(), null);
         resp.sendRedirect(logoutURL);
     }
 
diff --git a/testsuite/performance-web/src/test/jmeter/jmeter.properties b/testsuite/performance-web/src/test/jmeter/jmeter.properties
new file mode 100644
index 0000000000..a024bb5a2d
--- /dev/null
+++ b/testsuite/performance-web/src/test/jmeter/jmeter.properties
@@ -0,0 +1,20 @@
+#Thu Mar 07 18:46:04 BRT 2013
+not_in_menu=HTML Parameter Mask,HTTP User Parameter Modifier
+xml.parser=org.apache.xerces.parsers.SAXParser
+cookies=cookies
+wmlParser.className=org.apache.jmeter.protocol.http.parser.RegexpHTMLParser
+HTTPResponse.parsers=htmlParser wmlParser
+remote_hosts=127.0.0.1
+system.properties=system.properties
+beanshell.server.file=../extras/startup.bsh
+log_level.jmeter.junit=DEBUG
+sampleresult.timestamp.start=true
+jmeter.laf.mac=System
+log_level.jorphan=INFO
+classfinder.functions.contain=.functions.
+user.properties=user.properties
+wmlParser.types=text/vnd.wap.wml
+log_level.jmeter=DEBUG
+classfinder.functions.notContain=.gui.
+htmlParser.types=text/html application/xhtml+xml application/xml text/xml
+upgrade_properties=/bin/upgrade.properties
diff --git a/testsuite/performance-web/src/test/jmeter/keycloak_web_perf_test.jmx b/testsuite/performance-web/src/test/jmeter/keycloak_web_perf_test.jmx
new file mode 100644
index 0000000000..07074aa998
--- /dev/null
+++ b/testsuite/performance-web/src/test/jmeter/keycloak_web_perf_test.jmx
@@ -0,0 +1,387 @@
+
+
+  
+    
+      
+      false
+      false
+      
+        
+      
+      
+    
+    
+      
+        
+          
+            host
+            ${__P(host, localhost)}
+            =
+          
+          
+            port
+            ${__P(port, 8081)}
+            =
+          
+          
+            userPrefix
+            ${__P(userPrefix, user)}
+            =
+          
+          
+            concurrentUsers
+            ${__P(concurrentUsers, 50)}
+            =
+          
+          
+            iterationsPerUser
+            ${__P(iterationsPerUser, 50)}
+            =
+          
+          
+            refreshTokenRequestsPerIteration
+            ${__P(refreshTokenRequestsPerIteration, 2)}
+            =
+          
+        
+      
+      
+      
+        
+        true
+        org.apache.jmeter.protocol.http.control.HC4CookieHandler
+      
+      
+      
+        continue
+        
+          false
+          ${iterationsPerUser}
+        
+        ${concurrentUsers}
+        ${__javaScript(${concurrentUsers} / 5)}
+        1403769177000
+        1403769177000
+        false
+        
+        
+      
+      
+        
+          
+            username
+          
+          
+            
+              user-${__javaScript(${__threadNum}-1)}
+            
+          
+          false
+        
+        
+        
+          
+            
+          
+          ${host}
+          ${port}
+          
+          
+          
+          
+          
+          4
+        
+        
+        
+          
+            200
+          
+          Check that status is 200 in each HTTP response
+          Assertion.response_code
+          false
+          8
+        
+        
+        
+          
+            
+              
+                false
+                code
+                =
+                true
+                action
+              
+            
+          
+          
+          
+          
+          
+          
+          
+          /perf-app/perf-servlet
+          GET
+          true
+          false
+          true
+          false
+          false
+          
+        
+        
+          
+            
+              Log in to perf-realm
+            
+            Assertion.response_data
+            false
+            2
+          
+          
+        
+        
+          
+            
+              
+                false
+                ${username}
+                =
+                true
+                username
+              
+              
+                false
+                password
+                =
+                true
+                password
+              
+            
+          
+          
+          
+          
+          
+          
+          
+          /auth/realms/perf-realm/tokens/auth/request/login?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fperf-app%2Fperf-servlet&state=123&client_id=perf-app
+          POST
+          true
+          false
+          true
+          false
+          false
+          
+        
+        
+          
+            
+              RequestAction=Code retrieved
+            
+            Assertion.response_data
+            false
+            2
+          
+          
+          
+            false
+            code
+            Code=([\w\.]+)
+            $1$
+            
+            1
+          
+          
+        
+        
+          
+            
+          
+          
+          
+          
+          
+          
+          
+          /perf-app/perf-servlet?action=exchangeCode
+          GET
+          true
+          false
+          true
+          false
+          false
+          
+        
+        
+          
+            
+              RequestAction=Token retrieved
+              Username=${username}
+            
+            Assertion.response_data
+            false
+            16
+          
+          
+        
+        
+          true
+          ${refreshTokenRequestsPerIteration}
+        
+        
+          
+            
+              
+            
+            
+            
+            
+            
+            
+            
+            /perf-app/perf-servlet?action=refresh
+            GET
+            true
+            false
+            true
+            false
+            false
+            
+          
+          
+            
+              
+                RequestAction=Token refreshed
+              
+              Assertion.response_data
+              false
+              16
+            
+            
+          
+        
+        
+          
+            
+          
+          
+          
+          
+          
+          
+          
+          /perf-app/perf-servlet?action=logout
+          GET
+          true
+          false
+          true
+          false
+          false
+          
+        
+        
+          
+            
+              RequestAction=
+              Username=
+            
+            Assertion.response_data
+            false
+            20
+          
+          
+        
+      
+      
+        false
+        
+          
+            
+            true
+            true
+            true
+            
+            true
+            true
+            true
+            true
+            false
+            true
+            true
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            0
+            true
+          
+        
+        
+      
+      
+      
+        false
+        
+          
+            
+            true
+            true
+            true
+            
+            true
+            true
+            true
+            true
+            false
+            true
+            true
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            0
+            true
+          
+        
+        
+          
+            
+            true
+            true
+            true
+            
+            true
+            true
+            true
+            true
+            false
+            true
+            true
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            false
+            0
+            true
+          
+        
+        
+      
+      
+    
+  
+