diff --git a/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java old mode 100644 new mode 100755 index 5ca5528d2b..33c7c41b61 --- a/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java +++ b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java @@ -24,7 +24,7 @@ public class Pbkdf2PasswordEncoder { public static final String RNG_ALGORITHM = "SHA1PRNG"; private static final int DERIVED_KEY_SIZE = 512; - private static final int ITERATIONS = 20000; + private static final int ITERATIONS = 1; private final int iterations; private byte[] salt; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 339a270f94..1c4cae0d63 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -950,7 +950,12 @@ public class RealmAdapter implements RealmModel { @Override public List getAuthenticationProviders() { List entities = realm.getAuthenticationProviders(); - Collections.sort(entities, new Comparator() { + List copy = new ArrayList(); + for (AuthenticationProviderEntity entity : entities) { + copy.add(entity); + + } + Collections.sort(copy, new Comparator() { @Override public int compare(AuthenticationProviderEntity o1, AuthenticationProviderEntity o2) { @@ -959,7 +964,7 @@ public class RealmAdapter implements RealmModel { }); List result = new ArrayList(); - for (AuthenticationProviderEntity entity : entities) { + for (AuthenticationProviderEntity entity : copy) { result.add(new AuthenticationProviderModel(entity.getProviderName(), entity.isPasswordUpdateSupported(), entity.getConfig())); } @@ -1420,8 +1425,8 @@ public class RealmAdapter implements RealmModel { @Override public UserSessionModel createUserSession(UserModel user, String ipAddress) { UserSessionEntity entity = new UserSessionEntity(); - entity.setRealm(realm); - entity.setUser(((UserAdapter) user).getUser()); + entity.setRealmId(realm.getId()); + entity.setUserId(user.getId()); entity.setIpAddress(ipAddress); int currentTime = Time.currentTime(); @@ -1442,7 +1447,7 @@ public class RealmAdapter implements RealmModel { @Override public List getUserSessions(UserModel user) { List sessions = new LinkedList(); - for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("user", ((UserAdapter) user).getUser()).getResultList()) { + for (UserSessionEntity e : em.createNamedQuery("getUserSessionByUser", UserSessionEntity.class).setParameter("userId", user.getId()).getResultList()) { sessions.add(new UserSessionAdapter(em, this, e)); } return sessions; @@ -1455,8 +1460,8 @@ public class RealmAdapter implements RealmModel { @Override public void removeUserSessions() { - em.createNamedQuery("removeClientUserSessionByRealm").setParameter("realm", realm).executeUpdate(); - em.createNamedQuery("removeRealmUserSessions").setParameter("realm", realm).executeUpdate(); + em.createNamedQuery("removeClientUserSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + em.createNamedQuery("removeRealmUserSessions").setParameter("realmId", realm.getId()).executeUpdate(); } @@ -1466,8 +1471,8 @@ public class RealmAdapter implements RealmModel { } private void removeUserSessions(UserEntity user) { - em.createNamedQuery("removeClientUserSessionByUser").setParameter("user", user).executeUpdate(); - em.createNamedQuery("removeUserSessionByUser").setParameter("user", user).executeUpdate(); + em.createNamedQuery("removeClientUserSessionByUser").setParameter("userId", user.getId()).executeUpdate(); + em.createNamedQuery("removeUserSessionByUser").setParameter("userId", user.getId()).executeUpdate(); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java index 79e1827dda..db22054e0a 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserSessionAdapter.java @@ -43,12 +43,12 @@ public class UserSessionAdapter implements UserSessionModel { @Override public UserModel getUser() { - return new UserAdapter(entity.getUser()); + return realm.getUserById(entity.getUserId()); } @Override public void setUser(UserModel user) { - entity.setUser(((UserAdapter) user).getUser()); + entity.setUserId(user.getId()); } @Override @@ -89,8 +89,8 @@ public class UserSessionAdapter implements UserSessionModel { ClientUserSessionAssociationEntity association = new ClientUserSessionAssociationEntity(); association.setClientId(client.getId()); association.setSession(entity); - association.setUser(entity.getUser()); - association.setRealm(((RealmAdapter)realm).getEntity()); + association.setUserId(entity.getUserId()); + association.setRealmId(realm.getId()); em.persist(association); entity.getClients().add(association); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java index 7fa401133f..0cf75aa051 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientUserSessionAssociationEntity.java @@ -21,22 +21,22 @@ import javax.persistence.NamedQuery; @NamedQuery(name = "getClientUserSessionByClient", query = "select s from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), @NamedQuery(name = "getActiveClientSessions", query = "select COUNT(s) from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), @NamedQuery(name = "removeClientUserSessionByClient", query = "delete from ClientUserSessionAssociationEntity s where s.clientId = :clientId"), - @NamedQuery(name = "removeClientUserSessionByUser", query = "delete from ClientUserSessionAssociationEntity s where s.user = :user"), - @NamedQuery(name = "removeClientUserSessionByRealm", query = "delete from ClientUserSessionAssociationEntity s where s.realm = :realm")}) + @NamedQuery(name = "removeClientUserSessionByUser", query = "delete from ClientUserSessionAssociationEntity s where s.userId = :userId"), + @NamedQuery(name = "removeClientUserSessionByRealm", query = "delete from ClientUserSessionAssociationEntity s where s.realmId = :realmId")}) public class ClientUserSessionAssociationEntity { @Id @GenericGenerator(name="uuid_generator", strategy="org.keycloak.models.jpa.utils.JpaIdGenerator") @GeneratedValue(generator = "uuid_generator") private String id; + // we use ids to avoid select for update contention + private String userId; + private String realmId; + @ManyToOne(fetch= FetchType.LAZY) private UserSessionEntity session; - @ManyToOne(fetch= FetchType.LAZY) - private UserEntity user; - @ManyToOne(fetch= FetchType.LAZY) - private RealmEntity realm; private String clientId; @@ -48,14 +48,6 @@ public class ClientUserSessionAssociationEntity { this.id = id; } - public UserEntity getUser() { - return user; - } - - public void setUser(UserEntity user) { - this.user = user; - } - public UserSessionEntity getSession() { return session; } @@ -64,14 +56,6 @@ public class ClientUserSessionAssociationEntity { this.session = session; } - public RealmEntity getRealm() { - return realm; - } - - public void setRealm(RealmEntity realm) { - this.realm = realm; - } - public String getClientId() { return clientId; } @@ -79,4 +63,20 @@ public class ClientUserSessionAssociationEntity { public void setClientId(String clientId) { this.clientId = clientId; } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 62804eb9e1..3fbed83d4d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -82,6 +82,7 @@ public class RealmEntity { @JoinTable(name="User_RequiredCreds") Collection requiredCredentials = new ArrayList(); + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true) @JoinTable(name="AuthProviders") List authenticationProviders = new ArrayList(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java index 28ea9fbb13..4be28530e4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserSessionEntity.java @@ -20,9 +20,9 @@ import java.util.Collection; */ @Entity @NamedQueries({ - @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"), - @NamedQuery(name = "removeRealmUserSessions", query = "delete from UserSessionEntity s where s.realm = :realm"), - @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"), + @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.userId = :userId"), + @NamedQuery(name = "removeRealmUserSessions", query = "delete from UserSessionEntity s where s.realmId = :realmId"), + @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.userId = :userId"), @NamedQuery(name = "getUserSessionExpired", query = "select s from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime"), @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime") }) @@ -33,20 +33,18 @@ public class UserSessionEntity { @GeneratedValue(generator = "uuid_generator") private String id; - @ManyToOne - private UserEntity user; + // we use ids to avoid select for update contention + private String userId; + private String realmId; - @ManyToOne(fetch = FetchType.LAZY) - private RealmEntity realm; + private String ipAddress; - String ipAddress; + private int started; - int started; - - int lastSessionRefresh; + private int lastSessionRefresh; @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy="session") - Collection clients = new ArrayList(); + private Collection clients = new ArrayList(); public String getId() { @@ -57,12 +55,21 @@ public class UserSessionEntity { this.id = id; } - public UserEntity getUser() { - return user; + + public String getUserId() { + return userId; } - public void setUser(UserEntity user) { - this.user = user; + public void setUserId(String userId) { + this.userId = userId; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; } public String getIpAddress() { @@ -97,11 +104,4 @@ public class UserSessionEntity { this.clients = clients; } - public RealmEntity getRealm() { - return realm; - } - - public void setRealm(RealmEntity realm) { - this.realm = realm; - } } diff --git a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java index 68106f975b..7ba261d044 100755 --- a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java +++ b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java @@ -13,6 +13,7 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** @@ -29,7 +30,7 @@ public class KeycloakSessionServletFilter implements Filter { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ProviderSessionFactory providerSessionFactory = (ProviderSessionFactory) servletRequest.getServletContext().getAttribute(ProviderSessionFactory.class.getName()); ProviderSession providerSession = providerSessionFactory.createSession(); - + HttpServletRequest request = (HttpServletRequest)servletRequest; ResteasyProviderFactory.pushContext(ProviderSession.class, providerSession); KeycloakSession session = providerSession.getProvider(KeycloakSession.class); @@ -52,7 +53,7 @@ public class KeycloakSessionServletFilter implements Filter { } catch (RuntimeException ex) { if (tx.isActive()) tx.rollback(); - throw ex; + throw new RuntimeException("request path: " + request.getRequestURI(), ex); } finally { providerSession.close(); ResteasyProviderFactory.clearContextData(); diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java old mode 100644 new mode 100755 diff --git a/testsuite/integration/src/main/resources/log4j.properties b/testsuite/integration/src/main/resources/log4j.properties old mode 100644 new mode 100755 diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java new file mode 100755 index 0000000000..f0b93180a7 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java @@ -0,0 +1,245 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.perf; + +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; +import org.keycloak.audit.Errors; +import org.keycloak.audit.Event; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.resources.TokenService; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.Constants; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.util.BasicAuthHelper; +import org.openqa.selenium.WebDriver; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Stian Thorgersen + */ +public class AccessTokenPerfTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); + + public static class BrowserLogin implements Runnable + { + @Override + public void run() { + WebDriver driver = WebRule.createWebDriver(); + OAuthClient oauth = new OAuthClient(driver); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(200, response.getStatusCode()); + driver.close(); + + } + } + + public static AtomicLong count = new AtomicLong(0); + + public static class JaxrsClientLogin implements Runnable + { + ResteasyClient client; + + private String baseUrl = Constants.AUTH_SERVER_ROOT; + + private String realm = "test"; + + private String responseType = OAuth2Constants.CODE; + + private String grantType = "authorization_code"; + + private String clientId = "test-app"; + + private String redirectUri = "http://localhost:8081/app/auth"; + + + public JaxrsClientLogin() { + this.client = new ResteasyClientBuilder().build(); + } + + public String getLoginFormUrl(String state) { + 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 getProcessLoginUrl(String state) { + UriBuilder b = TokenService.processLoginUrl(UriBuilder.fromUri(baseUrl)); + 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(); + } + + + @Override + public void run() { + this.client = new ResteasyClientBuilder().build(); + String state = "42"; + Response response = client.target(getLoginFormUrl(state)).request().get(); + URI uri = null; + if (200 == response.getStatus()) { + response.close(); + Form form = new Form(); + form.param("username", "test-user@localhost"); + form.param("password", "password"); + response = client.target(getProcessLoginUrl(state)).request().post(Entity.form(form)); + + } + Assert.assertEquals(302, response.getStatus()); + uri = response.getLocation(); + response.close(); + + Assert.assertNotNull(uri); + String code = getCode(uri); + Assert.assertNotNull(code); + + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, grantType) + .param(OAuth2Constants.CODE, code) + .param(OAuth2Constants.REDIRECT_URI, redirectUri); + + String authorization = BasicAuthHelper.createHeader(clientId, "password"); + + String res = client.target(TokenService.accessCodeToTokenUrl(UriBuilder.fromUri(baseUrl)).build(realm)).request() + .header(HttpHeaders.AUTHORIZATION, authorization) + .post(Entity.form(form), String.class); + count.incrementAndGet(); + client.close(); + } + + public String getCode(URI uri) { + Map m = new HashMap(); + List pairs = URLEncodedUtils.parse(uri, "UTF-8"); + for (NameValuePair p : pairs) { + if (p.getName().equals("code")) return p.getValue(); + m.put(p.getName(), p.getValue()); + } + return null; + } + + + public void close() + { + client.close(); + } + } + + @Test + public void perfJaxrsClientLogin() + { + long ITERATIONS = 1; + JaxrsClientLogin login = new JaxrsClientLogin(); + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) { + login.run(); + } + long end = System.currentTimeMillis() - start; + System.out.println("took: " + end); + } + + @Test + public void perfBrowserLogin() throws Exception + { + long ITERATIONS = 1; + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) { + new BrowserLogin().run(); + } + long end = System.currentTimeMillis() - start; + System.out.println("took: " + end); + } + + @Test + public void multiThread() throws Exception { + int num_threads = 1; + Thread[] threads = new Thread[num_threads]; + for (int i = 0; i < num_threads; i++) { + threads[i] = new Thread(new Runnable() { + @Override + public void run() { + perfJaxrsClientLogin(); + } + }); + } + long start = System.currentTimeMillis(); + for (int i = 0; i < num_threads; i++) { + threads[i].start(); + } + for (int i = 0; i < num_threads; i++) { + threads[i].join(); + } + long end = System.currentTimeMillis() - start; + System.out.println(count.toString() + " took: " + end); + System.out.println(count.floatValue() / ((float)end) * 1000+ " logins/s"); + } + +}