keycloak-uncached

Changes

Details

diff --git a/examples/demo-template/angular2-product-app/pom.xml b/examples/demo-template/angular2-product-app/pom.xml
new file mode 100644
index 0000000..d4bd4d2
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-examples-demo-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.9.0.Final-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.keycloak.example.demo</groupId>
+    <artifactId>angular2-product-example</artifactId>
+    <packaging>war</packaging>
+    <name>Angular2 Product Portal JS</name>
+    <description/>
+
+    <build>
+        <finalName>angular2-product</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.jboss.as.plugins</groupId>
+                <artifactId>jboss-as-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.wildfly.plugins</groupId>
+                <artifactId>wildfly-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/app/app.ts b/examples/demo-template/angular2-product-app/src/main/webapp/app/app.ts
new file mode 100644
index 0000000..8630f00
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/app/app.ts
@@ -0,0 +1,78 @@
+import {Http, Headers,
+    RequestOptions, Response}   from 'angular2/http';
+import {Component}              from 'angular2/core';
+import {Observable}             from 'rxjs/Observable';
+import {KeycloakService}        from './keycloak';
+
+
+
+
+@Component({
+    selector: 'my-app',
+    template:
+`
+<div id="content-area" class="col-md-9" role="main">
+    <div id="content">
+        <h1>Angular2 Product (Beta)</h1>
+        <h2><span>Products</span></h2>
+       
+        <button type="button" (click)="logout()">Sign Out</button>
+        <button type="button" (click)="reloadData()">Reload</button>
+        <table class="table" [hidden]="!products.length">
+            <thead>
+            <tr>
+                <th>Product Listing</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr *ngFor="#p of products">
+                <td>{{p}}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+`
+})
+export class AppComponent {
+
+    constructor(private _kc:KeycloakService, private http:Http){ }
+
+    products : string[] = [];
+
+    logout(){
+        this._kc.logout();
+    }
+
+    reloadData() {
+        //angular dont have http interceptor yet
+
+        this._kc.getToken().then(
+            token=>{
+                let headers = new Headers({
+                        'Accept': 'application/json',
+                        'Authorization': 'Bearer ' + token
+                    });
+
+                let options = new RequestOptions({ headers: headers });
+
+                this.http.get('/database/products', options)
+                .map(res => <string[]> res.json())
+                .subscribe(
+                     prods => this.products = prods,
+                     error =>  console.log(error));
+
+            },
+            error=>{
+                console.log(error);
+            }
+        );
+
+    }
+
+    private handleError (error: Response) {
+        console.error(error);
+        return Observable.throw(error.json().error || 'Server error');
+    }
+
+}
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.ts b/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.ts
new file mode 100644
index 0000000..64242ae
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/app/keycloak.ts
@@ -0,0 +1,49 @@
+import {Injectable} from 'angular2/core';
+
+
+declare var Keycloak: any;
+
+@Injectable()
+export class KeycloakService {
+
+    static auth : any = {};
+
+    static init() : Promise<any>{
+        let keycloakAuth : any = new Keycloak('keycloak.json');
+        KeycloakService.auth.loggedIn = false;
+
+        return new Promise((resolve,reject)=>{
+            keycloakAuth.init({ onLoad: 'login-required' })
+                .success( () => {
+                    KeycloakService.auth.loggedIn = true;
+                    KeycloakService.auth.authz = keycloakAuth;
+                    KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl + "/realms/demo/tokens/logout?redirect_uri=/angular2-product/index.html";
+                    resolve(null);
+                })
+                .error(()=> {
+                    reject(null);
+                });
+        });
+    }
+
+    logout(){
+        console.log('*** LOGOUT');
+        KeycloakService.auth.loggedIn = false;
+        KeycloakService.auth.authz = null;
+
+        window.location.href = KeycloakService.auth.logoutUrl;
+    }
+
+    getToken(): Promise<string>{
+        return new Promise<string>((resolve,reject)=>{
+            if (KeycloakService.auth.authz.token) {
+                KeycloakService.auth.authz.updateToken(5).success(function() {
+                    resolve(<string>KeycloakService.auth.authz.token);
+                })
+                .error(function() {
+                    reject('Failed to refresh token');
+                });
+            }
+        });
+    }
+}
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/app/main.ts b/examples/demo-template/angular2-product-app/src/main/webapp/app/main.ts
new file mode 100644
index 0000000..73613b2
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/app/main.ts
@@ -0,0 +1,14 @@
+import 'rxjs/Rx';
+import {bootstrap}    from 'angular2/platform/browser';
+import {HTTP_BINDINGS} from 'angular2/http';
+import {KeycloakService} from './keycloak';
+import {AppComponent} from './app';
+
+KeycloakService.init().then(
+    o=>{
+        bootstrap(AppComponent,[HTTP_BINDINGS, KeycloakService]);
+    },
+    x=>{
+        window.location.reload();
+    }
+);
\ No newline at end of file
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/index.html b/examples/demo-template/angular2-product-app/src/main/webapp/index.html
new file mode 100644
index 0000000..2da600c
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/index.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Angular 2 QuickStart</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+
+
+
+
+
+  </head>
+
+  <!-- 3. Display the application -->
+  <body>
+    <my-app>Loading...</my-app>
+
+
+
+    <!-- 1. Load libraries -->
+    <!-- IE required polyfills, in this exact order -->
+    <script src="node_modules/es6-shim/es6-shim.min.js"></script>
+    <script src="node_modules/systemjs/dist/system-polyfills.js"></script>
+
+    <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
+    <script src="node_modules/systemjs/dist/system.src.js"></script>
+    <script src="node_modules/rxjs/bundles/Rx.js"></script>
+    <script src="node_modules/angular2/bundles/angular2.dev.js"></script>
+    <script src="node_modules/angular2/bundles/http.js"></script>
+
+
+    <script src="/auth/js/keycloak.js"></script>
+
+    <!-- 2. Configure SystemJS -->
+    <script>
+      System.config({
+        packages: {
+          app: {
+            format: 'register',
+            defaultExtension: 'js'
+          }
+        }
+      });
+      System.import('app/main')
+            .then(null, console.error.bind(console));
+    </script>
+  </body>
+
+</html>
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json b/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json
new file mode 100644
index 0000000..ddd1f2e
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json
@@ -0,0 +1,8 @@
+{
+  "realm": "demo",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url": "/auth",
+  "ssl-required": "external",
+  "resource": "angular2-product",
+  "public-client": true
+}
\ No newline at end of file
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/package.json b/examples/demo-template/angular2-product-app/src/main/webapp/package.json
new file mode 100644
index 0000000..f66b44c
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "angular2-product-app",
+  "version": "1.0.0",
+  "scripts": {
+    "tsc": "tsc",
+    "tsc:w": "tsc -w",
+    "lite": "lite-server",
+    "start": "concurrent \"npm run tsc:w\" \"npm run lite\" "
+  },
+  "license": "ISC",
+  "dependencies": {
+    "angular2": "2.0.0-beta.3",
+    "systemjs": "0.19.6",
+    "es6-promise": "^3.0.2",
+    "es6-shim": "^0.33.3",
+    "reflect-metadata": "0.1.2",
+    "rxjs": "5.0.0-beta.0",
+    "zone.js": "0.5.11"
+  },
+  "devDependencies": {
+    "concurrently": "^1.0.0",
+    "lite-server": "^2.0.1",
+    "typescript": "^1.7.5"
+  }
+}
diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/tsconfig.json b/examples/demo-template/angular2-product-app/src/main/webapp/tsconfig.json
new file mode 100644
index 0000000..52c77a5
--- /dev/null
+++ b/examples/demo-template/angular2-product-app/src/main/webapp/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "system",
+    "moduleResolution": "node",
+    "sourceMap": false,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "removeComments": false,
+    "noImplicitAny": false
+  },
+  "exclude": [
+    "node_modules"
+  ]
+}
\ No newline at end of file
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index fa46d6c..46211f9 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -202,7 +202,28 @@ An Angular JS example using Keycloak to secure it.
 If you are already logged in, you will not be asked for a username and password, but you will be redirected to
 an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
 
-Step 9: Pure HTML5/Javascript Example
+Step 10: Angular2 JS Example
+----------------------------------
+An Angular2 JS example using Keycloak to secure it. Angular2 is in beta version yet.
+
+To install angular2
+```
+$ cd keycloak/examples/demo-template/angular2-product-app/src/main/webapp/
+$ npm install
+```
+
+Transpile TypeScript to JavaScript before running the application.
+```
+$ npm run tsc
+```
+
+[http://localhost:8080/angular2-product](http://localhost:8080/angular2-product)
+
+If you are already logged in, you will not be asked for a username and password, but you will be redirected to
+an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
+
+
+Step 11: Pure HTML5/Javascript Example
 ----------------------------------
 An pure HTML5/Javascript example using Keycloak to secure it.
 
@@ -211,7 +232,7 @@ An pure HTML5/Javascript example using Keycloak to secure it.
 If you are already logged in, you will not be asked for a username and password, but you will be redirected to
 an oauth grant page.  This page asks you if you want to grant certain permissions to the third-part app.
 
-Step 10: Service Account Example
+Step 12: Service Account Example
 ================================
 An example for retrieve service account dedicated to the Client Application itself (not to any user). 
 
@@ -222,7 +243,7 @@ Client authentication is done with OAuth2 Client Credentials Grant in out-of-bou
 The example also shows different methods of client authentication. There is ProductSAClientSecretServlet using traditional authentication with clientId and client_secret, 
 but there is also ProductSAClientSignedJWTServlet using client authentication with JWT signed by client private key.  
 
-Step 11: Offline Access Example
+Step 13: Offline Access Example
 ===============================
 An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
 is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in 
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index 20cd615..fdebfc7 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -148,6 +148,15 @@
                 "/angular-product/*"
             ]
         },
+		{
+            "clientId": "angular2-product",
+            "enabled": true,
+            "publicClient": true,
+            "baseUrl": "/angular2-product/index.html",
+            "redirectUris": [
+                "/angular2-product/*"
+            ]
+        },
         {
             "clientId": "customer-portal-cli",
             "enabled": true,
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 9cf19c2..1fc04de 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -24,6 +24,7 @@ import org.keycloak.common.util.Time;
 import org.keycloak.models.*;
 import org.keycloak.models.session.UserSessionPersisterProvider;
 import org.keycloak.models.sessions.infinispan.entities.*;
+import org.keycloak.models.sessions.infinispan.initializer.TimeAwareInitializerState;
 import org.keycloak.models.sessions.infinispan.stream.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.RealmInfoUtil;
@@ -411,6 +412,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
+    public int getClusterStartupTime() {
+        TimeAwareInitializerState state = (TimeAwareInitializerState) offlineSessionCache.get(InfinispanUserSessionProviderFactory.SESSION_INITIALIZER_STATE_KEY);
+        int startTime;
+        if (state == null) {
+            log.warn("Cluster startup time not yet available. Fallback to local startup time");
+            startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+        } else {
+            startTime = state.getClusterStartupTime();
+        }
+        return startTime;
+    }
+
+    @Override
     public void close() {
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 7f20f0e..1bb82a1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -34,6 +34,9 @@ import org.keycloak.provider.ProviderEventListener;
 
 public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
 
+    private static final String STATE_KEY_PREFIX = "initializerState";
+    public static final String SESSION_INITIALIZER_STATE_KEY = STATE_KEY_PREFIX + "::offlineUserSessions";
+
     private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
 
     private Config.Scope config;
@@ -84,7 +87,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
                 InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
                 Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
 
-                InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
+                InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, SESSION_INITIALIZER_STATE_KEY);
                 initializer.initCache();
                 initializer.loadPersistentSessions();
             }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
index 2ce4ac2..deae897 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
@@ -35,6 +35,7 @@ import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
 import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
 import org.infinispan.remoting.transport.Transport;
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.KeycloakSessionTask;
@@ -51,8 +52,6 @@ public class InfinispanUserSessionInitializer {
 
     private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
 
-    private static final String STATE_KEY_PREFIX = "initializerState";
-
     private final KeycloakSessionFactory sessionFactory;
     private final Cache<String, SessionEntity> cache;
     private final SessionLoader sessionLoader;
@@ -60,21 +59,18 @@ public class InfinispanUserSessionInitializer {
     private final int sessionsPerSegment;
     private final String stateKey;
 
-    private volatile CountDownLatch latch = new CountDownLatch(1);
-
 
-    public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) {
+    public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKey) {
         this.sessionFactory = sessionFactory;
         this.cache = cache;
         this.sessionLoader = sessionLoader;
         this.maxErrors = maxErrors;
         this.sessionsPerSegment = sessionsPerSegment;
-        this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix;
+        this.stateKey = stateKey;
     }
 
     public void initCache() {
         this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
-        cache.getCacheManager().addListener(new ViewChangeListener());
     }
 
 
@@ -86,7 +82,7 @@ public class InfinispanUserSessionInitializer {
         while (!isFinished()) {
             if (!isCoordinator()) {
                 try {
-                    latch.await(500, TimeUnit.MILLISECONDS);
+                    Thread.sleep(1000);
                 } catch (InterruptedException ie) {
                     log.error("Interrupted", ie);
                 }
@@ -104,8 +100,10 @@ public class InfinispanUserSessionInitializer {
 
 
     private InitializerState getOrCreateInitializerState() {
-        InitializerState state = (InitializerState) cache.get(stateKey);
+        TimeAwareInitializerState state = (TimeAwareInitializerState) cache.get(stateKey);
         if (state == null) {
+            int startTime = (int)(sessionFactory.getServerStartupTimestamp() / 1000);
+
             final int[] count = new int[1];
 
             // Rather use separate transactions for update and counting
@@ -113,7 +111,7 @@ public class InfinispanUserSessionInitializer {
             KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
                 @Override
                 public void run(KeycloakSession session) {
-                    sessionLoader.init(session);
+                    sessionLoader.init(session, startTime);
                 }
 
             });
@@ -126,8 +124,9 @@ public class InfinispanUserSessionInitializer {
 
             });
 
-            state = new InitializerState();
+            state = new TimeAwareInitializerState();
             state.init(count[0], sessionsPerSegment);
+            state.setClusterStartupTime(startTime);
             saveStateToCache(state);
         }
         return state;
@@ -251,24 +250,6 @@ public class InfinispanUserSessionInitializer {
         }
     }
 
-
-    @Listener
-    public class ViewChangeListener {
-
-        @ViewChanged
-        public void viewChanged(ViewChangedEvent event) {
-            boolean isCoordinator = isCoordinator();
-            log.debug("View Changed: is coordinator: " + isCoordinator);
-
-            if (isCoordinator) {
-                latch.countDown();
-                latch = new CountDownLatch(1);
-            }
-        }
-
-    }
-
-
     public static class WorkerResult implements Serializable {
 
         private Integer segment;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
index c847d51..8b651c0 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
@@ -33,14 +33,13 @@ public class OfflineUserSessionLoader implements SessionLoader {
     private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
 
     @Override
-    public void init(KeycloakSession session) {
+    public void init(KeycloakSession session, int clusterStartupTime) {
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
-        int startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
 
-        log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", startTime);
+        log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", clusterStartupTime);
 
         persister.clearDetachedUserSessions();
-        persister.updateAllTimestamps(startTime);
+        persister.updateAllTimestamps(clusterStartupTime);
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
index 3185a39..b8aa0f8 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
@@ -26,7 +26,7 @@ import org.keycloak.models.KeycloakSession;
  */
 public interface SessionLoader extends Serializable {
 
-    void init(KeycloakSession session);
+    void init(KeycloakSession session, int clusterStartupTime);
 
     int getSessionsCount(KeycloakSession session);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/TimeAwareInitializerState.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/TimeAwareInitializerState.java
new file mode 100644
index 0000000..f5b7f04
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/TimeAwareInitializerState.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.initializer;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TimeAwareInitializerState extends InitializerState {
+
+    private int clusterStartupTime;
+
+    public int getClusterStartupTime() {
+        return clusterStartupTime;
+    }
+
+    public void setClusterStartupTime(int clusterStartupTime) {
+        this.clusterStartupTime = clusterStartupTime;
+    }
+}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-1.9.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-1.9.0.xml
index 99b47bc..11fd15e 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-1.9.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-1.9.0.xml
@@ -60,21 +60,18 @@
             <where>ACCESS_TOKEN_LIFE_IMPLICIT is NULL</where>
         </update>
 
-        <dropUniqueConstraint tableName="REALM_CLIENT" constraintName="UK_M6QGA3RFME47335JY8JXYXH3I"  />
         <dropForeignKeyConstraint baseTableName="REALM_CLIENT" constraintName="FK_93S3P0DIUXAWWQQSA528UBY2Q"/>
         <dropForeignKeyConstraint baseTableName="REALM_CLIENT" constraintName="FK_M6QGA3RFME47335JY8JXYXH3I"/>
-        <dropTable tableName="REALM_CLIENT" cascadeConstraints="true"/>
+        <dropUniqueConstraint tableName="REALM_CLIENT" constraintName="UK_M6QGA3RFME47335JY8JXYXH3I"  />
+        <dropTable tableName="REALM_CLIENT" />
         <dropForeignKeyConstraint baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_RLM" />
         <dropForeignKeyConstraint baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_CLI"/>
-        <dropTable tableName="REALM_CLIENT_TEMPLATE" cascadeConstraints="true"/>
+        <dropTable tableName="REALM_CLIENT_TEMPLATE" />
 
-        <dropUniqueConstraint tableName="FED_PROVIDERS" constraintName="UK_DCCIRJLIPU1478VQC89DID88C" />
         <dropForeignKeyConstraint baseTableName="FED_PROVIDERS" constraintName="FK_213LYQ09FKXQ8K8NY8DY3737T"/>
         <dropForeignKeyConstraint baseTableName="FED_PROVIDERS" constraintName="FK_DCCIRJLIPU1478VQC89DID88C"/>
-        <dropTable tableName="FED_PROVIDERS" cascadeConstraints="true"/>
-
-
-
+        <dropUniqueConstraint tableName="FED_PROVIDERS" constraintName="UK_DCCIRJLIPU1478VQC89DID88C" />
+        <dropTable tableName="FED_PROVIDERS" />
 
     </changeSet>
 </databaseChangeLog>
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index 57cd49c..9d7c413 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -82,6 +82,9 @@ public interface UserSessionProvider extends Provider {
     void removeClientInitialAccessModel(RealmModel realm, String id);
     List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
 
+    // Will use startup time of this server in non-cluster environment
+    int getClusterStartupTime();
+
     void close();
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 45dc043..e4174d9 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -193,9 +193,9 @@ public class TokenManager {
         int currentTime = Time.currentTime();
 
         if (realm.isRevokeRefreshToken()) {
-            int serverStartupTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+            int clusterStartupTime = session.sessions().getClusterStartupTime();
 
-            if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (serverStartupTime != validation.clientSession.getTimestamp())) {
+            if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (clusterStartupTime != validation.clientSession.getTimestamp())) {
                 throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
             }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 6fb06f6..4b49d79 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -91,7 +91,6 @@ public class KeycloakApplication extends Application {
         singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
 
         migrateModel();
-        sessionFactory.publish(new PostMigrationEvent());
 
         boolean bootstrapAdminUser = false;
 
@@ -138,6 +137,8 @@ public class KeycloakApplication extends Application {
             session.close();
         }
 
+        sessionFactory.publish(new PostMigrationEvent());
+
         singletons.add(new WelcomeResource(bootstrapAdminUser));
 
         setupScheduledTasks(sessionFactory);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
index 929de9b..2d763b6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
@@ -83,7 +83,7 @@ public class UserSessionInitializerTest {
 
         // Create and persist offline sessions
         int started = Time.currentTime();
-        int serverStartTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
+        int serverStartTime = session.sessions().getClusterStartupTime();
 
         for (UserSessionModel origSession : origSessions) {
             UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/InfinispanCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/InfinispanCLI.java
index 22b1026..b29b610 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/InfinispanCLI.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/InfinispanCLI.java
@@ -100,10 +100,9 @@ public class InfinispanCLI {
 
         BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
         String line;
+        System.out.print("$ ");
         try {
             while ((line = reader.readLine()) != null) {
-                log.info("Command: " + line);
-
                 String[] splits = line.split(" ");
                 String commandName = splits[0];
                 Class<? extends AbstractCommand> commandClass = commands.get(commandName);
@@ -128,6 +127,8 @@ public class InfinispanCLI {
                         log.error(ex);
                     }
                 }
+
+                System.out.print("$ ");
             }
         } finally {
             log.info("Exit infinispan CLI");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
index 950b9a4..9bde852 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java
@@ -18,14 +18,19 @@
 package org.keycloak.testsuite.util.cli;
 
 import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Set;
 
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -34,24 +39,59 @@ public class UserCommands {
 
     public static class Create extends AbstractCommand {
 
+        private String usernamePrefix;
+        private String password;
+        private String realmName;
+        private String roleNames;
+
         @Override
         public String getName() {
             return "createUsers";
         }
 
+        private class StateHolder {
+            int firstInThisBatch;
+            int countInThisBatch;
+            int remaining;
+        };
+
         @Override
         protected void doRunCommand(KeycloakSession session) {
-            String usernamePrefix = getArg(0);
-            String password = getArg(1);
-            String realmName = getArg(2);
+            usernamePrefix = getArg(0);
+            password = getArg(1);
+            realmName = getArg(2);
             int first = getIntArg(3);
             int count = getIntArg(4);
-            String roleNames = getArg(5);
+            int batchCount = getIntArg(5);
+            roleNames = getArg(6);
+
+            final StateHolder state = new StateHolder();
+            state.firstInThisBatch = first;
+            state.remaining = count;
+            state.countInThisBatch = Math.min(batchCount, state.remaining);
+            while (state.remaining > 0) {
+                KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
+
+                    @Override
+                    public void run(KeycloakSession session) {
+                        createUsersInBatch(session, state.firstInThisBatch, state.countInThisBatch);
+                    }
+                });
+
+                // update state
+                state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
+                state.remaining = state.remaining - state.countInThisBatch;
+                state.countInThisBatch = Math.min(batchCount, state.remaining);
+            }
+
+            log.infof("Command finished. All users from %s to %s created", usernamePrefix + first, usernamePrefix + (first + count - 1));
+        }
 
+        private void createUsersInBatch(KeycloakSession session, int first, int count) {
             RealmModel realm = session.realms().getRealmByName(realmName);
             if (realm == null) {
                 log.errorf("Unknown realm: %s", realmName);
-                return;
+                throw new HandledException();
             }
 
             Set<RoleModel> roles = findRoles(realm, roleNames);
@@ -74,8 +114,11 @@ public class UserCommands {
 
         @Override
         public String printUsage() {
-            return super.printUsage() + " <username-prefix> <password> <realm-name> <starting-user-offset> <count> <realm-roles-list>. \nRoles list is divided by comma (client roles not yet supported)>\n" +
-                    "Example usage: " + super.printUsage() + " test test demo 0 20 user,admin";
+            return super.printUsage() + " <username-prefix> <password> <realm-name> <starting-user-offset> <total-count> <batch-size> <realm-roles-list>. " +
+                    "\n'total-count' refers to total count of newly created users. 'batch-size' refers to number of created users in each transaction. 'starting-user-offset' refers to starting username offset." +
+                    "\nFor example if 'starting-user-offset' is 15 and total-count is 10 and username-prefix is 'test', it will create users test15, test16, test17, ... , test24" +
+                    "\nRoles list is divided by comma (client roles are referenced with format <client-id>/<role-name> )>\n" +
+                    "Example usage: " + super.printUsage() + " test test demo 0 500 100 user,admin";
         }
 
         private Set<RoleModel> findRoles(RealmModel realm, String rolesList) {
@@ -200,10 +243,25 @@ public class UserCommands {
             if (user == null) {
                 log.infof("User '%s' doesn't exist in realm '%s'", username, realmName);
             } else {
-                log.infof("User: ID: '%s', username: '%s', mail: '%s'", user.getId(), user.getUsername(), user.getEmail());
+                List<String> roleMappings = getRoleMappings(session, realm, user);
+                log.infof("User: ID: '%s', username: '%s', mail: '%s', roles: '%s'", user.getId(), user.getUsername(), user.getEmail(), roleMappings.toString());
             }
         }
 
+        private List<String> getRoleMappings(KeycloakSession session, RealmModel realm, UserModel user) {
+            Set<RoleModel> roles = user.getRoleMappings();
+            List<String> result = new LinkedList<>();
+            for (RoleModel role : roles) {
+                if (role.getContainer() instanceof RealmModel) {
+                    result.add(role.getName());
+                } else {
+                    ClientModel client = (ClientModel) role.getContainer();
+                    result.add(client.getClientId() + "/" + role.getName());
+                }
+            }
+            return result;
+        }
+
         @Override
         public String printUsage() {
             return super.printUsage() + " <realm-name> <username>";