keycloak-aplcache

Merge pull request #1653 from mposolda/master KEYCLOAK-904

9/30/2015 7:37:18 AM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java
new file mode 100644
index 0000000..b2374bc
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java
@@ -0,0 +1,35 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineClientSessionRepresentation {
+
+    private String clientSessionId;
+    private String client; // clientId (not DB ID)
+    private String data;
+
+    public String getClientSessionId() {
+        return clientSessionId;
+    }
+
+    public void setClientSessionId(String clientSessionId) {
+        this.clientSessionId = clientSessionId;
+    }
+
+    public String getClient() {
+        return client;
+    }
+
+    public void setClient(String client) {
+        this.client = client;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java
new file mode 100644
index 0000000..e877c3e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java
@@ -0,0 +1,37 @@
+package org.keycloak.representations.idm;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionRepresentation {
+
+    private String userSessionId;
+    private String data;
+    private List<OfflineClientSessionRepresentation> offlineClientSessions;
+
+    public String getUserSessionId() {
+        return userSessionId;
+    }
+
+    public void setUserSessionId(String userSessionId) {
+        this.userSessionId = userSessionId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+
+    public List<OfflineClientSessionRepresentation> getOfflineClientSessions() {
+        return offlineClientSessions;
+    }
+
+    public void setOfflineClientSessions(List<OfflineClientSessionRepresentation> offlineClientSessions) {
+        this.offlineClientSessions = offlineClientSessions;
+    }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index d724fd7..99f8f3c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -36,6 +36,7 @@ public class UserRepresentation {
     protected List<String> realmRoles;
     protected Map<String, List<String>> clientRoles;
     protected List<UserConsentRepresentation> clientConsents;
+    protected List<OfflineUserSessionRepresentation> offlineUserSessions;
 
     @Deprecated
     protected Map<String, List<String>> applicationRoles;
@@ -218,4 +219,12 @@ public class UserRepresentation {
     public void setServiceAccountClientId(String serviceAccountClientId) {
         this.serviceAccountClientId = serviceAccountClientId;
     }
+
+    public List<OfflineUserSessionRepresentation> getOfflineUserSessions() {
+        return offlineUserSessions;
+    }
+
+    public void setOfflineUserSessions(List<OfflineUserSessionRepresentation> offlineUserSessions) {
+        this.offlineUserSessions = offlineUserSessions;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
index 0a759a5..85c04a0 100644
--- a/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
+++ b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
@@ -3,6 +3,7 @@ package org.keycloak.util;
 import java.io.IOException;
 
 import org.keycloak.OAuth2Constants;
+import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.representations.RefreshToken;
 
 /**
@@ -30,7 +31,7 @@ public class RefreshTokenUtil {
 
 
     /**
-     * Return refresh token or offline otkne
+     * Return refresh token or offline token
      *
      * @param decodedToken
      * @return
@@ -39,9 +40,9 @@ public class RefreshTokenUtil {
         return JsonSerialization.readValue(decodedToken, RefreshToken.class);
     }
 
-    private static RefreshToken getRefreshToken(String refreshToken) throws IOException {
-        byte[] decodedToken = Base64Url.decode(refreshToken);
-        return getRefreshToken(decodedToken);
+    public static RefreshToken getRefreshToken(String refreshToken) throws IOException {
+        byte[] encodedContent = new JWSInput(refreshToken).getContent();
+        return getRefreshToken(encodedContent);
     }
 
     /**
diff --git a/docbook/reference/en/en-US/modules/timeouts.xml b/docbook/reference/en/en-US/modules/timeouts.xml
index 08e0450..78418cb 100755
--- a/docbook/reference/en/en-US/modules/timeouts.xml
+++ b/docbook/reference/en/en-US/modules/timeouts.xml
@@ -1,4 +1,4 @@
-    <chapter id="timeouts">
+<chapter id="timeouts">
     <title>Cookie settings, Session Timeouts, and Token Lifespans</title>
     <para>
         Keycloak has a bunch of fine-grain settings to manage browser cookies, user login sessions, and token lifespans.
@@ -52,4 +52,38 @@
             back to your application as an authentnicated user.  This value is relatively short and is usually measured in minutes.
         </para>
     </section>
+    <section id="offline-access">
+        <title>Offline Access</title>
+        <para>
+            The Offline access is the feature described in <ulink url="http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess">OpenID Connect specification</ulink> .
+            The idea is that during login, your client application will request Offline token instead of classic Refresh token.
+            Then the application can save this offline token in the database and can use it anytime later even if user is logged out.
+            This is useful for example if your application needs to do some "offline" actions on behalf of user even if user is not online. For example
+            periodic backup of some data every night etc.
+        </para>
+        <para>
+            Your application is responsible for persist the offline token in some storage (usually database) and then use it to
+            manually retrieve new access token from Keycloak server.
+        </para>
+        <para>
+            The difference between classic Refresh token and Offline token is, that offline token will never expire and is not subject of <literal>SSO Session Idle timeout</literal> .
+            The offline token is valid even after user logout or server restart. User can revoke the offline tokens in Account management UI. The admin
+            user can revoke offline tokens for individual users in admin console (The <literal>Consent</literal> tab of particular user) and he can
+            see all the offline tokens of all users for particular client application in the settings of the client. Revoking of all offline tokens for particular
+            client is possible by set <literal>notBefore</literal> policy for the client.
+        </para>
+        <para>
+            For requesting the offline token, user needs to be in realm role <literal>offline_access</literal> and client needs to have
+            scope for this role. If client has <literal>Full scope allowed</literal>, the scope is granted by default. Also users are automatically
+            members of the role as it's the default role.
+        </para>
+        <para>
+            The client can request offline token by adding parameter <literal>scope=offline_access</literal>
+            when sending authorization request to Keycloak. The adapter automatically adds this parameter when you use it to access secured
+            URL of your application (ie. http://localhost:8080/customer-portal/secured?scope=offline_access ).
+            The <link linkend='direct-access-grants'>Direct Access Grant</link> or <link linkend="service-accounts">Service account</link> flows also support
+            offline tokens if you include <literal>scope=offline_access</literal> in the body of the authentication request. For more details,
+            see the <literal>offline-access-app</literal> example from Keycloak demo.
+        </para>
+    </section>
 </chapter>
\ No newline at end of file
diff --git a/examples/demo-template/customer-app/src/main/webapp/index.html b/examples/demo-template/customer-app/src/main/webapp/index.html
old mode 100755
new mode 100644
diff --git a/examples/demo-template/offline-access-app/pom.xml b/examples/demo-template/offline-access-app/pom.xml
new file mode 100644
index 0000000..f6a6020
--- /dev/null
+++ b/examples/demo-template/offline-access-app/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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.6.0.Final-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.keycloak.example.demo</groupId>
+    <artifactId>offline-access-example</artifactId>
+    <packaging>war</packaging>
+    <name>Offline Access Portal</name>
+    <description/>
+
+    <repositories>
+        <repository>
+            <id>jboss</id>
+            <name>jboss repo</name>
+            <url>http://repository.jboss.org/nexus/content/groups/public/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-spi</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>offline-access-portal</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/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
new file mode 100644
index 0000000..b233574
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
@@ -0,0 +1,226 @@
+package org.keycloak.example;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.security.cert.X509Certificate;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.KeycloakUriBuilder;
+import org.keycloak.util.RefreshTokenUtil;
+import org.keycloak.util.StreamUtil;
+import org.keycloak.util.Time;
+import org.keycloak.util.UriUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineAccessPortalServlet extends HttpServlet {
+
+
+    @Override
+    public void init() throws ServletException {
+        getServletContext().setAttribute(HttpClient.class.getName(), new DefaultHttpClient());
+    }
+
+    @Override
+    public void destroy() {
+        getHttpClient().getConnectionManager().shutdown();
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+        if (req.getRequestURI().endsWith("/login")) {
+            storeToken(req);
+            req.getRequestDispatcher("/WEB-INF/pages/loginCallback.jsp").forward(req, resp);
+            return;
+        }
+
+        String refreshToken = RefreshTokenDAO.loadToken();
+        String refreshTokenInfo;
+        boolean savedTokenAvailable;
+        if (refreshToken == null) {
+            refreshTokenInfo = "No token saved in database. Please login first";
+            savedTokenAvailable = false;
+        } else {
+            RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken);
+            String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString();
+            refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp);
+            savedTokenAvailable = true;
+        }
+        req.setAttribute("tokenInfo", refreshTokenInfo);
+        req.setAttribute("savedTokenAvailable", savedTokenAvailable);
+
+        String customers;
+        if (req.getRequestURI().endsWith("/loadCustomers")) {
+            customers = loadCustomers(req, refreshToken);
+        } else {
+            customers = "";
+        }
+        req.setAttribute("customers", customers);
+
+        req.getRequestDispatcher("/WEB-INF/pages/view.jsp").forward(req, resp);
+    }
+
+    private void storeToken(HttpServletRequest req) throws IOException {
+        RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+        String refreshToken = ctx.getRefreshToken();
+
+        RefreshTokenDAO.saveToken(refreshToken);
+
+        RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken);
+        Boolean isOfflineToken = refreshTokenDecoded.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+        req.setAttribute("isOfflineToken", isOfflineToken);
+    }
+
+    private String loadCustomers(HttpServletRequest req, String refreshToken) throws ServletException, IOException {
+        // Retrieve accessToken first with usage of refresh (offline) token from DB
+        String accessToken = null;
+        try {
+            KeycloakDeployment deployment = getDeployment(req);
+            AccessTokenResponse response = ServerRequest.invokeRefresh(deployment, refreshToken);
+            accessToken = response.getToken();
+        } catch (ServerRequest.HttpFailure failure) {
+            return "Failed to refresh token. Status from auth-server request: " + failure.getStatus() + ", Error: " + failure.getError();
+        }
+
+        // Load customers now
+        HttpGet get = new HttpGet(UriUtils.getOrigin(req.getRequestURL().toString()) + "/database/customers");
+        get.addHeader("Authorization", "Bearer " + accessToken);
+
+        HttpResponse response = getHttpClient().execute(get);
+        InputStream is = response.getEntity().getContent();
+        try {
+            if (response.getStatusLine().getStatusCode() != 200) {
+                return "Error when loading customer. Status: " + response.getStatusLine().getStatusCode() + ", error: " + StreamUtil.readString(is);
+            } else {
+                List<String> list = JsonSerialization.readValue(is, TypedList.class);
+                StringBuilder result = new StringBuilder();
+                for (String customer : list) {
+                    result.append(customer + "<br />");
+                }
+                return result.toString();
+            }
+        } finally {
+            is.close();
+        }
+    }
+
+
+    private KeycloakDeployment getDeployment(HttpServletRequest servletRequest) throws ServletException {
+        // The facade object is needed just if you have relative "auth-server-url" in keycloak.json. Otherwise you can call deploymentContext.resolveDeployment(null)
+        HttpFacade facade = getFacade(servletRequest);
+
+        AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) getServletContext().getAttribute(AdapterDeploymentContext.class.getName());
+        if (deploymentContext == null) {
+            throw new ServletException("AdapterDeploymentContext not set");
+        }
+        return deploymentContext.resolveDeployment(facade);
+    }
+
+    // TODO: Merge with facade in ServletOAuthClient and move to some common servlet adapter
+    private HttpFacade getFacade(final HttpServletRequest servletRequest) {
+        return new HttpFacade() {
+
+            @Override
+            public Request getRequest() {
+                return new Request() {
+
+                    @Override
+                    public String getMethod() {
+                        return servletRequest.getMethod();
+                    }
+
+                    @Override
+                    public String getURI() {
+                        return servletRequest.getRequestURL().toString();
+                    }
+
+                    @Override
+                    public boolean isSecure() {
+                        return servletRequest.isSecure();
+                    }
+
+                    @Override
+                    public String getQueryParamValue(String param) {
+                        return servletRequest.getParameter(param);
+                    }
+
+                    @Override
+                    public String getFirstParam(String param) {
+                        return servletRequest.getParameter(param);
+                    }
+
+                    @Override
+                    public Cookie getCookie(String cookieName) {
+                        // not needed
+                        return null;
+                    }
+
+                    @Override
+                    public String getHeader(String name) {
+                        return servletRequest.getHeader(name);
+                    }
+
+                    @Override
+                    public List<String> getHeaders(String name) {
+                        // not needed
+                        return null;
+                    }
+
+                    @Override
+                    public InputStream getInputStream() {
+                        try {
+                            return servletRequest.getInputStream();
+                        } catch (IOException ioe) {
+                            throw new RuntimeException(ioe);
+                        }
+                    }
+
+                    @Override
+                    public String getRemoteAddr() {
+                        return servletRequest.getRemoteAddr();
+                    }
+                };
+            }
+
+            @Override
+            public Response getResponse() {
+                throw new IllegalStateException("Not yet implemented");
+            }
+
+            @Override
+            public X509Certificate[] getCertificateChain() {
+                throw new IllegalStateException("Not yet implemented");
+            }
+        };
+    }
+
+    private HttpClient getHttpClient() {
+        return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName());
+    }
+
+    static class TypedList extends ArrayList<String> {
+    }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java
new file mode 100644
index 0000000..0e56c71
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java
@@ -0,0 +1,27 @@
+package org.keycloak.example;
+
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.util.KeycloakUriBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineExampleUris {
+
+
+    public static final String LOGIN_CLASSIC = "/offline-access-portal/app/login";
+
+
+    public static final String LOGIN_WITH_OFFLINE_TOKEN = "/offline-access-portal/app/login?scope=offline_access";
+
+
+    public static final String LOAD_CUSTOMERS = "/offline-access-portal/app/loadCustomers";
+
+
+    public static final String ACCOUNT_MGMT = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH + "/applications")
+            .queryParam("referrer", "offline-access-portal").build("demo").toString();
+
+
+    public static final String LOGOUT = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+            .queryParam("redirect_uri", "/offline-access-portal").build("demo").toString();
+}
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java
new file mode 100644
index 0000000..2c94e19
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java
@@ -0,0 +1,47 @@
+package org.keycloak.example;
+
+import java.io.BufferedWriter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import org.keycloak.util.StreamUtil;
+
+/**
+ * Very simple DAO, which stores/loads just one token per whole application into file in tmp directory. Useful just for example purposes.
+ * In real environment, token should be stored in database.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RefreshTokenDAO {
+
+    public static final String FILE = System.getProperty("java.io.tmpdir") + "/offline-access-portal";
+
+    public static void saveToken(final String token) throws IOException {
+        PrintWriter writer = null;
+        try {
+            writer = new PrintWriter(new BufferedWriter(new FileWriter(FILE)));
+            writer.print(token);
+        } finally {
+            if (writer != null) {
+                writer.close();
+            }
+        }
+    }
+
+    public static String loadToken() throws IOException {
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(FILE);
+            return StreamUtil.readString(fis);
+        } catch (FileNotFoundException fnfe) {
+            return null;
+        } finally {
+            if (fis != null) {
+                fis.close();
+            }
+        }
+    }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/index.html b/examples/demo-template/offline-access-app/src/main/webapp/index.html
new file mode 100644
index 0000000..258d8f0
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/index.html
@@ -0,0 +1,3 @@
+<script>
+    window.location = "/offline-access-portal/app";
+</script>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..1abcf3f
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -0,0 +1,10 @@
+<jboss-deployment-structure>
+    <deployment>
+        <dependencies>
+            <!-- the Demo code uses classes in these modules.  These are optional to import if you are not using
+                 Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
+            <module name="org.apache.httpcomponents"/>
+            <module name="org.keycloak.keycloak-adapter-spi"/>
+        </dependencies>
+    </deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..dff976c
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm": "demo",
+  "resource": "offline-access-portal",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url": "/auth",
+  "ssl-required" : "external",
+  "credentials": {
+    "secret": "password"
+  }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp
new file mode 100644
index 0000000..31749f0
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp
@@ -0,0 +1,35 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+         pageEncoding="ISO-8859-1" %>
+<%@ page session="false" %>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+    <head>
+        <title>Offline Access Example</title>
+    </head>
+    <body bgcolor="#ffffff">
+        <h1>Offline Access Example</h1>
+
+        <hr />
+
+        <p>
+            Login finished and refresh token saved successfully.
+        </p>
+
+        <p>
+            <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+                <% if ((Boolean) request.getAttribute("isOfflineToken")) { %>
+                    Token type <b>is</b> offline token! You will be able to load customers even after logout or server restart. Offline token can be revoked in account management or by admin in admin console.
+                <% } else { %>
+                    Token <b>is not</b> offline token! Once you logout or restart server, token won't be valid anymore and you won't be able to load customers.
+                <% } %>
+            </div>
+        </p>
+
+        <p>
+            <a href="/offline-access-portal/app">Back to home page</a>
+        </p>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp
new file mode 100644
index 0000000..135a944
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp
@@ -0,0 +1,45 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+         pageEncoding="ISO-8859-1" %>
+<%@ page import="org.keycloak.example.OfflineExampleUris" %>
+<%@ page session="false" %>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+    <head>
+        <title>Offline Access Example</title>
+    </head>
+    <body bgcolor="#ffffff">
+        <h1>Offline Access Example</h1>
+
+        <hr />
+
+        <% if (request.getRemoteUser() == null) { %>
+            <a href="<%= OfflineExampleUris.LOGIN_CLASSIC %>">Login classic</a> |
+            <a href="<%= OfflineExampleUris.LOGIN_WITH_OFFLINE_TOKEN %>">Login with offline access</a> |
+        <% } else { %>
+            <a href='<%= OfflineExampleUris.LOGOUT %>'>Logout</a> |
+        <% } %>
+
+        <a href='<%= OfflineExampleUris.ACCOUNT_MGMT %>'>Account management</a> |
+
+        <% if ((Boolean) request.getAttribute("savedTokenAvailable")) { %>
+            <a href="<%= OfflineExampleUris.LOAD_CUSTOMERS %>">Load customers with saved token</a> |
+        <% } %>
+
+        <hr />
+
+        <h2>Saved Refresh Token Info</h2>
+        <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+            <%= request.getAttribute("tokenInfo") %>
+        </div>
+
+        <hr />
+
+        <h2>Customers</h2>
+        <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+            <%= request.getAttribute("customers") %>
+        </div>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..2092b4e
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>offline-access-portal</module-name>
+
+    <servlet>
+        <servlet-name>OfflineAccessPortalServle</servlet-name>
+        <servlet-class>org.keycloak.example.OfflineAccessPortalServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>OfflineAccessPortalServle</servlet-name>
+        <url-pattern>/app/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>User</web-resource-name>
+            <url-pattern>/app/login/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>user</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <!--
+    <security-constraint>
+        <web-resource-collection>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <user-data-constraint>
+            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+        </user-data-constraint>
+    </security-constraint> -->
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
\ No newline at end of file
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index 347a483..bc0011d 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -37,6 +37,7 @@
         <module>third-party</module>
         <module>third-party-cdi</module>
         <module>service-account</module>
+        <module>offline-access-app</module>
     </modules>
 
     <profiles>
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 2445e31..35b6db8 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -216,7 +216,19 @@ An example for retrieve service account dedicated to the Client Application itse
 
 [http://localhost:8080/service-account-portal](http://localhost:8080/service-account-portal)
 
-Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) 
+Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
+
+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
+===============================
+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 
+account management or by admin in admin console.
+
+[http://localhost:8080/offline-access-portal](http://localhost:8080/offline-access-portal)
+
 
 Admin Console
 ==========================
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
index 58c5f9b..ce5ff79 100644
--- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
@@ -1,6 +1,9 @@
 package org.keycloak.example;
 
 /**
+ * Client authentication based on JWT signed by client private key .
+ * See Keycloak documentation and <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
+ *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index 0ba235f..309afdd 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -22,7 +22,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access"  ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -37,7 +37,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access"  ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -52,7 +52,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access" ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -103,6 +103,10 @@
         {
             "client": "third-party",
             "roles": ["user"]
+        },
+        {
+            "client": "offline-access-portal",
+            "roles": ["user", "offline_access"]
         }
     ],
     "clients": [
@@ -190,6 +194,17 @@
             "attributes": {
                 "jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
             }
+        },
+        {
+            "clientId": "offline-access-portal",
+            "enabled": true,
+            "consentRequired": true,
+            "adminUrl": "/offline-access-portal",
+            "baseUrl": "/offline-access-portal",
+            "redirectUris": [
+                "/offline-access-portal/*"
+            ],
+            "secret": "password"
         }
     ],
     "clientScopeMappings": {
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 5137491..88471d0 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -1,5 +1,8 @@
 package org.keycloak.exportimport.util;
 
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
 import org.keycloak.util.Base64;
 import org.codehaus.jackson.JsonEncoding;
 import org.codehaus.jackson.JsonFactory;
@@ -295,6 +298,28 @@ public class ExportUtils {
             }
         }
 
+        // Offline sessions
+        List<OfflineUserSessionRepresentation> offlineSessionReps = new LinkedList<>();
+        Collection<OfflineUserSessionModel> offlineSessions = session.users().getOfflineUserSessions(realm, user);
+        Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, user);
+
+        Map<String, List<OfflineClientSessionModel>> processed = new HashMap<>();
+        for (OfflineClientSessionModel clsm : offlineClientSessions) {
+            String userSessionId = clsm.getUserSessionId();
+            List<OfflineClientSessionModel> current = processed.get(userSessionId);
+            if (current == null) {
+                current = new LinkedList<>();
+                processed.put(userSessionId, current);
+            }
+            current.add(clsm);
+        }
+
+        for (OfflineUserSessionModel userSession : offlineSessions) {
+            OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId()));
+            offlineSessionReps.add(sessionRep);
+        }
+        userRep.setOfflineUserSessions(offlineSessionReps);
+
         return userRep;
     }
 
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index a4b3771..f782543 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -202,7 +202,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
                 attributes.put("sessions", new SessionsBean(realm, sessions));
                 break;
             case APPLICATIONS:
-                attributes.put("applications", new ApplicationsBean(realm, user));
+                attributes.put("applications", new ApplicationsBean(session, realm, user));
                 attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
                 break;
             case PASSWORD:
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
index 371b5d0..71bf200 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
@@ -6,6 +6,7 @@ import java.util.List;
 import java.util.Set;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
@@ -22,9 +23,9 @@ public class ApplicationsBean {
 
     private List<ApplicationEntry> applications = new LinkedList<ApplicationEntry>();
 
-    public ApplicationsBean(RealmModel realm, UserModel user) {
+    public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
 
-        Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(realm, user);
+        Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
 
         List<ClientModel> realmClients = realm.getClients();
         for (ClientModel client : realmClients) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 350f0d0..a965325 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -637,6 +637,21 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'ClientSessionsCtrl'
         })
+        .when('/realms/:realm/clients/:client/offline-access', {
+            templateUrl : resourceUrl + '/partials/client-offline-sessions.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                client : function(ClientLoader) {
+                    return ClientLoader();
+                },
+                offlineSessionCount : function(ClientOfflineSessionCountLoader) {
+                    return ClientOfflineSessionCountLoader();
+                }
+            },
+            controller : 'ClientOfflineSessionsCtrl'
+        })
         .when('/realms/:realm/clients/:client/credentials', {
             templateUrl : resourceUrl + '/partials/client-credentials.html',
             resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 3cb3e60..27a9794 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -507,6 +507,54 @@ module.controller('ClientSessionsCtrl', function($scope, realm, sessionCount, cl
     };
 });
 
+module.controller('ClientOfflineSessionsCtrl', function($scope, realm, offlineSessionCount, client,
+                                                      ClientOfflineSessions) {
+    $scope.realm = realm;
+    $scope.count = offlineSessionCount.count;
+    $scope.sessions = [];
+    $scope.client = client;
+
+    $scope.page = 0;
+
+    $scope.query = {
+        realm : realm.realm,
+        client: $scope.client.id,
+        max : 5,
+        first : 0
+    }
+
+    $scope.firstPage = function() {
+        $scope.query.first = 0;
+        if ($scope.query.first < 0) {
+            $scope.query.first = 0;
+        }
+        $scope.loadUsers();
+    }
+
+    $scope.previousPage = function() {
+        $scope.query.first -= parseInt($scope.query.max);
+        if ($scope.query.first < 0) {
+            $scope.query.first = 0;
+        }
+        $scope.loadUsers();
+    }
+
+    $scope.nextPage = function() {
+        $scope.query.first += parseInt($scope.query.max);
+        $scope.loadUsers();
+    }
+
+    $scope.toDate = function(val) {
+        return new Date(val);
+    };
+
+    $scope.loadUsers = function() {
+        ClientOfflineSessions.query($scope.query, function(updated) {
+            $scope.sessions = updated;
+        })
+    };
+});
+
 module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role, roles, clients,
                                                         Role, ClientRole, RoleById, RoleRealmComposites, RoleClientComposites,
                                                         $http, $location, Dialog, Notifications) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index b2a4870..3746740 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -208,9 +208,9 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents
             UserConsents.query({realm: realm.realm, user: user.id}, function(updated) {
                 $scope.userConsents = updated;
             })
-            Notifications.success('Consent revoked successfully');
+            Notifications.success('Grant revoked successfully');
         }, function() {
-            Notifications.error("Consent couldn't be revoked");
+            Notifications.error("Grant couldn't be revoked");
         });
         console.log("Revoke consent " + clientId);
     }
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 6b09bc1..9c05940 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -246,6 +246,15 @@ module.factory('ClientSessionCountLoader', function(Loader, ClientSessionCount, 
     });
 });
 
+module.factory('ClientOfflineSessionCountLoader', function(Loader, ClientOfflineSessionCount, $route, $q) {
+    return Loader.get(ClientOfflineSessionCount, function() {
+        return {
+            realm : $route.current.params.realm,
+            client : $route.current.params.client
+        }
+    });
+});
+
 module.factory('ClientClaimsLoader', function(Loader, ClientClaims, $route, $q) {
     return Loader.get(ClientClaims, function() {
         return {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 8d56c90..03f4b99 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -841,6 +841,20 @@ module.factory('ClientUserSessions', function($resource) {
     });
 });
 
+module.factory('ClientOfflineSessionCount', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/clients/:client/offline-session-count', {
+        realm : '@realm',
+        client : "@client"
+    });
+});
+
+module.factory('ClientOfflineSessions', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/clients/:client/offline-sessions', {
+        realm : '@realm',
+        client : "@client"
+    });
+});
+
 module.factory('ClientLogoutAll', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/clients/:client/logout-all', {
         realm : '@realm',
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
new file mode 100644
index 0000000..c8afb02
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
@@ -0,0 +1,57 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+        <li>{{client.clientId}}</li>
+    </ol>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <form class="form-horizontal" name="sessionStats">
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="activeSessions">Offline Tokens</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="count" ng-disabled="true">
+                </div>
+                <kc-tooltip>Total number of active offline tokens for this client.</kc-tooltip>
+            </div>
+        </fieldset>
+    </form>
+    <table class="table table-striped table-bordered" data-ng-show="count > 0">
+        <thead>
+        <tr>
+            <th class="kc-table-actions" colspan="3">
+                <div class="pull-right">
+                    <a class="btn btn-default" ng-click="loadUsers()" tooltip-placement="left" tooltip-trigger="mouseover mouseout" tooltip="Warning, this is a potentially expensive operation depending on number of offline tokens.">Show Offline Tokens</a>
+                </div>
+            </th>
+        </tr>
+        <tr data-ng-show="sessions">
+            <th>User</th>
+            <th>From IP</th>
+            <th>Token Issued</th>
+        </tr>
+        </thead>
+        <tfoot data-ng-show="sessions && (sessions.length >= 5 || query.first != 0)">
+        <tr>
+            <td colspan="7">
+                <div class="table-nav">
+                    <button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">First page</button>
+                    <button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">Previous page</button>
+                    <button data-ng-click="nextPage()" class="next" ng-disabled="sessions.length < query.max">Next page</button>
+                </div>
+            </td>
+        </tr>
+        </tfoot>
+        <tbody>
+        <tr data-ng-repeat="session in sessions">
+            <td><a href="#/realms/{{realm.realm}}/users/{{session.userId}}">{{session.username}}</a></td>
+            <td>{{session.ipAddress}}</td>
+            <td>{{session.start | date:'medium'}}</td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
index 22db348..cf6db99 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
@@ -12,6 +12,7 @@
             <th>Client</th>
             <th>Granted Roles</th>
             <th>Granted Protocol Mappers</th>
+            <th>Additional Grants</th>
             <th>Action</th>
         </tr>
         </thead>
@@ -35,6 +36,11 @@
                     </span>
                 </span>
             </td>
+            <td>
+                <span data-ng-repeat="additionalGrant in consent.additionalGrants">
+                    <span ng-if="!$first">, </span>{{additionalGrant}}
+                </span>
+            </td>
             <td class="kc-action-cell">
                 <button class="btn btn-default btn-block btn-sm" ng-click="revokeConsent(consent.clientId)">
                     <i class="pficon pficon-delete"></i> Revoke
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 2aad2d5..f033aac 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -26,6 +26,13 @@
             <kc-tooltip>View active sessions for this client.  Allows you to see which users are active and when they logged in.</kc-tooltip>
         </li>
 
+        <li ng-class="{active: path[4] == 'offline-access'}" data-ng-show="!client.bearerOnly">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/offline-access">Offline Access</a>
+            <kc-tooltip>View offline sessions for this client.  Allows you to see which users retrieve offline token and when they retrieve it.
+                To revoke all tokens for the client, go to Revocation tab and set new not before value.
+            </kc-tooltip>
+        </li>
+
         <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
 
         <li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
index 8010586..e2db902 100644
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
@@ -3,8 +3,10 @@ package org.keycloak.migration.migrators;
 import java.util.List;
 
 import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 /**
@@ -17,6 +19,16 @@ public class MigrateTo1_6_0 {
     public void migrate(KeycloakSession session) {
         List<RealmModel> realms = session.realms().getRealms();
         for (RealmModel realm : realms) {
+
+            for (RoleModel realmRole : realm.getRoles()) {
+                realmRole.setScopeParamRequired(false);
+            }
+            for (ClientModel client : realm.getClients()) {
+                for (RoleModel clientRole : client.getRoles()) {
+                    clientRole.setScopeParamRequired(false);
+                }
+            }
+
             KeycloakModelUtils.setupOfflineTokens(realm);
         }
 
diff --git a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
index 47ae23a..59e325d 100644
--- a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
@@ -8,6 +8,7 @@ public class OfflineClientSessionModel {
     private String clientSessionId;
     private String userSessionId;
     private String clientId;
+    private String userId;
     private String data;
 
     public String getClientSessionId() {
@@ -34,6 +35,14 @@ public class OfflineClientSessionModel {
         this.clientId = clientId;
     }
 
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
     public String getData() {
         return data;
     }
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 13c05f8..0500308 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -475,6 +476,70 @@ public class UserFederationManager implements UserProvider {
     }
 
     @Override
+    public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        session.userStorage().addOfflineUserSession(realm, user, offlineUserSession);
+    }
+
+    @Override
+    public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().getOfflineUserSession(realm, user, userSessionId);
+    }
+
+    @Override
+    public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().getOfflineUserSessions(realm, user);
+    }
+
+    @Override
+    public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().removeOfflineUserSession(realm, user, userSessionId);
+    }
+
+    @Override
+    public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+        session.userStorage().addOfflineClientSession(realm, offlineClientSession);
+    }
+
+    @Override
+    public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().getOfflineClientSession(realm, user, clientSessionId);
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().getOfflineClientSessions(realm, user);
+    }
+
+    @Override
+    public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        validateUser(realm, user);
+        if (user == null) throw new IllegalStateException("Federated user no longer valid");
+        return session.userStorage().removeOfflineClientSession(realm, user, clientSessionId);
+    }
+
+    @Override
+    public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+        return session.userStorage().getOfflineClientSessionsCount(realm, client);
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        return session.userStorage().getOfflineClientSessions(realm, client, firstResult, maxResults);
+    }
+
+    @Override
     public void close() {
     }
 }
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 51e8fc4..f57bb04 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -114,15 +114,6 @@ public interface UserModel {
     void updateConsent(UserConsentModel consent);
     boolean revokeConsentForClient(String clientInternalId);
 
-    void addOfflineUserSession(OfflineUserSessionModel offlineUserSession);
-    OfflineUserSessionModel getOfflineUserSession(String userSessionId);
-    Collection<OfflineUserSessionModel> getOfflineUserSessions();
-    boolean removeOfflineUserSession(String userSessionId);
-    void addOfflineClientSession(OfflineClientSessionModel offlineClientSession);
-    OfflineClientSessionModel getOfflineClientSession(String clientSessionId);
-    Collection<OfflineClientSessionModel> getOfflineClientSessions();
-    boolean removeOfflineClientSession(String clientSessionId);
-
     public static enum RequiredAction {
         VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
     }
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index 8712f2b..0012b24 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -2,6 +2,7 @@ package org.keycloak.models;
 
 import org.keycloak.provider.Provider;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -55,5 +56,17 @@ public interface UserProvider extends Provider {
     boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
     CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
 
+    void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession);
+    OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
+    Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user);
+    boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
+    void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession);
+    OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
+    Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user);
+    boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
+
+    int getOfflineClientSessionsCount(RealmModel realm, ClientModel client);
+    Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int first, int max);
+
     void close();
 }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 9906ab2..6b19606 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -10,6 +10,8 @@ import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredActionProviderModel;
@@ -30,6 +32,8 @@ import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
 import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
@@ -43,6 +47,7 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
 import org.keycloak.util.Time;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -506,7 +511,31 @@ public class ModelToRepresentation {
         return rep;
     }
 
+    public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, OfflineUserSessionModel model, Collection<OfflineClientSessionModel> clientSessions) {
+        OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation();
+        rep.setData(model.getData());
+        rep.setUserSessionId(model.getUserSessionId());
 
+        List<OfflineClientSessionRepresentation> clientSessionReps = new LinkedList<>();
+        for (OfflineClientSessionModel clsm : clientSessions) {
+            OfflineClientSessionRepresentation clrep = toRepresentation(realm, clsm);
+            clientSessionReps.add(clrep);
+        }
+        rep.setOfflineClientSessions(clientSessionReps);
+        return rep;
+    }
+
+    public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, OfflineClientSessionModel model) {
+        OfflineClientSessionRepresentation rep = new OfflineClientSessionRepresentation();
+
+        String clientInternalId = model.getClientId();
+        ClientModel client = realm.getClientById(clientInternalId);
+        rep.setClient(client.getClientId());
+
+        rep.setClientSessionId(model.getClientSessionId());
+        rep.setData(model.getData());
+        return rep;
+    }
 
 
 
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index ffb9673..4829182 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1,5 +1,9 @@
 package org.keycloak.models.utils;
 
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
 import org.keycloak.util.Base64;
 import org.jboss.logging.Logger;
 import org.keycloak.enums.SslRequired;
@@ -981,6 +985,11 @@ public class RepresentationToModel {
                 user.addConsent(consentModel);
             }
         }
+        if (userRep.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionRepresentation sessionRep : userRep.getOfflineUserSessions()) {
+                importOfflineSession(session, newRealm, user, sessionRep);
+            }
+        }
         if (userRep.getServiceAccountClientId() != null) {
             String clientId = userRep.getServiceAccountClientId();
             ClientModel client = clientMap.get(clientId);
@@ -1151,6 +1160,29 @@ public class RepresentationToModel {
         return consentModel;
     }
 
+    public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) {
+        OfflineUserSessionModel model = new OfflineUserSessionModel();
+        model.setUserSessionId(sessionRep.getUserSessionId());
+        model.setData(sessionRep.getData());
+        session.users().addOfflineUserSession(newRealm, user, model);
+
+        for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) {
+            OfflineClientSessionModel csModel = new OfflineClientSessionModel();
+            String clientId = csRep.getClient();
+            ClientModel client = newRealm.getClientByClientId(clientId);
+            if (client == null) {
+                throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername());
+            }
+            csModel.setClientId(client.getId());
+            csModel.setUserId(user.getId());
+            csModel.setClientSessionId(csRep.getClientSessionId());
+            csModel.setUserSessionId(sessionRep.getUserSessionId());
+            csModel.setData(csRep.getData());
+
+            session.users().addOfflineClientSession(newRealm, csModel);
+        }
+    }
+
     public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) {
         AuthenticationFlowModel model = new AuthenticationFlowModel();
         model.setBuiltIn(rep.isBuiltIn());
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index f727c75..5f73031 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -258,44 +258,4 @@ public class UserModelDelegate implements UserModel {
     public void setCreatedTimestamp(Long timestamp){
         delegate.setCreatedTimestamp(timestamp);
     }
-
-    @Override
-    public void addOfflineUserSession(OfflineUserSessionModel userSession) {
-        delegate.addOfflineUserSession(userSession);
-    }
-
-    @Override
-    public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
-        return delegate.getOfflineUserSession(userSessionId);
-    }
-
-    @Override
-    public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
-        return delegate.getOfflineUserSessions();
-    }
-
-    @Override
-    public boolean removeOfflineUserSession(String userSessionId) {
-        return delegate.removeOfflineUserSession(userSessionId);
-    }
-
-    @Override
-    public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
-        delegate.addOfflineClientSession(clientSession);
-    }
-
-    @Override
-    public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
-        return delegate.getOfflineClientSession(clientSessionId);
-    }
-
-    @Override
-    public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
-        return delegate.getOfflineClientSessions();
-    }
-
-    @Override
-    public boolean removeOfflineClientSession(String clientSessionId) {
-        return delegate.removeOfflineClientSession(clientSessionId);
-    }
 }
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index d89ce63..2c233d1 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -574,141 +574,6 @@ public class UserAdapter implements UserModel, Comparable {
         return false;
     }
 
-    @Override
-    public void addOfflineUserSession(OfflineUserSessionModel userSession) {
-        if (user.getOfflineUserSessions() == null) {
-            user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
-        }
-
-        if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
-            throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
-        }
-
-        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
-        entity.setUserSessionId(userSession.getUserSessionId());
-        entity.setData(userSession.getData());
-        entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
-        user.getOfflineUserSessions().add(entity);
-    }
-
-    @Override
-    public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
-        OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
-        return entity==null ? null : toModel(entity);
-    }
-
-    @Override
-    public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
-        if (user.getOfflineUserSessions()==null) {
-            return Collections.emptyList();
-        } else {
-            List<OfflineUserSessionModel> result = new ArrayList<>();
-            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-                result.add(toModel(entity));
-            }
-            return result;
-        }
-    }
-
-    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
-        OfflineUserSessionModel model = new OfflineUserSessionModel();
-        model.setUserSessionId(entity.getUserSessionId());
-        model.setData(entity.getData());
-        return model;
-    }
-
-    @Override
-    public boolean removeOfflineUserSession(String userSessionId) {
-        OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
-        if (entity != null) {
-            user.getOfflineUserSessions().remove(entity);
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-                if (entity.getUserSessionId().equals(userSessionId)) {
-                    return entity;
-                }
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
-        OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
-        if (userSessionEntity == null) {
-            throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
-        }
-
-        OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
-        clEntity.setClientSessionId(clientSession.getClientSessionId());
-        clEntity.setClientId(clientSession.getClientId());
-        clEntity.setData(clientSession.getData());
-
-        userSessionEntity.getOfflineClientSessions().add(clEntity);
-    }
-
-    @Override
-    public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    if (clSession.getClientSessionId().equals(clientSessionId)) {
-                        return toModel(clSession, userSession.getUserSessionId());
-                    }
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
-        OfflineClientSessionModel model = new OfflineClientSessionModel();
-        model.setClientSessionId(cls.getClientSessionId());
-        model.setClientId(cls.getClientId());
-        model.setData(cls.getData());
-        model.setUserSessionId(userSessionId);
-        return model;
-    }
-
-    @Override
-    public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
-        List<OfflineClientSessionModel> result = new ArrayList<>();
-
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    result.add(toModel(clSession, userSession.getUserSessionId()));
-                }
-            }
-        }
-
-        return result;
-    }
-
-    @Override
-    public boolean removeOfflineClientSession(String clientSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    if (clSession.getClientSessionId().equals(clientSessionId)) {
-                        userSession.getOfflineClientSessions().remove(clSession);
-                        return true;
-                    }
-                }
-            }
-        }
-
-        return false;
-    }
-
 
     @Override
     public boolean equals(Object o) {
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 40547a8..9f4080a 100755
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -23,6 +23,9 @@ import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredActionProviderModel;
@@ -32,6 +35,8 @@ import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserProvider;
 import org.keycloak.models.entities.FederatedIdentityEntity;
+import org.keycloak.models.entities.OfflineClientSessionEntity;
+import org.keycloak.models.entities.OfflineUserSessionEntity;
 import org.keycloak.models.entities.UserEntity;
 import org.keycloak.models.file.adapter.UserAdapter;
 import org.keycloak.models.utils.CredentialValidation;
@@ -41,6 +46,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -489,4 +495,187 @@ public class FileUserProvider implements UserProvider {
         return null; // not supported yet
     }
 
+    @Override
+    public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
+
+        if (userEntity.getOfflineUserSessions() == null) {
+            userEntity.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
+        }
+
+        if (getUserSessionEntityById(userEntity, userSession.getUserSessionId()) != null) {
+            throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + userEntity.getUsername());
+        }
+
+        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+        entity.setUserSessionId(userSession.getUserSessionId());
+        entity.setData(userSession.getData());
+        entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
+        userEntity.getOfflineUserSessions().add(entity);
+    }
+
+    @Override
+    public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
+
+        OfflineUserSessionEntity entity = getUserSessionEntityById(userEntity, userSessionId);
+        return entity==null ? null : toModel(entity);
+    }
+
+    @Override
+    public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        if (user.getOfflineUserSessions()==null) {
+            return Collections.emptyList();
+        } else {
+            List<OfflineUserSessionModel> result = new ArrayList<>();
+            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+                result.add(toModel(entity));
+            }
+            return result;
+        }
+    }
+
+    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+        OfflineUserSessionModel model = new OfflineUserSessionModel();
+        model.setUserSessionId(entity.getUserSessionId());
+        model.setData(entity.getData());
+        return model;
+    }
+
+    @Override
+    public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+        if (entity != null) {
+            user.getOfflineUserSessions().remove(entity);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private OfflineUserSessionEntity getUserSessionEntityById(UserEntity user, String userSessionId) {
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+                if (entity.getUserSessionId().equals(userSessionId)) {
+                    return entity;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
+        UserModel userModel = getUserById(clientSession.getUserId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
+        if (userSessionEntity == null) {
+            throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
+        }
+
+        OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
+        clEntity.setClientSessionId(clientSession.getClientSessionId());
+        clEntity.setClientId(clientSession.getClientId());
+        clEntity.setData(clientSession.getData());
+
+        userSessionEntity.getOfflineClientSessions().add(clEntity);
+    }
+
+    @Override
+    public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientSessionId().equals(clientSessionId)) {
+                        return toModel(clSession, userSession.getUserSessionId());
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
+        OfflineClientSessionModel model = new OfflineClientSessionModel();
+        model.setClientSessionId(cls.getClientSessionId());
+        model.setClientId(cls.getClientId());
+        model.setData(cls.getData());
+        model.setUserSessionId(userSessionId);
+        return model;
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        List<OfflineClientSessionModel> result = new ArrayList<>();
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    result.add(toModel(clSession, userSession.getUserSessionId()));
+                }
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+        userModel = getUserById(userModel.getId(), realm);
+        UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientSessionId().equals(clientSessionId)) {
+                        userSession.getOfflineClientSessions().remove(clSession);
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+        return getOfflineClientSessions(realm, client, -1, -1).size();
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        List<OfflineClientSessionModel> result = new LinkedList<>();
+
+        List<UserModel> users = new ArrayList<>(inMemoryModel.getUsers(realm.getId()));
+        users = sortedSubList(users, firstResult, maxResults);
+
+        for (UserModel userModel : users) {
+            UserEntity user = ((UserAdapter) userModel).getUserEntity();
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientId().equals(client.getId())) {
+                        result.add(toModel(clSession, userSession.getUserSessionId()));
+                    }
+                }
+            }
+        }
+        return result;
+    }
 }
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
index df7c144..584e91f 100644
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
@@ -104,13 +104,14 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
         };
     }
 
+    private boolean isRegisteredForInvalidation(RealmModel realm, String userId) {
+        return realmInvalidations.contains(realm.getId()) || userInvalidations.containsKey(userId);
+    }
+
     @Override
     public UserModel getUserById(String id, RealmModel realm) {
         if (!cache.isEnabled()) return getDelegate().getUserById(id, realm);
-        if (realmInvalidations.contains(realm.getId())) {
-            return getDelegate().getUserById(id, realm);
-        }
-        if (userInvalidations.containsKey(id)) {
+        if (isRegisteredForInvalidation(realm, id)) {
             return getDelegate().getUserById(id, realm);
         }
 
@@ -120,7 +121,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
             if (model == null) return null;
             if (managedUsers.containsKey(id)) return managedUsers.get(id);
             if (userInvalidations.containsKey(id)) return model;
-            cached = new CachedUser(realm, model);
+            cached = new CachedUser(this, realm, model);
             cache.addCachedUser(realm.getId(), cached);
         } else if (managedUsers.containsKey(id)) {
             return managedUsers.get(id);
@@ -145,7 +146,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
             if (model == null) return null;
             if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId());
             if (userInvalidations.containsKey(model.getId())) return model;
-            cached = new CachedUser(realm, model);
+            cached = new CachedUser(this, realm, model);
             cache.addCachedUser(realm.getId(), cached);
         } else if (userInvalidations.containsKey(cached.getId())) {
             return getDelegate().getUserById(cached.getId(), realm);
@@ -172,7 +173,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
             UserModel model = getDelegate().getUserByEmail(email, realm);
             if (model == null) return null;
             if (userInvalidations.containsKey(model.getId())) return model;
-            cached = new CachedUser(realm, model);
+            cached = new CachedUser(this, realm, model);
             cache.addCachedUser(realm.getId(), cached);
         } else if (userInvalidations.containsKey(cached.getId())) {
             return getDelegate().getUserByEmail(email, realm);
@@ -327,4 +328,94 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
     public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
         getDelegate().preRemove(client, protocolMapper);
     }
+
+    @Override
+    public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+        registerUserInvalidation(realm, user.getId());
+        getDelegate().addOfflineUserSession(realm, user, offlineUserSession);
+    }
+
+    @Override
+    public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        if (isRegisteredForInvalidation(realm, user.getId())) {
+            return getDelegate().getOfflineUserSession(realm, user, userSessionId);
+        }
+
+        CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+        if (cachedUser == null) {
+            return getDelegate().getOfflineUserSession(realm, user, userSessionId);
+        } else {
+            return cachedUser.getOfflineUserSessions().get(userSessionId);
+        }
+    }
+
+    @Override
+    public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+        if (isRegisteredForInvalidation(realm, user.getId())) {
+            return getDelegate().getOfflineUserSessions(realm, user);
+        }
+
+        CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+        if (cachedUser == null) {
+            return getDelegate().getOfflineUserSessions(realm, user);
+        } else {
+            return cachedUser.getOfflineUserSessions().values();
+        }
+    }
+
+    @Override
+    public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        registerUserInvalidation(realm, user.getId());
+        return getDelegate().removeOfflineUserSession(realm, user, userSessionId);
+    }
+
+    @Override
+    public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+        registerUserInvalidation(realm, offlineClientSession.getUserId());
+        getDelegate().addOfflineClientSession(realm, offlineClientSession);
+    }
+
+    @Override
+    public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        if (isRegisteredForInvalidation(realm, user.getId())) {
+            return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
+        }
+
+        CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+        if (cachedUser == null) {
+            return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
+        } else {
+            return cachedUser.getOfflineClientSessions().get(clientSessionId);
+        }
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+        if (isRegisteredForInvalidation(realm, user.getId())) {
+            return getDelegate().getOfflineClientSessions(realm, user);
+        }
+
+        CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+        if (cachedUser == null) {
+            return getDelegate().getOfflineClientSessions(realm, user);
+        } else {
+            return cachedUser.getOfflineClientSessions().values();
+        }
+    }
+
+    @Override
+    public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        registerUserInvalidation(realm, user.getId());
+        return getDelegate().removeOfflineClientSession(realm, user, clientSessionId);
+    }
+
+    @Override
+    public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+        return getDelegate().getOfflineClientSessionsCount(realm, client);
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        return getDelegate().getOfflineClientSessions(realm, client, firstResult, maxResults);
+    }
 }
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
index 769f1b4..5a74b01 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
@@ -348,52 +348,4 @@ public class UserAdapter implements UserModel {
         getDelegateForUpdate();
         return updated.revokeConsentForClient(clientId);
     }
-
-    @Override
-    public void addOfflineUserSession(OfflineUserSessionModel userSession) {
-        getDelegateForUpdate();
-        updated.addOfflineUserSession(userSession);
-    }
-
-    @Override
-    public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
-        if (updated != null) return updated.getOfflineUserSession(userSessionId);
-        return cached.getOfflineUserSessions().get(userSessionId);
-    }
-
-    @Override
-    public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
-        if (updated != null) return updated.getOfflineUserSessions();
-        return cached.getOfflineUserSessions().values();
-    }
-
-    @Override
-    public boolean removeOfflineUserSession(String userSessionId) {
-        getDelegateForUpdate();
-        return updated.removeOfflineUserSession(userSessionId);
-    }
-
-    @Override
-    public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
-        getDelegateForUpdate();
-        updated.addOfflineClientSession(clientSession);
-    }
-
-    @Override
-    public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
-        if (updated != null) return updated.getOfflineClientSession(clientSessionId);
-        return cached.getOfflineClientSessions().get(clientSessionId);
-    }
-
-    @Override
-    public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
-        if (updated != null) return updated.getOfflineClientSessions();
-        return cached.getOfflineClientSessions().values();
-    }
-
-    @Override
-    public boolean removeOfflineClientSession(String clientSessionId) {
-        getDelegateForUpdate();
-        return updated.removeOfflineClientSession(clientSessionId);
-    }
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index d38b6f9..24e866a 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -6,6 +6,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.CacheUserProvider;
 import org.keycloak.util.MultivaluedHashMap;
 
 import java.io.Serializable;
@@ -40,7 +41,7 @@ public class CachedUser implements Serializable {
     private Map<String, OfflineUserSessionModel> offlineUserSessions = new HashMap<>();
     private Map<String, OfflineClientSessionModel> offlineClientSessions = new HashMap<>();
 
-    public CachedUser(RealmModel realm, UserModel user) {
+    public CachedUser(CacheUserProvider cacheUserProvider, RealmModel realm, UserModel user) {
         this.id = user.getId();
         this.realm = realm.getId();
         this.username = user.getUsername();
@@ -59,10 +60,10 @@ public class CachedUser implements Serializable {
         for (RoleModel role : user.getRoleMappings()) {
             roleMappings.add(role.getId());
         }
-        for (OfflineUserSessionModel offlineSession : user.getOfflineUserSessions()) {
+        for (OfflineUserSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineUserSessions(realm, user)) {
             offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession);
         }
-        for (OfflineClientSessionModel offlineSession : user.getOfflineClientSessions()) {
+        for (OfflineClientSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineClientSessions(realm, user)) {
             offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession);
         }
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
index 23081c4..e1c636a 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
@@ -16,7 +16,9 @@ import javax.persistence.Table;
 @NamedQueries({
         @NamedQuery(name="deleteOfflineClientSessionsByRealm", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"),
         @NamedQuery(name="deleteOfflineClientSessionsByRealmAndLink", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
-        @NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId")
+        @NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId"),
+        @NamedQuery(name="findOfflineClientSessionsCountByClient", query="select count(sess) from OfflineClientSessionEntity sess where sess.clientId=:clientId"),
+        @NamedQuery(name="findOfflineClientSessionsByClient", query="select sess from OfflineClientSessionEntity sess where sess.clientId=:clientId order by sess.user.username")
 })
 @Table(name="OFFLINE_CLIENT_SESSION")
 @Entity
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 8cee4ea..563e898 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -4,6 +4,8 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredActionProviderModel;
@@ -13,6 +15,8 @@ import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserProvider;
 import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
+import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
+import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
 import org.keycloak.models.jpa.entities.UserAttributeEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.utils.CredentialValidation;
@@ -22,8 +26,10 @@ import javax.persistence.EntityManager;
 import javax.persistence.Query;
 import javax.persistence.TypedQuery;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -473,4 +479,167 @@ public class JpaUserProvider implements UserProvider {
         // Not supported yet
         return null;
     }
+
+    @Override
+    public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+        entity.setUser(userEntity);
+        entity.setUserSessionId(offlineUserSession.getUserSessionId());
+        entity.setData(offlineUserSession.getData());
+        em.persist(entity);
+        userEntity.getOfflineUserSessions().add(entity);
+        em.flush();
+    }
+
+    @Override
+    public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
+            if (entity.getUserSessionId().equals(userSessionId)) {
+                return toModel(entity);
+            }
+        }
+        return null;
+    }
+
+    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+        OfflineUserSessionModel model = new OfflineUserSessionModel();
+        model.setUserSessionId(entity.getUserSessionId());
+        model.setData(entity.getData());
+        return model;
+    }
+
+    @Override
+    public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        List<OfflineUserSessionModel> result = new LinkedList<>();
+        for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
+            result.add(toModel(entity));
+        }
+        return result;
+    }
+
+    @Override
+    public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        OfflineUserSessionEntity found = null;
+        for (OfflineUserSessionEntity session : userEntity.getOfflineUserSessions()) {
+            if (session.getUserSessionId().equals(userSessionId)) {
+                found = session;
+                break;
+            }
+        }
+
+        if (found == null) {
+            return false;
+        } else {
+            userEntity.getOfflineUserSessions().remove(found);
+            em.remove(found);
+            em.flush();
+            return true;
+        }
+    }
+
+    @Override
+    public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+        UserEntity userEntity = em.getReference(UserEntity.class, offlineClientSession.getUserId());
+
+        OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
+        entity.setUser(userEntity);
+        entity.setClientSessionId(offlineClientSession.getClientSessionId());
+        entity.setUserSessionId(offlineClientSession.getUserSessionId());
+        entity.setClientId(offlineClientSession.getClientId());
+        entity.setData(offlineClientSession.getData());
+        em.persist(entity);
+        userEntity.getOfflineClientSessions().add(entity);
+        em.flush();
+    }
+
+    @Override
+    public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
+            if (entity.getClientSessionId().equals(clientSessionId)) {
+                return toModel(entity);
+            }
+        }
+        return null;
+    }
+
+    private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
+        OfflineClientSessionModel model = new OfflineClientSessionModel();
+        model.setClientSessionId(entity.getClientSessionId());
+        model.setClientId(entity.getClientId());
+        model.setUserId(entity.getUser().getId());
+        model.setUserSessionId(entity.getUserSessionId());
+        model.setData(entity.getData());
+        return model;
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        List<OfflineClientSessionModel> result = new LinkedList<>();
+        for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
+            result.add(toModel(entity));
+        }
+        return result;
+    }
+
+    @Override
+    public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+        UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+        OfflineClientSessionEntity found = null;
+        for (OfflineClientSessionEntity session : userEntity.getOfflineClientSessions()) {
+            if (session.getClientSessionId().equals(clientSessionId)) {
+                found = session;
+                break;
+            }
+        }
+
+        if (found == null) {
+            return false;
+        } else {
+            userEntity.getOfflineClientSessions().remove(found);
+            em.remove(found);
+            em.flush();
+            return true;
+        }
+    }
+
+    @Override
+    public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+        Query query = em.createNamedQuery("findOfflineClientSessionsCountByClient");
+        query.setParameter("clientId", client.getId());
+        Number n = (Number) query.getSingleResult();
+        return n.intValue();
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        TypedQuery<OfflineClientSessionEntity> query = em.createNamedQuery("findOfflineClientSessionsByClient", OfflineClientSessionEntity.class);
+        query.setParameter("clientId", client.getId());
+
+        if (firstResult != -1) {
+            query.setFirstResult(firstResult);
+        }
+        if (maxResults != -1) {
+            query.setMaxResults(maxResults);
+        }
+
+        List<OfflineClientSessionEntity> results = query.getResultList();
+        Set<OfflineClientSessionModel> set = new HashSet<>();
+        for (OfflineClientSessionEntity entity : results) {
+            set.add(toModel(entity));
+        }
+        return set;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index bdcf1f1..9c75057 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -756,124 +756,6 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public void addOfflineUserSession(OfflineUserSessionModel offlineSession) {
-        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
-        entity.setUser(user);
-        entity.setUserSessionId(offlineSession.getUserSessionId());
-        entity.setData(offlineSession.getData());
-        em.persist(entity);
-        user.getOfflineUserSessions().add(entity);
-        em.flush();
-    }
-
-    @Override
-    public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
-        for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-            if (entity.getUserSessionId().equals(userSessionId)) {
-                return toModel(entity);
-            }
-        }
-        return null;
-    }
-
-    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
-        OfflineUserSessionModel model = new OfflineUserSessionModel();
-        model.setUserSessionId(entity.getUserSessionId());
-        model.setData(entity.getData());
-        return model;
-    }
-
-    @Override
-    public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
-        List<OfflineUserSessionModel> result = new LinkedList<>();
-        for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-            result.add(toModel(entity));
-        }
-        return result;
-    }
-
-    @Override
-    public boolean removeOfflineUserSession(String userSessionId) {
-        OfflineUserSessionEntity found = null;
-        for (OfflineUserSessionEntity session : user.getOfflineUserSessions()) {
-            if (session.getUserSessionId().equals(userSessionId)) {
-                found = session;
-                break;
-            }
-        }
-
-        if (found == null) {
-            return false;
-        } else {
-            user.getOfflineUserSessions().remove(found);
-            em.remove(found);
-            em.flush();
-            return true;
-        }
-    }
-
-    @Override
-    public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
-        OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
-        entity.setUser(user);
-        entity.setClientSessionId(clientSession.getClientSessionId());
-        entity.setUserSessionId(clientSession.getUserSessionId());
-        entity.setClientId(clientSession.getClientId());
-        entity.setData(clientSession.getData());
-        em.persist(entity);
-        user.getOfflineClientSessions().add(entity);
-        em.flush();
-    }
-
-    @Override
-    public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
-        for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
-            if (entity.getClientSessionId().equals(clientSessionId)) {
-                return toModel(entity);
-            }
-        }
-        return null;
-    }
-
-    private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
-        OfflineClientSessionModel model = new OfflineClientSessionModel();
-        model.setClientSessionId(entity.getClientSessionId());
-        model.setClientId(entity.getClientId());
-        model.setUserSessionId(entity.getUserSessionId());
-        model.setData(entity.getData());
-        return model;
-    }
-
-    @Override
-    public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
-        List<OfflineClientSessionModel> result = new LinkedList<>();
-        for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
-            result.add(toModel(entity));
-        }
-        return result;
-    }
-
-    @Override
-    public boolean removeOfflineClientSession(String clientSessionId) {
-        OfflineClientSessionEntity found = null;
-        for (OfflineClientSessionEntity session : user.getOfflineClientSessions()) {
-            if (session.getClientSessionId().equals(clientSessionId)) {
-                found = session;
-                break;
-            }
-        }
-
-        if (found == null) {
-            return false;
-        } else {
-            user.getOfflineClientSessions().remove(found);
-            em.remove(found);
-            em.flush();
-            return true;
-        }
-    }
-
-    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 14081c1..214733a 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -10,6 +10,10 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.CredentialValidationOutput;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredActionProviderModel;
@@ -26,8 +30,10 @@ import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
 import org.keycloak.models.utils.CredentialValidation;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -51,7 +57,7 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
-    public UserModel getUserById(String id, RealmModel realm) {
+    public UserAdapter getUserById(String id, RealmModel realm) {
         MongoUserEntity user = getMongoStore().loadEntity(MongoUserEntity.class, id, invocationContext);
 
         // Check that it's user from this realm
@@ -244,8 +250,8 @@ public class MongoUserProvider implements UserProvider {
 
     @Override
     public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
-        UserModel user = getUserById(userModel.getId(), realm);
-        MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+        UserAdapter user = getUserById(userModel.getId(), realm);
+        MongoUserEntity userEntity = user.getUser();
         List<FederatedIdentityEntity> linkEntities = userEntity.getFederatedIdentities();
 
         if (linkEntities == null) {
@@ -263,8 +269,8 @@ public class MongoUserProvider implements UserProvider {
 
     @Override
     public FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm) {
-        user = getUserById(user.getId(), realm);
-        MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+        UserAdapter mongoUser = getUserById(user.getId(), realm);
+        MongoUserEntity userEntity = mongoUser.getUser();
         FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);
 
         return federatedIdentityEntity != null ? new FederatedIdentityModel(federatedIdentityEntity.getIdentityProvider(), federatedIdentityEntity.getUserId(),
@@ -320,8 +326,8 @@ public class MongoUserProvider implements UserProvider {
 
     @Override
     public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel identity) {
-        user = getUserById(user.getId(), realm);
-        MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+        UserAdapter mongoUser = getUserById(user.getId(), realm);
+        MongoUserEntity userEntity = mongoUser.getUser();
         FederatedIdentityEntity federatedIdentityEntity = new FederatedIdentityEntity();
         federatedIdentityEntity.setIdentityProvider(identity.getIdentityProvider());
         federatedIdentityEntity.setUserId(identity.getUserId());
@@ -333,8 +339,8 @@ public class MongoUserProvider implements UserProvider {
 
     @Override
     public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
-        federatedUser = getUserById(federatedUser.getId(), realm);
-        MongoUserEntity userEntity = ((UserAdapter) federatedUser).getUser();
+        UserAdapter mongoUser = getUserById(federatedUser.getId(), realm);
+        MongoUserEntity userEntity = mongoUser.getUser();
         FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, federatedIdentityModel.getIdentityProvider());
 
         federatedIdentityEntity.setToken(federatedIdentityModel.getToken());
@@ -342,8 +348,8 @@ public class MongoUserProvider implements UserProvider {
 
     @Override
     public boolean removeFederatedIdentity(RealmModel realm, UserModel userModel, String socialProvider) {
-        UserModel user = getUserById(userModel.getId(), realm);
-        MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+        UserAdapter user = getUserById(userModel.getId(), realm);
+        MongoUserEntity userEntity = user.getUser();
         FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);
         if (federatedIdentityEntity == null) {
             return false;
@@ -476,4 +482,205 @@ public class MongoUserProvider implements UserProvider {
         // Not supported yet
         return null;
     }
+
+    @Override
+    public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        if (user.getOfflineUserSessions() == null) {
+            user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
+        }
+
+        if (getUserSessionEntityById(user, userSession.getUserSessionId()) != null) {
+            throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
+        }
+
+        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+        entity.setUserSessionId(userSession.getUserSessionId());
+        entity.setData(userSession.getData());
+        entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
+        user.getOfflineUserSessions().add(entity);
+
+        getMongoStore().updateEntity(user, invocationContext);
+    }
+
+    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+        OfflineUserSessionModel model = new OfflineUserSessionModel();
+        model.setUserSessionId(entity.getUserSessionId());
+        model.setData(entity.getData());
+        return model;
+    }
+
+    private OfflineUserSessionEntity getUserSessionEntityById(MongoUserEntity user, String userSessionId) {
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+                if (entity.getUserSessionId().equals(userSessionId)) {
+                    return entity;
+                }
+            }
+        }
+        return null;
+    }
+
+
+    @Override
+    public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+        return entity==null ? null : toModel(entity);
+    }
+
+    @Override
+    public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        if (user.getOfflineUserSessions()==null) {
+            return Collections.emptyList();
+        } else {
+            List<OfflineUserSessionModel> result = new ArrayList<>();
+            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+                result.add(toModel(entity));
+            }
+            return result;
+        }
+    }
+
+    @Override
+    public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+        if (entity != null) {
+            user.getOfflineUserSessions().remove(entity);
+            getMongoStore().updateEntity(user, invocationContext);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
+        MongoUserEntity user = getUserById(clientSession.getUserId(), realm).getUser();
+
+        OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
+        if (userSessionEntity == null) {
+            throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
+        }
+
+        OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
+        clEntity.setClientSessionId(clientSession.getClientSessionId());
+        clEntity.setClientId(clientSession.getClientId());
+        clEntity.setData(clientSession.getData());
+
+        userSessionEntity.getOfflineClientSessions().add(clEntity);
+        getMongoStore().updateEntity(user, invocationContext);
+    }
+
+    @Override
+    public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientSessionId().equals(clientSessionId)) {
+                        return toModel(clSession, user.getId(), userSession.getUserSessionId());
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userId, String userSessionId) {
+        OfflineClientSessionModel model = new OfflineClientSessionModel();
+        model.setClientSessionId(cls.getClientSessionId());
+        model.setClientId(cls.getClientId());
+        model.setUserId(userId);
+        model.setData(cls.getData());
+        model.setUserSessionId(userSessionId);
+        return model;
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+        List<OfflineClientSessionModel> result = new ArrayList<>();
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    result.add(toModel(clSession, user.getId(), userSession.getUserSessionId()));
+                }
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+        MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+        boolean updated = false;
+
+        if (user.getOfflineUserSessions() != null) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientSessionId().equals(clientSessionId)) {
+                        userSession.getOfflineClientSessions().remove(clSession);
+                        updated = true;
+                        break;
+                    }
+                }
+
+                if (updated && userSession.getOfflineClientSessions().isEmpty()) {
+                    user.getOfflineUserSessions().remove(userSession);
+                }
+
+                if (updated) {
+                    getMongoStore().updateEntity(user, invocationContext);
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+        DBObject query = new QueryBuilder()
+                .and("realmId").is(realm.getId())
+                .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
+                .get();
+        return getMongoStore().countEntities(MongoUserEntity.class, query, invocationContext);
+    }
+
+    @Override
+    public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+        DBObject query = new QueryBuilder()
+                .and("realmId").is(realm.getId())
+                .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
+                .get();
+        DBObject sort = new BasicDBObject("username", 1);
+        List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext);
+
+        List<OfflineClientSessionModel> result = new LinkedList<>();
+        for (MongoUserEntity user : users) {
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+                    if (clSession.getClientId().equals(client.getId())) {
+                        OfflineClientSessionModel model = toModel(clSession, user.getId(), userSession.getUserSessionId());
+                        result.add(model);
+                    }
+                }
+            }
+        }
+
+        return result;
+    }
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index a475bb6..9f13e63 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -633,145 +633,6 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     }
 
     @Override
-    public void addOfflineUserSession(OfflineUserSessionModel userSession) {
-        if (user.getOfflineUserSessions() == null) {
-            user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
-        }
-
-        if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
-            throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + getMongoEntity().getUsername());
-        }
-
-        OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
-        entity.setUserSessionId(userSession.getUserSessionId());
-        entity.setData(userSession.getData());
-        entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
-        user.getOfflineUserSessions().add(entity);
-        updateUser();
-    }
-
-    @Override
-    public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
-        OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
-        return entity==null ? null : toModel(entity);
-    }
-
-    @Override
-    public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
-        if (user.getOfflineUserSessions()==null) {
-            return Collections.emptyList();
-        } else {
-            List<OfflineUserSessionModel> result = new ArrayList<>();
-            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-                result.add(toModel(entity));
-            }
-            return result;
-        }
-    }
-
-    private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
-        OfflineUserSessionModel model = new OfflineUserSessionModel();
-        model.setUserSessionId(entity.getUserSessionId());
-        model.setData(entity.getData());
-        return model;
-    }
-
-    @Override
-    public boolean removeOfflineUserSession(String userSessionId) {
-        OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
-        if (entity != null) {
-            user.getOfflineUserSessions().remove(entity);
-            updateUser();
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
-                if (entity.getUserSessionId().equals(userSessionId)) {
-                    return entity;
-                }
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
-        OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
-        if (userSessionEntity == null) {
-            throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + getMongoEntity().getUsername());
-        }
-
-        OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
-        clEntity.setClientSessionId(clientSession.getClientSessionId());
-        clEntity.setClientId(clientSession.getClientId());
-        clEntity.setData(clientSession.getData());
-
-        userSessionEntity.getOfflineClientSessions().add(clEntity);
-        updateUser();
-    }
-
-    @Override
-    public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    if (clSession.getClientSessionId().equals(clientSessionId)) {
-                        return toModel(clSession, userSession.getUserSessionId());
-                    }
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
-        OfflineClientSessionModel model = new OfflineClientSessionModel();
-        model.setClientSessionId(cls.getClientSessionId());
-        model.setClientId(cls.getClientId());
-        model.setData(cls.getData());
-        model.setUserSessionId(userSessionId);
-        return model;
-    }
-
-    @Override
-    public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
-        List<OfflineClientSessionModel> result = new ArrayList<>();
-
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    result.add(toModel(clSession, userSession.getUserSessionId()));
-                }
-            }
-        }
-
-        return result;
-    }
-
-    @Override
-    public boolean removeOfflineClientSession(String clientSessionId) {
-        if (user.getOfflineUserSessions() != null) {
-            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
-                for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
-                    if (clSession.getClientSessionId().equals(clientSessionId)) {
-                        userSession.getOfflineClientSessions().remove(clSession);
-                        updateUser();
-                        return true;
-                    }
-                }
-            }
-        }
-
-        return false;
-    }
-
-    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
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 c166565..ee8e9e8 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -98,7 +98,7 @@ public class TokenManager {
         ClientSessionModel clientSession = null;
         if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
 
-            clientSession = OfflineTokenUtils.findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
+            clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState());
             if (clientSession != null) {
                 userSession = clientSession.getUserSession();
             }
@@ -496,7 +496,7 @@ public class TokenManager {
 
                 refreshToken = new RefreshToken(accessToken);
                 refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
-                OfflineTokenUtils.persistOfflineSession(clientSession, userSession);
+                OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession);
             } else {
                 refreshToken = new RefreshToken(accessToken);
                 refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
index 50abfd4..519ae25 100644
--- a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
@@ -75,7 +75,7 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
 
     @Override
     public String getRedirectUri() {
-        return data.getRedirectUri();
+        return getData().getRedirectUri();
     }
 
     @Override
@@ -85,7 +85,7 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
 
     @Override
     public int getTimestamp() {
-        return 0;
+        return getData().getTimestamp();
     }
 
     @Override
@@ -238,6 +238,9 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
         @JsonProperty("authenticatorStatus")
         private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
 
+        @JsonProperty("timestamp")
+        private int timestamp;
+
         public String getAuthMethod() {
             return authMethod;
         }
@@ -285,5 +288,13 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
         public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
             this.authenticatorStatus = authenticatorStatus;
         }
+
+        public int getTimestamp() {
+            return timestamp;
+        }
+
+        public void setTimestamp(int timestamp) {
+            this.timestamp = timestamp;
+        }
     }
 }
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
index b4900c1..13ff55b 100644
--- a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
@@ -9,6 +9,7 @@ import org.jboss.logging.Logger;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.OfflineClientSessionModel;
 import org.keycloak.models.OfflineUserSessionModel;
@@ -17,9 +18,9 @@ import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.Time;
 
 /**
- * TODO: Change to utils?
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
@@ -27,12 +28,12 @@ public class OfflineTokenUtils {
 
     protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class);
 
-    public static void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
+    public static void persistOfflineSession(KeycloakSession kcSession, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
         ClientModel client = clientSession.getClient();
 
         // First verify if we already have offlineToken for this user+client . If yes, then invalidate it (This is to avoid leaks)
-        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+        Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
         for (OfflineClientSessionModel existing : clientSessions) {
             if (existing.getClientId().equals(client.getId())) {
                 if (logger.isTraceEnabled()) {
@@ -40,28 +41,28 @@ public class OfflineTokenUtils {
                             user.getUsername(), client.getClientId(), existing.getClientSessionId());
                 }
 
-                user.removeOfflineClientSession(existing.getClientSessionId());
+                kcSession.users().removeOfflineClientSession(realm, user, existing.getClientSessionId());
 
                 // Check if userSession is ours. If not, then check if it has other clientSessions and remove it otherwise
                 if (!existing.getUserSessionId().equals(userSession.getId())) {
-                    checkUserSessionHasClientSessions(user, existing.getUserSessionId());
+                    checkUserSessionHasClientSessions(kcSession, realm, user, existing.getUserSessionId());
                 }
             }
         }
 
         // Verify if we already have UserSession with this ID. If yes, don't create another one
-        OfflineUserSessionModel userSessionRep = user.getOfflineUserSession(userSession.getId());
+        OfflineUserSessionModel userSessionRep = kcSession.users().getOfflineUserSession(realm, user, userSession.getId());
         if (userSessionRep == null) {
-            createOfflineUserSession(user, userSession);
+            createOfflineUserSession(kcSession, realm, user, userSession);
         }
 
         // Create clientRep and save to DB.
-        createOfflineClientSession(user, clientSession, userSession);
+        createOfflineClientSession(kcSession, realm, user, clientSession, userSession);
     }
 
     // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
-    public static ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
-        OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId);
+    public static ClientSessionModel findOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
+        OfflineClientSessionModel clientSession = kcSession.users().getOfflineClientSession(realm, user, clientSessionId);
         if (clientSession == null) {
             return null;
         }
@@ -71,7 +72,7 @@ public class OfflineTokenUtils {
                     "  Wanted user session: " + userSessionId);
         }
 
-        OfflineUserSessionModel userSession = user.getOfflineUserSession(userSessionId);
+        OfflineUserSessionModel userSession = kcSession.users().getOfflineUserSession(realm, user, userSessionId);
         if (userSession == null) {
             throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId);
         }
@@ -79,13 +80,11 @@ public class OfflineTokenUtils {
         OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user);
 
         ClientModel client = realm.getClientById(clientSession.getClientId());
-        OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
-
-        return clientSessionAdapter;
+        return new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
     }
 
-    public static Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
-        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+    public static Set<ClientModel> findClientsWithOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user) {
+        Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
         Set<ClientModel> clients = new HashSet<>();
         for (OfflineClientSessionModel clientSession : clientSessions) {
             ClientModel client = realm.getClientById(clientSession.getClientId());
@@ -94,8 +93,8 @@ public class OfflineTokenUtils {
         return clients;
     }
 
-    public static boolean revokeOfflineToken(UserModel user, ClientModel client) {
-        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+    public static boolean revokeOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientModel client) {
+        Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
         boolean anyRemoved = false;
         for (OfflineClientSessionModel clientSession : clientSessions) {
             if (clientSession.getClientId().equals(client.getId())) {
@@ -104,8 +103,8 @@ public class OfflineTokenUtils {
                             user.getUsername(), client.getClientId(), clientSession.getClientSessionId());
                 }
 
-                user.removeOfflineClientSession(clientSession.getClientSessionId());
-                checkUserSessionHasClientSessions(user, clientSession.getUserSessionId());
+                kcSession.users().removeOfflineClientSession(realm, user, clientSession.getClientSessionId());
+                checkUserSessionHasClientSessions(kcSession, realm, user, clientSession.getUserSessionId());
                 anyRemoved = true;
             }
         }
@@ -123,7 +122,7 @@ public class OfflineTokenUtils {
         return clientSession.getRoles().contains(offlineAccessRole.getId());
     }
 
-    private static void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
+    private static void createOfflineUserSession(KeycloakSession kcSession, RealmModel realm, UserModel user, UserSessionModel userSession) {
         if (logger.isTraceEnabled()) {
             logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
         }
@@ -141,13 +140,13 @@ public class OfflineTokenUtils {
             OfflineUserSessionModel sessionModel = new OfflineUserSessionModel();
             sessionModel.setUserSessionId(userSession.getId());
             sessionModel.setData(stringRep);
-            user.addOfflineUserSession(sessionModel);
+            kcSession.users().addOfflineUserSession(realm, user, sessionModel);
         } catch (IOException ioe) {
             throw new ModelException(ioe);
         }
     }
 
-    private static void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
+    private static void createOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
         if (logger.isTraceEnabled()) {
             logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
                     clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
@@ -159,23 +158,25 @@ public class OfflineTokenUtils {
         rep.setRoles(clientSession.getRoles());
         rep.setNotes(clientSession.getNotes());
         rep.setAuthenticatorStatus(clientSession.getExecutionStatus());
+        rep.setTimestamp(Time.currentTime());
 
         try {
             String stringRep = JsonSerialization.writeValueAsString(rep);
             OfflineClientSessionModel clsModel = new OfflineClientSessionModel();
             clsModel.setClientSessionId(clientSession.getId());
             clsModel.setClientId(clientSession.getClient().getId());
+            clsModel.setUserId(user.getId());
             clsModel.setUserSessionId(userSession.getId());
             clsModel.setData(stringRep);
-            user.addOfflineClientSession(clsModel);
+            kcSession.users().addOfflineClientSession(realm, clsModel);
         } catch (IOException ioe) {
             throw new ModelException(ioe);
         }
     }
 
     // Check if userSession has any offline clientSessions attached to it. Remove userSession if not
-    private static void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
-        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+    private static void checkUserSessionHasClientSessions(KeycloakSession kcSession, RealmModel realm, UserModel user, String userSessionId) {
+        Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
 
         for (OfflineClientSessionModel clientSession : clientSessions) {
             if (clientSession.getUserSessionId().equals(userSessionId)) {
@@ -186,6 +187,6 @@ public class OfflineTokenUtils {
         if (logger.isTraceEnabled()) {
             logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
         }
-        user.removeOfflineUserSession(userSessionId);
+        kcSession.users().removeOfflineUserSession(realm, user, userSessionId);
     }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 20e7b6e..17de6a2 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -486,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
         // Revoke grant in UserModel
         UserModel user = auth.getUser();
         user.revokeConsentForClient(client.getId());
-        OfflineTokenUtils.revokeOfflineToken(user, client);
+        OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
 
         // Logout clientSessions for this user and client
         AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 2198333..71d8ac0 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -7,8 +7,11 @@ import org.jboss.resteasy.spi.NotFoundException;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
@@ -24,6 +27,8 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
 import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.offline.OfflineClientSessionAdapter;
+import org.keycloak.services.offline.OfflineUserSessionAdapter;
 import org.keycloak.services.resources.KeycloakApplication;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.util.JsonSerialization;
@@ -391,6 +396,65 @@ public class ClientResource {
     }
 
     /**
+     * Get application offline session count
+     *
+     * Returns a number of offline user sessions associated with this client
+     *
+     * {
+     *     "count": number
+     * }
+     *
+     * @return
+     */
+    @Path("offline-session-count")
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Integer> getOfflineSessionCount() {
+        auth.requireView();
+        Map<String, Integer> map = new HashMap<String, Integer>();
+        map.put("count", session.users().getOfflineClientSessionsCount(client.getRealm(), client));
+        return map;
+    }
+
+    /**
+     * Get offline sessions for client
+     *
+     * Returns a list of offline user sessions associated with this client
+     *
+     * @param firstResult Paging offset
+     * @param maxResults Paging size
+     * @return
+     */
+    @Path("offline-sessions")
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<UserSessionRepresentation> getOfflineUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) {
+        auth.requireView();
+        firstResult = firstResult != null ? firstResult : -1;
+        maxResults = maxResults != null ? maxResults : -1;
+        List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
+        for (OfflineClientSessionModel offlineClientSession : session.users().getOfflineClientSessions(client.getRealm(), client, firstResult, maxResults)) {
+            UserModel user = session.users().getUserById(offlineClientSession.getUserId(), client.getRealm());
+            OfflineUserSessionModel offlineUserSession = session.users().getOfflineUserSession(client.getRealm(), user, offlineClientSession.getUserSessionId());
+            OfflineUserSessionAdapter sessionAdapter = new OfflineUserSessionAdapter(offlineUserSession, user);
+            OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(offlineClientSession, client.getRealm(), client, sessionAdapter);
+
+            UserSessionRepresentation rep = new UserSessionRepresentation();
+            rep.setId(sessionAdapter.getId());
+            rep.setStart(Time.toMillis(clientSessionAdapter.getTimestamp()));
+            rep.setUsername(user.getUsername());
+            rep.setUserId(user.getId());
+            rep.setIpAddress(sessionAdapter.getIpAddress());
+
+            sessions.add(rep);
+        }
+        return sessions;
+    }
+
+
+    /**
      * Logout all sessions
      *
      * If the client has an admin URL, invalidate all sessions associated with that client directly.
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index e33e50e..bf535d9 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -66,15 +66,18 @@ import javax.ws.rs.WebApplicationException;
 
 import java.net.URI;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.keycloak.models.UsernameLoginFailureModel;
 import org.keycloak.services.managers.BruteForceProtector;
+import org.keycloak.services.offline.OfflineTokenUtils;
 import org.keycloak.services.resources.AccountService;
 
 /**
@@ -439,25 +442,44 @@ public class UsersResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<UserConsentRepresentation> getConsents(final @PathParam("id") String id) {
+    public List<Map<String, Object>> getConsents(final @PathParam("id") String id) {
         auth.requireView();
         UserModel user = session.users().getUserById(id, realm);
         if (user == null) {
             throw new NotFoundException("User not found");
         }
 
-        List<UserConsentModel> consents = user.getConsents();
-        List<UserConsentRepresentation> result = new ArrayList<UserConsentRepresentation>();
+        List<Map<String, Object>> result = new LinkedList<>();
 
-        for (UserConsentModel consent : consents) {
-            UserConsentRepresentation rep = ModelToRepresentation.toRepresentation(consent);
-            result.add(rep);
+        Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
+
+        for (ClientModel client : realm.getClients()) {
+            UserConsentModel consent = user.getConsentByClient(client.getId());
+            boolean hasOfflineToken = offlineClients.contains(client);
+
+            if (consent == null && !hasOfflineToken) {
+                continue;
+            }
+
+            UserConsentRepresentation rep = (consent == null) ? null : ModelToRepresentation.toRepresentation(consent);
+
+            Map<String, Object> currentRep = new HashMap<>();
+            currentRep.put("clientId", client.getClientId());
+            currentRep.put("grantedProtocolMappers", (rep==null ? Collections.emptyMap() : rep.getGrantedProtocolMappers()));
+            currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
+            currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles()));
+
+            List<String> additionalGrants = hasOfflineToken ? Arrays.asList("Offline Token") : Collections.<String>emptyList();
+            currentRep.put("additionalGrants", additionalGrants);
+
+            result.add(currentRep);
         }
+
         return result;
     }
 
     /**
-     * Revoke consent for particular client from user
+     * Revoke consent and offline tokens for particular client from user
      *
      * @param id User id
      * @param clientId Client id
@@ -473,12 +495,16 @@ public class UsersResource {
         }
 
         ClientModel client = realm.getClientByClientId(clientId);
-        boolean revoked = user.revokeConsentForClient(client.getId());
-        if (revoked) {
+        boolean revokedConsent = user.revokeConsentForClient(client.getId());
+        boolean revokedOfflineToken = OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
+
+        if (revokedConsent) {
             // Logout clientSessions for this user and client
             AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
-        } else {
-            throw new NotFoundException("Consent not found");
+        }
+
+        if (!revokedConsent && !revokedOfflineToken) {
+            throw new NotFoundException("Consent nor offline token not found");
         }
         adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 6f42fd5..6040c72 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -15,6 +15,8 @@ import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
@@ -32,6 +34,7 @@ import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.RealmManager;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -327,6 +330,21 @@ public class ImportTest extends AbstractModelTest {
         Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
         Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
 
+        // Test offline sessions
+        Collection<OfflineUserSessionModel> offlineUserSessions = session.users().getOfflineUserSessions(realm, admin);
+        Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, admin);
+        Assert.assertEquals(offlineUserSessions.size(), 1);
+        Assert.assertEquals(offlineClientSessions.size(), 1);
+        OfflineUserSessionModel offlineSession = offlineUserSessions.iterator().next();
+        OfflineClientSessionModel offlineClSession = offlineClientSessions.iterator().next();
+        Assert.assertEquals(offlineSession.getData(), "something1");
+        Assert.assertEquals(offlineSession.getUserSessionId(), "123");
+        Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId());
+        Assert.assertEquals(offlineClSession.getUserSessionId(), "123");
+        Assert.assertEquals(offlineClSession.getUserId(), admin.getId());
+        Assert.assertEquals(offlineClSession.getData(), "something2");
+
+
         // Test service accounts
         Assert.assertFalse(application.isServiceAccountsEnabled());
         Assert.assertTrue(otherApp.isServiceAccountsEnabled());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index f0118c2..68fa2f8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -15,6 +15,7 @@ import static org.junit.Assert.assertNotNull;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -292,12 +293,35 @@ public class UserModelTest extends AbstractModelTest {
         ClientModel barClient = realm.addClient("bar");
 
         UserModel user1 = session.users().addUser(realm, "user1");
-        addOfflineUserSession(user1, "123", "something1");
-        addOfflineClientSession(user1, "456", "123", fooClient.getId(), "something2");
-        addOfflineClientSession(user1, "789", "123", barClient.getId(), "something3");
+        UserModel user2 = session.users().addUser(realm, "user2");
+
+        addOfflineUserSession(realm, user1, "123", "something1");
+        addOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2");
+        addOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3");
+
+        addOfflineUserSession(realm, user2, "2123", "something4");
+        addOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5");
 
         commit();
 
+        // Searching by clients
+        Assert.assertEquals(2, session.users().getOfflineClientSessionsCount(realm, fooClient));
+        Assert.assertEquals(1, session.users().getOfflineClientSessionsCount(realm, barClient));
+
+        Collection<OfflineClientSessionModel> clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10);
+        Assert.assertEquals(2, clientSessions.size());
+        clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1);
+        OfflineClientSessionModel cls = clientSessions.iterator().next();
+        assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2");
+        clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1);
+        cls = clientSessions.iterator().next();
+        assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5");
+
+        clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10);
+        Assert.assertEquals(1, clientSessions.size());
+        cls = clientSessions.iterator().next();
+        assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3");
+
         realm = realmManager.getRealmByName("original");
         realm.removeClient(barClient.getId());
 
@@ -305,9 +329,9 @@ public class UserModelTest extends AbstractModelTest {
 
         realm = realmManager.getRealmByName("original");
         user1 = session.users().getUserByUsername("user1", realm);
-        Assert.assertEquals("something1", user1.getOfflineUserSession("123").getData());
-        Assert.assertEquals("something2", user1.getOfflineClientSession("456").getData());
-        Assert.assertNull(user1.getOfflineClientSession("789"));
+        Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData());
+        Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData());
+        Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
 
         realm.removeClient(fooClient.getId());
 
@@ -315,27 +339,28 @@ public class UserModelTest extends AbstractModelTest {
 
         realm = realmManager.getRealmByName("original");
         user1 = session.users().getUserByUsername("user1", realm);
-        Assert.assertNull(user1.getOfflineClientSession("456"));
-        Assert.assertNull(user1.getOfflineClientSession("789"));
-        Assert.assertNull(user1.getOfflineUserSession("123"));
-        Assert.assertEquals(0, user1.getOfflineUserSessions().size());
-        Assert.assertEquals(0, user1.getOfflineClientSessions().size());
+        Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456"));
+        Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
+        Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123"));
+        Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size());
+        Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size());
     }
 
-    private void addOfflineUserSession(UserModel user, String userSessionId, String data) {
+    private void addOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) {
         OfflineUserSessionModel model = new OfflineUserSessionModel();
         model.setUserSessionId(userSessionId);
         model.setData(data);
-        user.addOfflineUserSession(model);
+        session.users().addOfflineUserSession(realm, user, model);
     }
 
-    private void addOfflineClientSession(UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
+    private void addOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
         OfflineClientSessionModel model = new OfflineClientSessionModel();
         model.setClientSessionId(clientSessionId);
         model.setUserSessionId(userSessionId);
+        model.setUserId(user.getId());
         model.setClientId(clientId);
         model.setData(data);
-        user.addOfflineClientSession(model);
+        session.users().addOfflineClientSession(realm, model);
     }
 
     public static void assertEquals(UserModel expected, UserModel actual) {
@@ -352,5 +377,14 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions);
     }
 
+    private static void assertSessionEquals(OfflineClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId,
+                                     String expectedClientId, String expectedUserId, String expectedData) {
+        Assert.assertEquals(cls.getData(), expectedData);
+        Assert.assertEquals(cls.getClientSessionId(), expectedClientSessionId);
+        Assert.assertEquals(cls.getUserSessionId(), expectedUserSessionId);
+        Assert.assertEquals(cls.getUserId(), expectedUserId);
+        Assert.assertEquals(cls.getClientId(), expectedClientId);
+    }
+
 }
 
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index f7c8cdd..521ef83 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -119,6 +119,19 @@
                         "openid-connect": [ "gss delegation credential" ]
                     }
                 }
+            ],
+            "offlineUserSessions": [
+                {
+                    "userSessionId": "123",
+                    "data": "something1",
+                    "offlineClientSessions": [
+                        {
+                            "clientSessionId": "456",
+                            "client": "OtherApp",
+                            "data": "something2"
+                        }
+                    ]
+                }
             ]
         },
         {
diff --git a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index 644435a..916d855 100755
--- a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -82,7 +82,7 @@ public class JettySamlTest {
         list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
-        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
 
 
 
diff --git a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index c9323c4..c6092b4 100755
--- a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -81,7 +81,7 @@ public class JettySamlTest {
         list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
-        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
 
 
 
diff --git a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index ac09618..96709ad 100755
--- a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -81,7 +81,7 @@ public class JettySamlTest {
         list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
         list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
-        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
 
 
 
diff --git a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index 29a2e05..f9cb853 100755
--- a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -78,7 +78,7 @@ public class TomcatSamlTest {
         tomcat.addWebapp("/bad-client-sales-post-sig", new File(base, "bad-client-signed-post").toString());
         tomcat.addWebapp("/bad-realm-sales-post-sig", new File(base, "bad-realm-signed-post").toString());
         tomcat.addWebapp("/sales-post-enc", new File(base, "encrypted-post").toString());
-        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
 
 
         tomcat.start();
diff --git a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index 6e314ee..405c6ee 100755
--- a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -76,7 +76,7 @@ public class TomcatSamlTest {
         tomcat.addWebapp("/bad-client-sales-post-sig", new File(base, "bad-client-signed-post").toString());
         tomcat.addWebapp("/bad-realm-sales-post-sig", new File(base, "bad-realm-signed-post").toString());
         tomcat.addWebapp("/sales-post-enc", new File(base, "encrypted-post").toString());
-        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+        SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
 
 
         tomcat.start();