keycloak-aplcache

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

9/21/2015 7:06:33 AM

Changes

services/src/main/java/org/keycloak/services/managers/HttpAuthenticationChallenge.java 11(+0 -11)

Details

diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index 7a55d1e..f8f33be 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -29,6 +29,8 @@
         <class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
         <class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
         <class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
+        <class>org.keycloak.models.jpa.entities.OfflineUserSessionEntity</class>
+        <class>org.keycloak.models.jpa.entities.OfflineClientSessionEntity</class>
 
         <!-- JpaAuditProviders -->
         <class>org.keycloak.events.jpa.EventEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
new file mode 100644
index 0000000..061eaa3
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+    <changeSet author="mposolda@redhat.com" id="1.6.0">
+
+        <addColumn tableName="CLIENT">
+            <column name="OFFLINE_TOKENS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+
+        <createTable tableName="OFFLINE_USER_SESSION">
+            <column name="USER_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="USER_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="DATA" type="CLOB"/>
+        </createTable>
+
+        <createTable tableName="OFFLINE_CLIENT_SESSION">
+            <column name="USER_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="CLIENT_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="USER_SESSION_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="CLIENT_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="DATA" type="CLOB"/>
+        </createTable>
+
+        <addPrimaryKey columnNames="USER_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
+        <addPrimaryKey columnNames="CLIENT_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
+        <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_USER_SESSION" constraintName="FK_OFFLINE_US_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
+        <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
+        <addForeignKeyConstraint baseColumnNames="USER_SESSION_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_US_SES" referencedColumnNames="USER_SESSION_ID" referencedTableName="OFFLINE_USER_SESSION"/>
+
+    </changeSet>
+</databaseChangeLog>
\ No newline at end of file
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
index ca5d0e9..6cd96c6 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -9,4 +9,5 @@
     <include file="META-INF/jpa-changelog-1.3.0.xml"/>
     <include file="META-INF/jpa-changelog-1.4.0.xml"/>
     <include file="META-INF/jpa-changelog-1.5.0.xml"/>
+    <include file="META-INF/jpa-changelog-1.6.0.xml"/>
 </databaseChangeLog>
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 423bf3e..90e8c98 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -48,6 +48,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
             "org.keycloak.models.entities.AuthenticationFlowEntity",
             "org.keycloak.models.entities.AuthenticatorConfigEntity",
             "org.keycloak.models.entities.RequiredActionProviderEntity",
+            "org.keycloak.models.entities.OfflineUserSessionEntity",
+            "org.keycloak.models.entities.OfflineClientSessionEntity",
     };
 
     private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index e17d21f..022dadd 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -38,6 +38,9 @@ public interface OAuth2Constants {
     // https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03#section-2.2
     String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
 
+    // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
+    String OFFLINE_ACCESS = "offline_access";
+
 }
 
 
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index 020445e..42618a1 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -24,6 +24,7 @@ public class ClientRepresentation {
     protected Boolean bearerOnly;
     protected Boolean consentRequired;
     protected Boolean serviceAccountsEnabled;
+    protected Boolean offlineTokensEnabled;
     protected Boolean directGrantsOnly;
     protected Boolean publicClient;
     protected Boolean frontchannelLogout;
@@ -162,6 +163,14 @@ public class ClientRepresentation {
         this.serviceAccountsEnabled = serviceAccountsEnabled;
     }
 
+    public Boolean isOfflineTokensEnabled() {
+        return offlineTokensEnabled;
+    }
+
+    public void setOfflineTokensEnabled(Boolean offlineTokensEnabled) {
+        this.offlineTokensEnabled = offlineTokensEnabled;
+    }
+
     public Boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 7bc1edf..ff1ce68 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -1,6 +1,7 @@
 package org.keycloak.representations;
 
 import org.codehaus.jackson.annotate.JsonProperty;
+import org.keycloak.util.RefreshTokenUtil;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -11,9 +12,8 @@ import java.util.Map;
  */
 public class RefreshToken extends AccessToken {
 
-
     private RefreshToken() {
-        type("REFRESH");
+        type(RefreshTokenUtil.TOKEN_TYPE_REFRESH);
     }
 
     /**
diff --git a/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
new file mode 100644
index 0000000..0a759a5
--- /dev/null
+++ b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
@@ -0,0 +1,62 @@
+package org.keycloak.util;
+
+import java.io.IOException;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.RefreshToken;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RefreshTokenUtil {
+
+    public static final String TOKEN_TYPE_REFRESH = "REFRESH";
+
+    public static final String TOKEN_TYPE_OFFLINE = "OFFLINE";
+
+    public static boolean isOfflineTokenRequested(String scopeParam) {
+        if (scopeParam == null) {
+            return false;
+        }
+
+        String[] scopes = scopeParam.split(" ");
+        for (String scope : scopes) {
+            if (OAuth2Constants.OFFLINE_ACCESS.equals(scope)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * Return refresh token or offline otkne
+     *
+     * @param decodedToken
+     * @return
+     */
+    public static RefreshToken getRefreshToken(byte[] decodedToken) throws IOException {
+        return JsonSerialization.readValue(decodedToken, RefreshToken.class);
+    }
+
+    private static RefreshToken getRefreshToken(String refreshToken) throws IOException {
+        byte[] decodedToken = Base64Url.decode(refreshToken);
+        return getRefreshToken(decodedToken);
+    }
+
+    /**
+     * Return true if given refreshToken represents offline token
+     *
+     * @param refreshToken
+     * @return
+     */
+    public static boolean isOfflineToken(String refreshToken) {
+        try {
+            RefreshToken token = getRefreshToken(refreshToken);
+            return token.getType().equals(TOKEN_TYPE_OFFLINE);
+        } catch (IOException ioe) {
+            throw new RuntimeException(ioe);
+        }
+    }
+
+}
diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index ff7f061..10cb89d 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -898,7 +898,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
 }
 ]]></programlisting>
 
-                                where the <literal>mysecret</literal> needs to be replaced with the real value of client secret. You can obtain it from client admin console.
+                                where the <literal>mysecret</literal> needs to be replaced with the real value of client secret. You can obtain it from admin console from client configuration.
                             </para>
                         </listitem>
                     </varlistentry>
@@ -906,7 +906,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
                         <term>Authentication with signed JWT</term>
                         <listitem>
                             <para>
-                                This is based on the <ulink url="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">JWT Bearer Token Profiles for OAuth 2.0</ulink> specification.
+                                This is based on the <ulink url="https://tools.ietf.org/html/rfc7523">JWT Bearer Token Profiles for OAuth 2.0</ulink> specification.
                                 The client/adapter generates the <ulink url="https://tools.ietf.org/html/rfc7519">JWT</ulink> and signs it with his private key.
                                 The Keycloak then verifies the signed JWT with the client's public key and authenticates client based on it.
                             </para>
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index b7ec677..b9c5338 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -20,6 +20,7 @@ public interface Details {
     String REMEMBER_ME = "remember_me";
     String TOKEN_ID = "token_id";
     String REFRESH_TOKEN_ID = "refresh_token_id";
+    String REFRESH_TOKEN_TYPE = "refresh_token_type";
     String VALIDATE_ACCESS_TOKEN = "validate_access_token";
     String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
     String NODE_HOST = "node_host";
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 9d9035a..63e04db 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
@@ -1,9 +1,11 @@
 package org.keycloak.account.freemarker.model;
 
+import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
+import org.keycloak.OAuth2Constants;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.ProtocolMapperModel;
@@ -11,6 +13,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.services.offline.OfflineUserSessionManager;
 import org.keycloak.util.MultivaluedHashMap;
 
 /**
@@ -21,6 +24,9 @@ public class ApplicationsBean {
     private List<ApplicationEntry> applications = new LinkedList<ApplicationEntry>();
 
     public ApplicationsBean(RealmModel realm, UserModel user) {
+
+        Set<ClientModel> offlineClients = new OfflineUserSessionManager().findClientsWithOfflineToken(realm, user);
+
         List<ClientModel> realmClients = realm.getClients();
         for (ClientModel client : realmClients) {
             // Don't show bearerOnly clients
@@ -52,7 +58,13 @@ public class ApplicationsBean {
                 }
             }
 
-            ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client, claimsGranted);
+            List<String> additionalGrants = new ArrayList<>();
+            if (offlineClients.contains(client)) {
+                additionalGrants.add("${offlineAccess}");
+            }
+
+            ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client,
+                    claimsGranted, additionalGrants);
             applications.add(appEntry);
         }
     }
@@ -82,16 +94,18 @@ public class ApplicationsBean {
         private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted;
         private final ClientModel client;
         private final List<String> claimsGranted;
+        private final List<String> additionalGrants;
 
         public ApplicationEntry(List<RoleModel> realmRolesAvailable, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable,
                                 List<RoleModel> realmRolesGranted, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted,
-                                ClientModel client, List<String> claimsGranted) {
+                                ClientModel client, List<String> claimsGranted, List<String> additionalGrants) {
             this.realmRolesAvailable = realmRolesAvailable;
             this.resourceRolesAvailable = resourceRolesAvailable;
             this.realmRolesGranted = realmRolesGranted;
             this.resourceRolesGranted = resourceRolesGranted;
             this.client = client;
             this.claimsGranted = claimsGranted;
+            this.additionalGrants = additionalGrants;
         }
 
         public List<RoleModel> getRealmRolesAvailable() {
@@ -118,6 +132,9 @@ public class ApplicationsBean {
             return claimsGranted;
         }
 
+        public List<String> getAdditionalGrants() {
+            return additionalGrants;
+        }
     }
 
     // Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker...
diff --git a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
index 4e01c02..b2bbdf2 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
@@ -18,6 +18,7 @@
                 <td>${msg("availablePermissions")}</td>
                 <td>${msg("grantedPermissions")}</td>
                 <td>${msg("grantedPersonalInfo")}</td>
+                <td>${msg("additionalGrants")}</td>
                 <td>${msg("action")}</td>
               </tr>
             </thead>
@@ -76,7 +77,13 @@
                     </td>
 
                     <td>
-                        <#if application.client.consentRequired && application.claimsGranted?has_content>
+                       <#list application.additionalGrants as grant>
+                            ${advancedMsg(grant)}<#if grant_has_next>, </#if>
+                        </#list>
+                    </td>
+
+                    <td>
+                        <#if (application.client.consentRequired && application.claimsGranted?has_content) || application.additionalGrants?has_content>
                             <button type='submit' class='${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}' id='revoke-${application.client.clientId}' name='clientId' value="${application.client.id}">${msg("revoke")}</button>
                         </#if>
                     </td>
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 2c3b8c7..b0ea19c 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -85,9 +85,11 @@ application=Application
 availablePermissions=Available Permissions
 grantedPermissions=Granted Permissions
 grantedPersonalInfo=Granted Personal Info
+additionalGrants=Additional Grants
 action=Action
 inResource=in
 fullAccess=Full Access
+offlineAccess=Offline Access
 revoke=Revoke Grant
 
 configureAuthenticators=Configured Authenticators
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 06c939b..303e439 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -79,6 +79,13 @@
                     <input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
                 </div>
             </div>
+            <div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.bearerOnly">
+                <label class="col-md-2 control-label" for="offlineTokensEnabled">Offline Tokens Enabled</label>
+                <kc-tooltip>Allows you to retrieve offline tokens for users. Offline token can be stored by client application and is valid even if user is not logged anymore.</kc-tooltip>
+                <div class="col-md-6">
+                    <input ng-model="client.offlineTokensEnabled" name="offlineTokensEnabled" id="offlineTokensEnabled" onoffswitch />
+                </div>
+            </div>
             <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
                 <label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
                 <div class="col-sm-6">
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
index 80a0c4d..2c6a92d 100644
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
@@ -14,7 +14,7 @@ import org.keycloak.adapters.KeycloakDeployment;
  *
  * You must specify a file
  * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
- * if you want to share the implementation among more WARs). This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes
+ * if you want to share the implementation among more WARs).
  *
  * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support
  * authentication with client certificate)
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
index d68c7cb..1c8907e 100644
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
@@ -13,7 +13,7 @@ import org.keycloak.util.Time;
 
 /**
  * Client authentication based on JWT signed by client private key .
- * See <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> for more details.
+ * See <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
index d91b134..3189ab2 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
@@ -135,6 +135,9 @@ public class OAuthRequestAuthenticator {
         String idpHint = getQueryParamValue(AdapterConstants.KC_IDP_HINT);
         url = UriUtils.stripQueryParam(url, AdapterConstants.KC_IDP_HINT);
 
+        String scope = getQueryParamValue(OAuth2Constants.SCOPE);
+        url = UriUtils.stripQueryParam(url, OAuth2Constants.SCOPE);
+
         KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
                 .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
                 .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
@@ -147,6 +150,9 @@ public class OAuthRequestAuthenticator {
         if (idpHint != null && idpHint.length() > 0) {
             redirectUriBuilder.queryParam(AdapterConstants.KC_IDP_HINT,idpHint);
         }
+        if (scope != null && scope.length() > 0) {
+            redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, scope);
+        }
 
         return redirectUriBuilder.build().toString();
     }
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
index 52668de..b938a0b 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
@@ -117,7 +117,12 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext 
         }
 
         this.token = token;
-        this.refreshToken = response.getRefreshToken();
+        if (response.getRefreshToken() != null) {
+            if (log.isTraceEnabled()) {
+                log.trace("Setup new refresh token to the security context");
+            }
+            this.refreshToken = response.getRefreshToken();
+        }
         this.tokenString = tokenString;
         tokenStore.refreshCallback(this);
         return true;
diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js
index 46d3b18..f384e7b 100755
--- a/integration/js/src/main/resources/keycloak.js
+++ b/integration/js/src/main/resources/keycloak.js
@@ -164,6 +164,10 @@
                 url += '&kc_idp_hint=' + options.idpHint;
             }
 
+            if (options && options.scope) {
+                url += '&scope=' + options.scope;
+            }
+
             return url;
         }
 
diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java
index daccf8e..6461a33 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -109,6 +109,9 @@ public interface ClientModel extends RoleContainerModel {
     boolean isServiceAccountsEnabled();
     void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
 
+    boolean isOfflineTokensEnabled();
+    void setOfflineTokensEnabled(boolean offlineTokensEnabled);
+
     Set<RoleModel> getScopeMappings();
     void addScopeMapping(RoleModel role);
     void deleteScopeMapping(RoleModel role);
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index 52e9721..83fa12d 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -28,6 +28,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     private boolean bearerOnly;
     private boolean consentRequired;
     private boolean serviceAccountsEnabled;
+    private boolean offlineTokensEnabled;
     private boolean directGrantsOnly;
     private int nodeReRegistrationTimeout;
 
@@ -228,6 +229,14 @@ public class ClientEntity extends AbstractIdentifiableEntity {
         this.serviceAccountsEnabled = serviceAccountsEnabled;
     }
 
+    public boolean isOfflineTokensEnabled() {
+        return offlineTokensEnabled;
+    }
+
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        this.offlineTokensEnabled = offlineTokensEnabled;
+    }
+
     public boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
diff --git a/model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java
new file mode 100644
index 0000000..69ad60b
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/entities/OfflineClientSessionEntity.java
@@ -0,0 +1,35 @@
+package org.keycloak.models.entities;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineClientSessionEntity {
+
+    private String clientSessionId;
+    private String clientId;
+    private String data;
+
+    public String getClientSessionId() {
+        return clientSessionId;
+    }
+
+    public void setClientSessionId(String clientSessionId) {
+        this.clientSessionId = clientSessionId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java
new file mode 100644
index 0000000..e785898
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/entities/OfflineUserSessionEntity.java
@@ -0,0 +1,37 @@
+package org.keycloak.models.entities;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionEntity {
+
+    private String userSessionId;
+    private String data;
+    private List<OfflineClientSessionEntity> 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<OfflineClientSessionEntity> getOfflineClientSessions() {
+        return offlineClientSessions;
+    }
+
+    public void setOfflineClientSessions(List<OfflineClientSessionEntity> offlineClientSessions) {
+        this.offlineClientSessions = offlineClientSessions;
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index 8c82a8e..66020db 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
@@ -28,6 +28,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
     private List<FederatedIdentityEntity> federatedIdentities;
     private String federationLink;
     private String serviceAccountClientLink;
+    private List<OfflineUserSessionEntity> offlineUserSessions;
 
     public String getUsername() {
         return username;
@@ -157,5 +158,13 @@ public class UserEntity extends AbstractIdentifiableEntity {
     public void setServiceAccountClientLink(String serviceAccountClientLink) {
         this.serviceAccountClientLink = serviceAccountClientLink;
     }
+
+    public List<OfflineUserSessionEntity> getOfflineUserSessions() {
+        return offlineUserSessions;
+    }
+
+    public void setOfflineUserSessions(List<OfflineUserSessionEntity> offlineUserSessions) {
+        this.offlineUserSessions = offlineUserSessions;
+    }
 }
 
diff --git a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
new file mode 100644
index 0000000..47ae23a
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
@@ -0,0 +1,44 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineClientSessionModel {
+
+    private String clientSessionId;
+    private String userSessionId;
+    private String clientId;
+    private String data;
+
+    public String getClientSessionId() {
+        return clientSessionId;
+    }
+
+    public void setClientSessionId(String clientSessionId) {
+        this.clientSessionId = clientSessionId;
+    }
+
+    public String getUserSessionId() {
+        return userSessionId;
+    }
+
+    public void setUserSessionId(String userSessionId) {
+        this.userSessionId = userSessionId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java b/model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java
new file mode 100644
index 0000000..9907783
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/OfflineUserSessionModel.java
@@ -0,0 +1,26 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionModel {
+
+    private String userSessionId;
+    private String data;
+
+    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;
+    }
+}
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 3282e61..51e8fc4 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -1,5 +1,6 @@
 package org.keycloak.models;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -113,6 +114,15 @@ 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/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
index ff3f19a..12ebd70 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -40,6 +40,7 @@ public interface UserSessionModel {
     public String getNote(String name);
     public void setNote(String name, String value);
     public void removeNote(String name);
+    public Map<String, String> getNotes();
 
     State getState();
     void setState(State state);
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 bf2360e..a382880 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
@@ -303,6 +303,7 @@ public class ModelToRepresentation {
         rep.setBearerOnly(clientModel.isBearerOnly());
         rep.setConsentRequired(clientModel.isConsentRequired());
         rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
+        rep.setOfflineTokensEnabled(clientModel.isOfflineTokensEnabled());
         rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
         rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
         rep.setBaseUrl(clientModel.getBaseUrl());
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 811655b..3907f96 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
@@ -692,6 +692,7 @@ public class RepresentationToModel {
         if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
         if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
         if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
+        if (resourceRep.isOfflineTokensEnabled() != null) client.setOfflineTokensEnabled(resourceRep.isOfflineTokensEnabled());
         if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
         if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
         if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
@@ -788,6 +789,7 @@ public class RepresentationToModel {
         if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
         if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
         if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
+        if (rep.isOfflineTokensEnabled() != null) resource.setOfflineTokensEnabled(rep.isOfflineTokensEnabled());
         if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
         if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
         if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
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 4cd162b..f727c75 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
@@ -1,12 +1,15 @@
 package org.keycloak.models.utils;
 
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -255,4 +258,44 @@ 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/ClientAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
index 8003b70..366dc31 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
@@ -462,6 +462,16 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isOfflineTokensEnabled() {
+        return entity.isOfflineTokensEnabled();
+    }
+
+    @Override
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        entity.setOfflineTokensEnabled(offlineTokensEnabled);
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return entity.isDirectGrantsOnly();
     }
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 7461cbd..d89ce63 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
@@ -22,7 +22,10 @@ import org.keycloak.models.ClientModel;
 import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
 
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
 import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
@@ -32,6 +35,8 @@ import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.entities.CredentialEntity;
 import org.keycloak.models.entities.FederatedIdentityEntity;
+import org.keycloak.models.entities.OfflineClientSessionEntity;
+import org.keycloak.models.entities.OfflineUserSessionEntity;
 import org.keycloak.models.entities.RoleEntity;
 import org.keycloak.models.entities.UserEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -39,6 +44,7 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.util.Time;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -216,7 +222,7 @@ public class UserAdapter implements UserModel, Comparable {
 
     @Override
     public Map<String, List<String>> getAttributes() {
-        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
+        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes());
     }
 
     @Override
@@ -569,6 +575,142 @@ public class UserAdapter implements UserModel, Comparable {
     }
 
     @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) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 9f79b15..582e5f1 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -431,6 +431,18 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isOfflineTokensEnabled() {
+        if (updated != null) return updated.isOfflineTokensEnabled();
+        return cached.isOfflineTokensEnabled();
+    }
+
+    @Override
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        getDelegateForUpdate();
+        updated.setOfflineTokensEnabled(offlineTokensEnabled);
+    }
+
+    @Override
     public RoleModel getRole(String name) {
         if (updated != null) return updated.getRole(name);
         String id = cached.getRoles().get(name);
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 68b31ed..df7c144 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
@@ -319,6 +319,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
 
     @Override
     public void preRemove(RealmModel realm, ClientModel client) {
+        realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, client);
     }
 
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 5a74b01..769f1b4 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,4 +348,52 @@ 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/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index 11447d0..b90c077 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -47,6 +47,7 @@ public class CachedClient implements Serializable {
     private boolean bearerOnly;
     private boolean consentRequired;
     private boolean serviceAccountsEnabled;
+    private boolean offlineTokensEnabled;
     private Map<String, String> roles = new HashMap<String, String>();
     private int nodeReRegistrationTimeout;
     private Map<String, Integer> registeredNodes;
@@ -81,6 +82,7 @@ public class CachedClient implements Serializable {
         bearerOnly = model.isBearerOnly();
         consentRequired = model.isConsentRequired();
         serviceAccountsEnabled = model.isServiceAccountsEnabled();
+        offlineTokensEnabled = model.isOfflineTokensEnabled();
         for (RoleModel role : model.getRoles()) {
             roles.put(role.getName(), role.getId());
             cache.addCachedRole(new CachedClientRole(id, role, realm));
@@ -189,6 +191,10 @@ public class CachedClient implements Serializable {
         return serviceAccountsEnabled;
     }
 
+    public boolean isOfflineTokensEnabled() {
+        return offlineTokensEnabled;
+    }
+
     public Map<String, String> getRoles() {
         return roles;
     }
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 9757c63..d38b6f9 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
@@ -1,5 +1,7 @@
 package org.keycloak.models.cache.entities;
 
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialValueModel;
@@ -7,9 +9,11 @@ import org.keycloak.models.UserModel;
 import org.keycloak.util.MultivaluedHashMap;
 
 import java.io.Serializable;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -33,6 +37,8 @@ public class CachedUser implements Serializable {
     private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
     private Set<String> requiredActions = new HashSet<>();
     private Set<String> roleMappings = new HashSet<String>();
+    private Map<String, OfflineUserSessionModel> offlineUserSessions = new HashMap<>();
+    private Map<String, OfflineClientSessionModel> offlineClientSessions = new HashMap<>();
 
     public CachedUser(RealmModel realm, UserModel user) {
         this.id = user.getId();
@@ -53,6 +59,12 @@ public class CachedUser implements Serializable {
         for (RoleModel role : user.getRoleMappings()) {
             roleMappings.add(role.getId());
         }
+        for (OfflineUserSessionModel offlineSession : user.getOfflineUserSessions()) {
+            offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession);
+        }
+        for (OfflineClientSessionModel offlineSession : user.getOfflineClientSessions()) {
+            offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession);
+        }
     }
 
     public String getId() {
@@ -118,4 +130,12 @@ public class CachedUser implements Serializable {
     public String getServiceAccountClientLink() {
         return serviceAccountClientLink;
     }
+
+    public Map<String, OfflineUserSessionModel> getOfflineUserSessions() {
+        return offlineUserSessions;
+    }
+
+    public Map<String, OfflineClientSessionModel> getOfflineClientSessions() {
+        return offlineClientSessions;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index b0acc41..9fc7377 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -482,6 +482,16 @@ public class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isOfflineTokensEnabled() {
+        return entity.isOfflineTokensEnabled();
+    }
+
+    @Override
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        entity.setOfflineTokensEnabled(offlineTokensEnabled);
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return entity.isDirectGrantsOnly();
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 1f6ac25..d853cb3 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -100,6 +100,9 @@ public class ClientEntity {
     @Column(name="SERVICE_ACCOUNTS_ENABLED")
     private boolean serviceAccountsEnabled;
 
+    @Column(name="OFFLINE_TOKENS_ENABLED")
+    private boolean offlineTokensEnabled;
+
     @Column(name="NODE_REREG_TIMEOUT")
     private int nodeReRegistrationTimeout;
 
@@ -316,6 +319,14 @@ public class ClientEntity {
         this.serviceAccountsEnabled = serviceAccountsEnabled;
     }
 
+    public boolean isOfflineTokensEnabled() {
+        return offlineTokensEnabled;
+    }
+
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        this.offlineTokensEnabled = offlineTokensEnabled;
+    }
+
     public boolean isDirectGrantsOnly() {
         return directGrantsOnly;
     }
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
new file mode 100644
index 0000000..23081c4
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
@@ -0,0 +1,81 @@
+package org.keycloak.models.jpa.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@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")
+})
+@Table(name="OFFLINE_CLIENT_SESSION")
+@Entity
+public class OfflineClientSessionEntity {
+
+    @Id
+    @Column(name="CLIENT_SESSION_ID", length = 36)
+    protected String clientSessionId;
+
+    @Column(name="USER_SESSION_ID", length = 36)
+    protected String userSessionId;
+
+    @Column(name="CLIENT_ID", length = 36)
+    protected String clientId;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name="USER_ID")
+    protected UserEntity user;
+
+    @Column(name="DATA")
+    protected String data;
+
+    public String getClientSessionId() {
+        return clientSessionId;
+    }
+
+    public void setClientSessionId(String clientSessionId) {
+        this.clientSessionId = clientSessionId;
+    }
+
+    public String getUserSessionId() {
+        return userSessionId;
+    }
+
+    public void setUserSessionId(String userSessionId) {
+        this.userSessionId = userSessionId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public UserEntity getUser() {
+        return user;
+    }
+
+    public void setUser(UserEntity user) {
+        this.user = user;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java
new file mode 100644
index 0000000..d2726fa
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineUserSessionEntity.java
@@ -0,0 +1,59 @@
+package org.keycloak.models.jpa.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+        @NamedQuery(name="deleteOfflineUserSessionsByRealm", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"),
+        @NamedQuery(name="deleteOfflineUserSessionsByRealmAndLink", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
+        @NamedQuery(name="deleteDetachedOfflineUserSessions", query="delete from OfflineUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from OfflineClientSessionEntity c)")
+})
+@Table(name="OFFLINE_USER_SESSION")
+@Entity
+public class OfflineUserSessionEntity {
+
+    @Id
+    @Column(name="USER_SESSION_ID", length = 36)
+    protected String userSessionId;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name="USER_ID")
+    protected UserEntity user;
+
+    @Column(name="DATA")
+    protected String data;
+
+    public String getUserSessionId() {
+        return userSessionId;
+    }
+
+    public void setUserSessionId(String userSessionId) {
+        this.userSessionId = userSessionId;
+    }
+
+    public UserEntity getUser() {
+        return user;
+    }
+
+    public void setUser(UserEntity user) {
+        this.user = user;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 2da1641..24d23db 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -3,9 +3,13 @@ package org.keycloak.models.jpa.entities;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
 import javax.persistence.Column;
+import javax.persistence.ElementCollection;
 import javax.persistence.Entity;
 import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.MapKeyColumn;
 import javax.persistence.NamedQueries;
 import javax.persistence.NamedQuery;
 import javax.persistence.OneToMany;
@@ -14,6 +18,8 @@ import javax.persistence.UniqueConstraint;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -83,6 +89,12 @@ public class UserEntity {
     @Column(name="SERVICE_ACCOUNT_CLIENT_LINK")
     protected String serviceAccountClientLink;
 
+    @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
+    protected Collection<OfflineUserSessionEntity> offlineUserSessions = new ArrayList<>();
+
+    @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
+    protected Collection<OfflineClientSessionEntity> offlineClientSessions = new ArrayList<>();
+
     public String getId() {
         return id;
     }
@@ -212,6 +224,22 @@ public class UserEntity {
         this.serviceAccountClientLink = serviceAccountClientLink;
     }
 
+    public Collection<OfflineUserSessionEntity> getOfflineUserSessions() {
+        return offlineUserSessions;
+    }
+
+    public void setOfflineUserSessions(Collection<OfflineUserSessionEntity> offlineUserSessions) {
+        this.offlineUserSessions = offlineUserSessions;
+    }
+
+    public Collection<OfflineClientSessionEntity> getOfflineClientSessions() {
+        return offlineClientSessions;
+    }
+
+    public void setOfflineClientSessions(Collection<OfflineClientSessionEntity> offlineClientSessions) {
+        this.offlineClientSessions = offlineClientSessions;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
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 d92e93d..8cee4ea 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
@@ -169,6 +169,10 @@ public class JpaUserProvider implements UserProvider {
                 .setParameter("realmId", realm.getId()).executeUpdate();
         num = em.createNamedQuery("deleteUserAttributesByRealm")
                 .setParameter("realmId", realm.getId()).executeUpdate();
+        num = em.createNamedQuery("deleteOfflineClientSessionsByRealm")
+                .setParameter("realmId", realm.getId()).executeUpdate();
+        num = em.createNamedQuery("deleteOfflineUserSessionsByRealm")
+                .setParameter("realmId", realm.getId()).executeUpdate();
         num = em.createNamedQuery("deleteUsersByRealm")
                 .setParameter("realmId", realm.getId()).executeUpdate();
     }
@@ -195,6 +199,14 @@ public class JpaUserProvider implements UserProvider {
                 .setParameter("realmId", realm.getId())
                 .setParameter("link", link.getId())
                 .executeUpdate();
+        num = em.createNamedQuery("deleteOfflineClientSessionsByRealmAndLink")
+                .setParameter("realmId", realm.getId())
+                .setParameter("link", link.getId())
+                .executeUpdate();
+        num = em.createNamedQuery("deleteOfflineUserSessionsByRealmAndLink")
+                .setParameter("realmId", realm.getId())
+                .setParameter("link", link.getId())
+                .executeUpdate();
         num = em.createNamedQuery("deleteUsersByRealmAndLink")
                 .setParameter("realmId", realm.getId())
                 .setParameter("link", link.getId())
@@ -212,6 +224,8 @@ public class JpaUserProvider implements UserProvider {
         em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
         em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
         em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+        em.createNamedQuery("deleteOfflineClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
+        em.createNamedQuery("deleteDetachedOfflineUserSessions").executeUpdate();
     }
 
     @Override
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 67def6b..bdcf1f1 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
@@ -2,6 +2,8 @@ package org.keycloak.models.jpa;
 
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.ModelDuplicateException;
@@ -14,6 +16,8 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.jpa.entities.CredentialEntity;
+import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
+import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
 import org.keycloak.models.jpa.entities.UserConsentEntity;
 import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
 import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
@@ -37,6 +41,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -751,6 +756,124 @@ 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/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index 26effca..2cb863e 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -484,6 +484,17 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
     }
 
     @Override
+    public boolean isOfflineTokensEnabled() {
+        return getMongoEntity().isOfflineTokensEnabled();
+    }
+
+    @Override
+    public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
+        getMongoEntity().setOfflineTokensEnabled(offlineTokensEnabled);
+        updateMongoEntity();
+    }
+
+    @Override
     public boolean isDirectGrantsOnly() {
         return getMongoEntity().isDirectGrantsOnly();
     }
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 308d9fb..14081c1 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
@@ -19,6 +19,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.mongo.keycloak.entities.MongoUserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
 import org.keycloak.models.utils.CredentialValidation;
@@ -399,6 +401,36 @@ public class MongoUserProvider implements UserProvider {
                 .and("clientId").is(client.getId())
                 .get();
         getMongoStore().removeEntities(MongoUserConsentEntity.class, query, false, invocationContext);
+
+        // Remove all offlineClientSessions
+        query = new QueryBuilder()
+                .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
+                .get();
+        List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
+        for (MongoUserEntity user : users) {
+            boolean anyRemoved = false;
+            for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+                for (OfflineClientSessionEntity clientSession : userSession.getOfflineClientSessions()) {
+                    if (clientSession.getClientId().equals(client.getId())) {
+                        userSession.getOfflineClientSessions().remove(clientSession);
+                        anyRemoved = true;
+                        break;
+                    }
+                }
+
+                // Check if it was last clientSession. Then remove userSession too
+                if (userSession.getOfflineClientSessions().size() == 0) {
+                    user.getOfflineUserSessions().remove(userSession);
+                    anyRemoved = true;
+                    break;
+                }
+            }
+
+            if (anyRemoved) {
+                getMongoStore().updateEntity(user, invocationContext);
+            }
+
+        }
     }
 
     @Override
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 8130ff5..a475bb6 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
@@ -8,6 +8,8 @@ import com.mongodb.QueryBuilder;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.KeycloakSession;
@@ -20,6 +22,8 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.entities.CredentialEntity;
+import org.keycloak.models.entities.OfflineClientSessionEntity;
+import org.keycloak.models.entities.OfflineUserSessionEntity;
 import org.keycloak.models.entities.UserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
@@ -30,6 +34,7 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.util.Time;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -628,6 +633,145 @@ 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/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
index fbe8682..a9db618 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
@@ -10,6 +10,7 @@ import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity
 
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -144,5 +145,8 @@ public class UserSessionAdapter implements UserSessionModel {
 
     }
 
-
+    @Override
+    public Map<String, String> getNotes() {
+        return entity.getNotes();
+    }
 }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index 6cfc121..c7104fb 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -14,6 +14,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -110,6 +111,11 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public Map<String, String> getNotes() {
+        return entity.getNotes();
+    }
+
+    @Override
     public State getState() {
         return entity.getState();
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index 0c308ab..96f81cb 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -27,7 +27,7 @@ import org.keycloak.services.Urls;
 
 /**
  * Client authentication based on JWT signed by client private key .
- * See <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> for more details.
+ * See <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
  *
  * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by
  * org.keycloak.adapters.authentication.JWTClientCredentialsProvider
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 15c1f46..00970e2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -262,11 +262,14 @@ public class TokenEndpoint {
 
         AccessTokenResponse res;
         try {
-            res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers);
+            TokenManager.RefreshResult result = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers);
+            res = result.getResponse();
 
-            UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
-            updateClientSessions(userSession.getClientSessions());
-            updateUserSessionFromClientAuth(userSession);
+            if (!result.isOfflineToken()) {
+                UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
+                updateClientSessions(userSession.getClientSessions());
+                updateUserSessionFromClientAuth(userSession);
+            }
 
         } catch (OAuthErrorException e) {
             event.error(Errors.INVALID_TOKEN);
@@ -337,6 +340,8 @@ public class TokenEndpoint {
         clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
         clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
         clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+        clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
+
         AuthenticationFlowModel flow = realm.getDirectGrantFlow();
         String flowId = flow.getId();
         AuthenticationProcessor processor = new AuthenticationProcessor();
@@ -363,7 +368,7 @@ public class TokenEndpoint {
         updateUserSessionFromClientAuth(userSession);
 
         AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
-                .generateAccessToken(session, scope, client, user, userSession, clientSession)
+                .generateAccessToken()
                 .generateRefreshToken()
                 .generateIDToken()
                 .build();
@@ -415,6 +420,7 @@ public class TokenEndpoint {
         ClientSessionModel clientSession = sessions.createClientSession(realm, client);
         clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
         clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+        clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
 
         UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
         event.session(userSession);
@@ -429,7 +435,7 @@ public class TokenEndpoint {
         updateUserSessionFromClientAuth(userSession);
 
         AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
-                .generateAccessToken(session, scope, client, clientUser, userSession, clientSession)
+                .generateAccessToken()
                 .generateRefreshToken()
                 .generateIDToken()
                 .build();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index afd2a8a..714c0d1 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -20,7 +20,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
 
     public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
 
-    public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD);
+    public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
 
     public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE);
 
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 5ee8191..46d995f 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -3,8 +3,8 @@ package org.keycloak.protocol.oidc;
 import org.jboss.logging.Logger;
 import org.keycloak.ClientConnection;
 import org.keycloak.OAuthErrorException;
-import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.jose.jws.JWSInput;
@@ -27,11 +27,15 @@ import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
 import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.offline.OfflineUserSessionManager;
+import org.keycloak.util.RefreshTokenUtil;
 import org.keycloak.util.Time;
 
 import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.io.IOException;
 import java.util.HashSet;
@@ -85,16 +89,31 @@ public class TokenManager {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
         }
 
-        UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
-        if (!AuthenticationManager.isSessionValid(realm, userSession)) {
-            AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
-            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
-        }
+        UserSessionModel userSession = null;
         ClientSessionModel clientSession = null;
-        for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) {
-            if (clientSessionModel.getId().equals(oldToken.getClientSession())) {
-                clientSession = clientSessionModel;
-                break;
+        if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
+            // Check if offline tokens still allowed for the client
+            clientSession = new OfflineUserSessionManager().findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
+            if (clientSession != null) {
+                if (!clientSession.getClient().isOfflineTokensEnabled()) {
+                    throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline tokens not allowed for client", "Offline tokens not allowed for client");
+                }
+
+                userSession = clientSession.getUserSession();
+            }
+        } else {
+            // Find userSession regularly for online tokens
+            userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
+            if (!AuthenticationManager.isSessionValid(realm, userSession)) {
+                AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
+                throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
+            }
+
+            for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) {
+                if (clientSessionModel.getId().equals(oldToken.getClientSession())) {
+                    clientSession = clientSessionModel;
+                    break;
+                }
             }
         }
 
@@ -126,10 +145,12 @@ public class TokenManager {
 
     }
 
-    public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
+    public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
         RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
 
-        event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
+        event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
 
         TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers);
         // validate authorizedClient is same as validated client
@@ -140,11 +161,17 @@ public class TokenManager {
         int currentTime = Time.currentTime();
         validation.userSession.setLastSessionRefresh(currentTime);
 
-        AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
+        AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
                 .accessToken(validation.newToken)
-                .generateIDToken()
-                .generateRefreshToken().build();
-        return res;
+                .generateIDToken();
+
+        // Don't generate refresh token again if refresh was triggered with offline token
+        if (!refreshToken.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE)) {
+            responseBuilder.generateRefreshToken();
+        }
+
+        AccessTokenResponse res = responseBuilder.build();
+        return new RefreshResult(res, RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
     }
 
     public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
@@ -158,7 +185,7 @@ public class TokenManager {
         } catch (Exception e) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
         }
-        if (refreshToken.isExpired()) {
+        if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
             throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
         }
 
@@ -172,18 +199,18 @@ public class TokenManager {
         IDToken idToken = null;
         try {
             if (!RSAProvider.verify(jws, realm.getPublicKey())) {
-                throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
+                throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
             }
             idToken = jws.readJsonContent(IDToken.class);
         } catch (IOException e) {
-            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
+            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
         }
         if (idToken.isExpired()) {
-            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
+            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
         }
 
         if (idToken.getIssuedAt() < realm.getNotBefore()) {
-            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
+            throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
         }
         return idToken;
     }
@@ -250,9 +277,7 @@ public class TokenManager {
         if (client.isFullScopeAllowed()) return roleMappings;
 
         Set<RoleModel> scopeMappings = client.getScopeMappings();
-        if (client instanceof ClientModel) {
-            scopeMappings.addAll(((ClientModel) client).getRoles());
-        }
+        scopeMappings.addAll(client.getRoles());
 
         for (RoleModel role : roleMappings) {
             for (RoleModel desiredRole : scopeMappings) {
@@ -409,7 +434,9 @@ public class TokenManager {
             return this;
         }
 
-        public AccessTokenResponseBuilder generateAccessToken(KeycloakSession session, String scopeParam, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
+        public AccessTokenResponseBuilder generateAccessToken() {
+            UserModel user = userSession.getUser();
+            String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
             Set<RoleModel> requestedRoles = getAccess(scopeParam, client, user);
             accessToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
             return this;
@@ -419,10 +446,24 @@ public class TokenManager {
             if (accessToken == null) {
                 throw new IllegalStateException("accessToken not set");
             }
-            refreshToken = new RefreshToken(accessToken);
+
+            String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
+            boolean offlineTokenRequested = RefreshTokenUtil.isOfflineTokenRequested(scopeParam);
+            if (offlineTokenRequested) {
+                if (!clientSession.getClient().isOfflineTokensEnabled()) {
+                    event.error(Errors.INVALID_CLIENT);
+                    throw new ErrorResponseException("invalid_client", "Offline tokens not allowed for the client", Response.Status.BAD_REQUEST);
+                }
+
+                refreshToken = new RefreshToken(accessToken);
+                refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+                new OfflineUserSessionManager().persistOfflineSession(clientSession, userSession);
+            } else {
+                refreshToken = new RefreshToken(accessToken);
+                refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
+            }
             refreshToken.id(KeycloakModelUtils.generateId());
             refreshToken.issuedNow();
-            refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
             return this;
         }
 
@@ -459,6 +500,7 @@ public class TokenManager {
                 } else {
                     event.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
                 }
+                event.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
             }
 
             AccessTokenResponse res = new AccessTokenResponse();
@@ -489,4 +531,23 @@ public class TokenManager {
         }
     }
 
+    public class RefreshResult {
+
+        private final AccessTokenResponse response;
+        private final boolean offlineToken;
+
+        private RefreshResult(AccessTokenResponse response, boolean offlineToken) {
+            this.response = response;
+            this.offlineToken = offlineToken;
+        }
+
+        public AccessTokenResponse getResponse() {
+            return response;
+        }
+
+        public boolean isOfflineToken() {
+            return offlineToken;
+        }
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
new file mode 100644
index 0000000..50abfd4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
@@ -0,0 +1,289 @@
+package org.keycloak.services.offline;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineClientSessionAdapter implements ClientSessionModel {
+
+    private final OfflineClientSessionModel model;
+    private final RealmModel realm;
+    private final ClientModel client;
+    private final OfflineUserSessionAdapter userSession;
+
+    private OfflineClientSessionData data;
+
+    public OfflineClientSessionAdapter(OfflineClientSessionModel model, RealmModel realm, ClientModel client, OfflineUserSessionAdapter userSession) {
+        this.model = model;
+        this.realm = realm;
+        this.client = client;
+        this.userSession = userSession;
+    }
+
+    // lazily init representation
+    private OfflineClientSessionData getData() {
+        if (data == null) {
+            try {
+                data = JsonSerialization.readValue(model.getData(), OfflineClientSessionData.class);
+            } catch (IOException ioe) {
+                throw new ModelException(ioe);
+            }
+        }
+
+        return data;
+    }
+
+    @Override
+    public String getId() {
+        return model.getClientSessionId();
+    }
+
+    @Override
+    public RealmModel getRealm() {
+        return realm;
+    }
+
+    @Override
+    public ClientModel getClient() {
+        return client;
+    }
+
+    @Override
+    public UserSessionModel getUserSession() {
+        return userSession;
+    }
+
+    @Override
+    public void setUserSession(UserSessionModel userSession) {
+        throw new IllegalStateException("Not supported setUserSession");
+    }
+
+    @Override
+    public String getRedirectUri() {
+        return data.getRedirectUri();
+    }
+
+    @Override
+    public void setRedirectUri(String uri) {
+        throw new IllegalStateException("Not supported setRedirectUri");
+    }
+
+    @Override
+    public int getTimestamp() {
+        return 0;
+    }
+
+    @Override
+    public void setTimestamp(int timestamp) {
+        throw new IllegalStateException("Not supported setTimestamp");
+    }
+
+    @Override
+    public String getAction() {
+        return null;
+    }
+
+    @Override
+    public void setAction(String action) {
+        throw new IllegalStateException("Not supported setAction");
+    }
+
+    @Override
+    public Set<String> getRoles() {
+        return getData().getRoles();
+    }
+
+    @Override
+    public void setRoles(Set<String> roles) {
+        throw new IllegalStateException("Not supported setRoles");
+    }
+
+    @Override
+    public Set<String> getProtocolMappers() {
+        return getData().getProtocolMappers();
+    }
+
+    @Override
+    public void setProtocolMappers(Set<String> protocolMappers) {
+        throw new IllegalStateException("Not supported setProtocolMappers");
+    }
+
+    @Override
+    public Map<String, ExecutionStatus> getExecutionStatus() {
+        return getData().getAuthenticatorStatus();
+    }
+
+    @Override
+    public void setExecutionStatus(String authenticator, ExecutionStatus status) {
+        throw new IllegalStateException("Not supported setExecutionStatus");
+    }
+
+    @Override
+    public void clearExecutionStatus() {
+        throw new IllegalStateException("Not supported clearExecutionStatus");
+    }
+
+    @Override
+    public UserModel getAuthenticatedUser() {
+        return userSession.getUser();
+    }
+
+    @Override
+    public void setAuthenticatedUser(UserModel user) {
+        throw new IllegalStateException("Not supported setAuthenticatedUser");
+    }
+
+    @Override
+    public String getAuthMethod() {
+        return getData().getAuthMethod();
+    }
+
+    @Override
+    public void setAuthMethod(String method) {
+        throw new IllegalStateException("Not supported setAuthMethod");
+    }
+
+    @Override
+    public String getNote(String name) {
+        return getData().getNotes()==null ? null : getData().getNotes().get(name);
+    }
+
+    @Override
+    public void setNote(String name, String value) {
+        throw new IllegalStateException("Not supported setNote");
+    }
+
+    @Override
+    public void removeNote(String name) {
+        throw new IllegalStateException("Not supported removeNote");
+    }
+
+    @Override
+    public Map<String, String> getNotes() {
+        return getData().getNotes();
+    }
+
+    @Override
+    public Set<String> getRequiredActions() {
+        throw new IllegalStateException("Not supported getRequiredActions");
+    }
+
+    @Override
+    public void addRequiredAction(String action) {
+        throw new IllegalStateException("Not supported addRequiredAction");
+    }
+
+    @Override
+    public void removeRequiredAction(String action) {
+        throw new IllegalStateException("Not supported removeRequiredAction");
+    }
+
+    @Override
+    public void addRequiredAction(UserModel.RequiredAction action) {
+        throw new IllegalStateException("Not supported addRequiredAction");
+    }
+
+    @Override
+    public void removeRequiredAction(UserModel.RequiredAction action) {
+        throw new IllegalStateException("Not supported removeRequiredAction");
+    }
+
+    @Override
+    public void setUserSessionNote(String name, String value) {
+        throw new IllegalStateException("Not supported setUserSessionNote");
+    }
+
+    @Override
+    public Map<String, String> getUserSessionNotes() {
+        throw new IllegalStateException("Not supported getUserSessionNotes");
+    }
+
+    @Override
+    public void clearUserSessionNotes() {
+        throw new IllegalStateException("Not supported clearUserSessionNotes");
+    }
+
+    protected static class OfflineClientSessionData {
+
+        @JsonProperty("authMethod")
+        private String authMethod;
+
+        @JsonProperty("redirectUri")
+        private String redirectUri;
+
+        @JsonProperty("protocolMappers")
+        private Set<String> protocolMappers;
+
+        @JsonProperty("roles")
+        private Set<String> roles;
+
+        @JsonProperty("notes")
+        private Map<String, String> notes;
+
+        @JsonProperty("authenticatorStatus")
+        private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
+
+        public String getAuthMethod() {
+            return authMethod;
+        }
+
+        public void setAuthMethod(String authMethod) {
+            this.authMethod = authMethod;
+        }
+
+        public String getRedirectUri() {
+            return redirectUri;
+        }
+
+        public void setRedirectUri(String redirectUri) {
+            this.redirectUri = redirectUri;
+        }
+
+        public Set<String> getProtocolMappers() {
+            return protocolMappers;
+        }
+
+        public void setProtocolMappers(Set<String> protocolMappers) {
+            this.protocolMappers = protocolMappers;
+        }
+
+        public Set<String> getRoles() {
+            return roles;
+        }
+
+        public void setRoles(Set<String> roles) {
+            this.roles = roles;
+        }
+
+        public Map<String, String> getNotes() {
+            return notes;
+        }
+
+        public void setNotes(Map<String, String> notes) {
+            this.notes = notes;
+        }
+
+        public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
+            return authenticatorStatus;
+        }
+
+        public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
+            this.authenticatorStatus = authenticatorStatus;
+        }
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java b/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java
new file mode 100644
index 0000000..bd01d38
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionAdapter.java
@@ -0,0 +1,215 @@
+package org.keycloak.services.offline;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionAdapter implements UserSessionModel {
+
+    private final OfflineUserSessionModel model;
+    private final UserModel user;
+
+    private OfflineUserSessionData data;
+
+    public OfflineUserSessionAdapter(OfflineUserSessionModel model, UserModel user) {
+        this.model = model;
+        this.user = user;
+    }
+
+    // lazily init representation
+    private OfflineUserSessionData getData() {
+        if (data == null) {
+            try {
+                data = JsonSerialization.readValue(model.getData(), OfflineUserSessionData.class);
+            } catch (IOException ioe) {
+                throw new ModelException(ioe);
+            }
+        }
+
+        return data;
+    }
+
+    @Override
+    public String getId() {
+        return model.getUserSessionId();
+    }
+
+    @Override
+    public String getBrokerSessionId() {
+        return getData().getBrokerSessionId();
+    }
+
+    @Override
+    public String getBrokerUserId() {
+        return getData().getBrokerUserId();
+    }
+
+    @Override
+    public UserModel getUser() {
+        return user;
+    }
+
+    @Override
+    public String getLoginUsername() {
+        return user.getUsername();
+    }
+
+    @Override
+    public String getIpAddress() {
+        return getData().getIpAddress();
+    }
+
+    @Override
+    public String getAuthMethod() {
+        return getData().getAuthMethod();
+    }
+
+    @Override
+    public boolean isRememberMe() {
+        return getData().isRememberMe();
+    }
+
+    @Override
+    public int getStarted() {
+        return getData().getStarted();
+    }
+
+    @Override
+    public int getLastSessionRefresh() {
+        return 0;
+    }
+
+    @Override
+    public void setLastSessionRefresh(int seconds) {
+        // Ignore
+    }
+
+    @Override
+    public List<ClientSessionModel> getClientSessions() {
+        throw new IllegalStateException("Not yet supported");
+    }
+
+    @Override
+    public String getNote(String name) {
+        return getData().getNotes()==null ? null : getData().getNotes().get(name);
+    }
+
+    @Override
+    public void setNote(String name, String value) {
+        throw new IllegalStateException("Illegal to set note offline session");
+
+    }
+
+    @Override
+    public void removeNote(String name) {
+        throw new IllegalStateException("Illegal to remove note from offline session");
+    }
+
+    @Override
+    public Map<String, String> getNotes() {
+        return getData().getNotes();
+    }
+
+    @Override
+    public State getState() {
+        return null;
+    }
+
+    @Override
+    public void setState(State state) {
+        throw new IllegalStateException("Illegal to set state on offline session");
+    }
+
+
+    protected static class OfflineUserSessionData {
+
+        @JsonProperty("brokerSessionId")
+        private String brokerSessionId;
+
+        @JsonProperty("brokerUserId")
+        private String brokerUserId;
+
+        @JsonProperty("ipAddress")
+        private String ipAddress;
+
+        @JsonProperty("authMethod")
+        private String authMethod;
+
+        @JsonProperty("rememberMe")
+        private boolean rememberMe;
+
+        @JsonProperty("started")
+        private int started;
+
+        @JsonProperty("notes")
+        private Map<String, String> notes;
+
+        public String getBrokerSessionId() {
+            return brokerSessionId;
+        }
+
+        public void setBrokerSessionId(String brokerSessionId) {
+            this.brokerSessionId = brokerSessionId;
+        }
+
+        public String getBrokerUserId() {
+            return brokerUserId;
+        }
+
+        public void setBrokerUserId(String brokerUserId) {
+            this.brokerUserId = brokerUserId;
+        }
+
+        public String getIpAddress() {
+            return ipAddress;
+        }
+
+        public void setIpAddress(String ipAddress) {
+            this.ipAddress = ipAddress;
+        }
+
+        public String getAuthMethod() {
+            return authMethod;
+        }
+
+        public void setAuthMethod(String authMethod) {
+            this.authMethod = authMethod;
+        }
+
+        public boolean isRememberMe() {
+            return rememberMe;
+        }
+
+        public void setRememberMe(boolean rememberMe) {
+            this.rememberMe = rememberMe;
+        }
+
+        public int getStarted() {
+            return started;
+        }
+
+        public void setStarted(int started) {
+            this.started = started;
+        }
+
+        public Map<String, String> getNotes() {
+            return notes;
+        }
+
+        public void setNotes(Map<String, String> notes) {
+            this.notes = notes;
+        }
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java b/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java
new file mode 100644
index 0000000..5e1a0dc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java
@@ -0,0 +1,182 @@
+package org.keycloak.services.offline;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * TODO: Change to utils?
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionManager {
+
+    protected static Logger logger = Logger.getLogger(OfflineUserSessionManager.class);
+
+    public void persistOfflineSession(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();
+        for (OfflineClientSessionModel existing : clientSessions) {
+            if (existing.getClientId().equals(client.getId())) {
+                if (logger.isTraceEnabled()) {
+                    logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' . Offline token will be replaced with new one",
+                            user.getUsername(), client.getClientId(), existing.getClientSessionId());
+                }
+
+                user.removeOfflineClientSession(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());
+                }
+            }
+        }
+
+        // Verify if we already have UserSession with this ID. If yes, don't create another one
+        OfflineUserSessionModel userSessionRep = user.getOfflineUserSession(userSession.getId());
+        if (userSessionRep == null) {
+            createOfflineUserSession(user, userSession);
+        }
+
+        // Create clientRep and save to DB.
+        createOfflineClientSession(user, clientSession, userSession);
+    }
+
+    // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
+    public ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
+        OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId);
+        if (clientSession == null) {
+            return null;
+        }
+
+        if (!userSessionId.equals(clientSession.getUserSessionId())) {
+            throw new ModelException("User session don't match. Offline client session " + clientSession.getClientSessionId() + ", It's user session " + clientSession.getUserSessionId() +
+                    "  Wanted user session: " + userSessionId);
+        }
+
+        OfflineUserSessionModel userSession = user.getOfflineUserSession(userSessionId);
+        if (userSession == null) {
+            throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId);
+        }
+
+        OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user);
+
+        ClientModel client = realm.getClientById(clientSession.getClientId());
+        OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
+
+        return clientSessionAdapter;
+    }
+
+    public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
+        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+        Set<ClientModel> clients = new HashSet<>();
+        for (OfflineClientSessionModel clientSession : clientSessions) {
+            ClientModel client = realm.getClientById(clientSession.getClientId());
+            clients.add(client);
+        }
+        return clients;
+    }
+
+    public boolean revokeOfflineToken(UserModel user, ClientModel client) {
+        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+        boolean anyRemoved = false;
+        for (OfflineClientSessionModel clientSession : clientSessions) {
+            if (clientSession.getClientId().equals(client.getId())) {
+                if (logger.isTraceEnabled()) {
+                    logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
+                            user.getUsername(), client.getClientId(), clientSession.getClientSessionId());
+                }
+
+                user.removeOfflineClientSession(clientSession.getClientSessionId());
+                checkUserSessionHasClientSessions(user, clientSession.getUserSessionId());
+                anyRemoved = true;
+            }
+        }
+
+        return anyRemoved;
+    }
+
+    private void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
+        if (logger.isTraceEnabled()) {
+            logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
+        }
+        OfflineUserSessionAdapter.OfflineUserSessionData rep = new OfflineUserSessionAdapter.OfflineUserSessionData();
+        rep.setBrokerUserId(userSession.getBrokerUserId());
+        rep.setBrokerSessionId(userSession.getBrokerSessionId());
+        rep.setIpAddress(userSession.getIpAddress());
+        rep.setAuthMethod(userSession.getAuthMethod());
+        rep.setRememberMe(userSession.isRememberMe());
+        rep.setStarted(userSession.getStarted());
+        rep.setNotes(userSession.getNotes());
+
+        try {
+            String stringRep = JsonSerialization.writeValueAsString(rep);
+            OfflineUserSessionModel sessionModel = new OfflineUserSessionModel();
+            sessionModel.setUserSessionId(userSession.getId());
+            sessionModel.setData(stringRep);
+            user.addOfflineUserSession(sessionModel);
+        } catch (IOException ioe) {
+            throw new ModelException(ioe);
+        }
+    }
+
+    private void createOfflineClientSession(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());
+        }
+        OfflineClientSessionAdapter.OfflineClientSessionData rep = new OfflineClientSessionAdapter.OfflineClientSessionData();
+        rep.setAuthMethod(clientSession.getAuthMethod());
+        rep.setRedirectUri(clientSession.getRedirectUri());
+        rep.setProtocolMappers(clientSession.getProtocolMappers());
+        rep.setRoles(clientSession.getRoles());
+        rep.setNotes(clientSession.getNotes());
+        rep.setAuthenticatorStatus(clientSession.getExecutionStatus());
+
+        try {
+            String stringRep = JsonSerialization.writeValueAsString(rep);
+            OfflineClientSessionModel clsModel = new OfflineClientSessionModel();
+            clsModel.setClientSessionId(clientSession.getId());
+            clsModel.setClientId(clientSession.getClient().getId());
+            clsModel.setUserSessionId(userSession.getId());
+            clsModel.setData(stringRep);
+            user.addOfflineClientSession(clsModel);
+        } catch (IOException ioe) {
+            throw new ModelException(ioe);
+        }
+    }
+
+    // Check if userSession has any offline clientSessions attached to it. Remove userSession if not
+    private void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
+        Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+
+        for (OfflineClientSessionModel clientSession : clientSessions) {
+            if (clientSession.getUserSessionId().equals(userSessionId)) {
+                return;
+            }
+        }
+
+        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);
+    }
+}
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 1756f12..14d3639 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -58,6 +58,7 @@ import org.keycloak.services.managers.Auth;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
+import org.keycloak.services.offline.OfflineUserSessionManager;
 import org.keycloak.services.util.ResolveRelative;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.util.UriUtils;
@@ -486,6 +487,7 @@ public class AccountService extends AbstractSecuredLocalService {
         // Revoke grant in UserModel
         UserModel user = auth.getUser();
         user.revokeConsentForClient(client.getId());
+        new OfflineUserSessionManager().revokeOfflineToken(user, client);
 
         // Logout clientSessions for this user and client
         AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 95d644e..1f401fa 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -646,7 +646,7 @@ public class AccountTest {
         }
     }
 
-    // More tests (including revoke) are in OAuthGrantTest
+    // More tests (including revoke) are in OAuthGrantTest and OfflineTokenTest
     @Test
     public void applications() {
         applicationsPage.open();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 88c1ccc..77b6d19 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -30,6 +30,7 @@ import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.util.RefreshTokenUtil;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -156,6 +157,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
                 .detail(Details.CODE_ID, codeId)
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, isUUID())
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH)
                 .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .session(sessionId);
     }
@@ -164,6 +166,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
         return expect(EventType.REFRESH_TOKEN)
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH)
                 .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
                 .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
                 .session(sessionId);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
index 5bc3556..597e7dd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
@@ -1,6 +1,7 @@
 package org.keycloak.testsuite.model;
 
 import java.util.List;
+import java.util.Set;
 
 import org.junit.Assert;
 import org.junit.ClassRule;
@@ -89,4 +90,51 @@ public class CacheTest {
         }
     }
 
+    // KEYCLOAK-1842
+    @Test
+    public void testRoleMappingsInvalidatedWhenClientRemoved() {
+        KeycloakSession session = kc.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().addUser(realm, "joel");
+            ClientModel client = realm.addClient("foo");
+            RoleModel fooRole = client.addRole("foo-role");
+            user.grantRole(fooRole);
+        } finally {
+            session.getTransaction().commit();
+            session.close();
+        }
+
+        // Remove client
+        session = kc.startSession();
+        int grantedRolesCount;
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("joel", realm);
+            grantedRolesCount = user.getRoleMappings().size();
+
+            ClientModel client = realm.getClientByClientId("foo");
+            realm.removeClient(client.getId());
+        } finally {
+            session.getTransaction().commit();
+            session.close();
+        }
+
+        // Assert role mappings was removed from user as well
+        session = kc.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("joel", realm);
+            Set<RoleModel> roles = user.getRoleMappings();
+            for (RoleModel role : roles) {
+                Assert.assertNotNull(role.getContainer());
+            }
+
+            Assert.assertEquals(roles.size(), grantedRolesCount - 1);
+        } finally {
+            session.getTransaction().commit();
+            session.close();
+        }
+    }
+
 }
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 2f33e43..d56056d 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
@@ -326,6 +326,8 @@ public class ImportTest extends AbstractModelTest {
         // Test service accounts
         Assert.assertFalse(application.isServiceAccountsEnabled());
         Assert.assertTrue(otherApp.isServiceAccountsEnabled());
+        Assert.assertFalse(application.isOfflineTokensEnabled());
+        Assert.assertTrue(otherApp.isOfflineTokensEnabled());
         Assert.assertNull(session.users().getUserByServiceAccountClient(application));
         UserModel linked = session.users().getUserByServiceAccountClient(otherApp);
         Assert.assertNotNull(linked);
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 258dd3c..f0118c2 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
@@ -4,6 +4,8 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
@@ -283,6 +285,59 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertNull(session.users().getUserByUsername("user1", realm));
     }
 
+    @Test
+    public void testOfflineSessionsRemoved() {
+        RealmModel realm = realmManager.createRealm("original");
+        ClientModel fooClient = realm.addClient("foo");
+        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");
+
+        commit();
+
+        realm = realmManager.getRealmByName("original");
+        realm.removeClient(barClient.getId());
+
+        commit();
+
+        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"));
+
+        realm.removeClient(fooClient.getId());
+
+        commit();
+
+        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());
+    }
+
+    private void addOfflineUserSession(UserModel user, String userSessionId, String data) {
+        OfflineUserSessionModel model = new OfflineUserSessionModel();
+        model.setUserSessionId(userSessionId);
+        model.setData(data);
+        user.addOfflineUserSession(model);
+    }
+
+    private void addOfflineClientSession(UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
+        OfflineClientSessionModel model = new OfflineClientSessionModel();
+        model.setClientSessionId(clientSessionId);
+        model.setUserSessionId(userSessionId);
+        model.setClientId(clientId);
+        model.setData(data);
+        user.addOfflineClientSession(model);
+    }
+
     public static void assertEquals(UserModel expected, UserModel actual) {
         Assert.assertEquals(expected.getUsername(), actual.getUsername());
         Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index cbfc76f..24f4911 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -180,7 +180,11 @@ public class AccessTokenTest {
         Assert.assertEquals("invalid_grant", response.getError());
         Assert.assertEquals("Incorrect redirect_uri", response.getErrorDescription());
 
-        events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
+        events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_code")
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_TYPE)
+                .assertEvent();
     }
 
     @Test
@@ -201,7 +205,13 @@ public class AccessTokenTest {
         assertNull(tokenResponse.getAccessToken());
         assertNull(tokenResponse.getRefreshToken());
 
-        events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).user((String) null).session((String) null).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent();
+        events.expectCodeToToken(codeId, sessionId)
+                .removeDetail(Details.TOKEN_ID)
+                .user((String) null)
+                .session((String) null)
+                .removeDetail(Details.REFRESH_TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_TYPE)
+                .error(Errors.INVALID_CODE).assertEvent();
 
         events.clear();
     }
@@ -230,7 +240,11 @@ public class AccessTokenTest {
         Assert.assertEquals(400, response.getStatusCode());
 
         AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
-        expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).user((String) null);
+        expectedEvent.error("invalid_code")
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_TYPE)
+                .user((String) null);
         expectedEvent.assertEvent();
 
         events.clear();
@@ -264,7 +278,11 @@ public class AccessTokenTest {
         Assert.assertEquals(400, response.getStatusCode());
 
         AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
-        expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).user((String) null);
+        expectedEvent.error("invalid_code")
+                .removeDetail(Details.TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_ID)
+                .removeDetail(Details.REFRESH_TOKEN_TYPE)
+                .user((String) null);
         expectedEvent.assertEvent();
 
         events.clear();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
new file mode 100644
index 0000000..33e6ff4
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -0,0 +1,441 @@
+package org.keycloak.testsuite.oauth;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.UriBuilder;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.constants.ServiceAccountConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.Event;
+import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.RefreshTokenUtil;
+import org.keycloak.util.Time;
+import org.keycloak.util.UriUtils;
+import org.openqa.selenium.WebDriver;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineTokenTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            // For testing
+            appRealm.setAccessTokenLifespan(10);
+            appRealm.setSsoSessionIdleTimeout(30);
+
+            ClientModel app = new ClientManager(manager).createClient(appRealm, "offline-client");
+            app.setSecret("secret1");
+            String testAppRedirectUri = appRealm.getClientByClientId("test-app").getRedirectUris().iterator().next();
+            offlineClientAppUri = UriUtils.getOrigin(testAppRedirectUri) + "/offline-client";
+            app.setRedirectUris(new HashSet<>(Arrays.asList(offlineClientAppUri)));
+            app.setManagementUrl(offlineClientAppUri);
+
+            new ClientManager(manager).enableServiceAccount(app);
+            UserModel serviceAccountUser = manager.getSession().users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client", appRealm);
+            RoleModel customerUserRole = appRealm.getClientByClientId("test-app").getRole("customer-user");
+            serviceAccountUser.grantRole(customerUserRole);
+
+            app.setOfflineTokensEnabled(true);
+            userId = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).getId();
+
+            URL url = getClass().getResource("/oidc/offline-client-keycloak.json");
+            keycloakRule.createApplicationDeployment()
+                    .name("offline-client").contextPath("/offline-client")
+                    .servletClass(OfflineTokenServlet.class).adapterConfigPath(url.getPath())
+                    .role("user").deployApplication();
+        }
+
+    });
+
+    private static String userId;
+    private static String offlineClientAppUri;
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @WebResource
+    protected AccountApplicationsPage accountAppPage;
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+
+//    @Test
+//    public void testSleep() throws Exception {
+//        Thread.sleep(9999000);
+//    }
+
+    @Test
+    public void offlineTokenDisabledForClient() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.doLogin("test-user@localhost", "password");
+
+        Event loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertEquals("invalid_client", tokenResponse.getError());
+
+        events.expectCodeToToken(codeId, sessionId)
+                .error("invalid_client")
+                .clearDetails()
+                .assertEvent();
+    }
+
+    @Test
+    public void offlineTokenBrowserFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
+        oauth.doLogin("test-user@localhost", "password");
+
+        Event loginEvent = events.expectLogin()
+                .client("offline-client")
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .assertEvent();
+
+        final String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectCodeToToken(codeId, sessionId)
+                .client("offline-client")
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+
+        Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
+    }
+
+    private void testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
+                                             final String sessionId, String userId) {
+        // Change offset to big value to ensure userSession expired
+        Time.setOffset(99999);
+        Assert.assertFalse(oldToken.isActive());
+        Assert.assertTrue(offlineToken.isActive());
+
+        // Assert userSession expired
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                manager.getSession().sessions().removeExpiredUserSessions(appRealm);
+            }
+
+        });
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                Assert.assertNull(manager.getSession().sessions().getUserSession(appRealm, sessionId));
+            }
+
+        });
+
+
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
+        AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
+        Assert.assertEquals(200, response.getStatusCode());
+        Assert.assertEquals(sessionId, refreshedToken.getSessionState());
+
+        // Assert no refreshToken in the response
+        Assert.assertNull(response.getRefreshToken());
+        Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
+
+        Assert.assertEquals(userId, refreshedToken.getSubject());
+
+        Assert.assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
+
+        Assert.assertEquals(1, refreshedToken.getResourceAccess("test-app").getRoles().size());
+        Assert.assertTrue(refreshedToken.getResourceAccess("test-app").isUserInRole("customer-user"));
+
+        Event refreshEvent = events.expectRefresh(offlineToken.getId(), sessionId)
+                .client("offline-client")
+                .user(userId)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+        Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
+
+        Time.setOffset(0);
+    }
+
+    @Test
+    public void offlineTokenDirectGrantFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
+
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectLogin()
+                .client("offline-client")
+                .user(userId)
+                .session(token.getSessionState())
+                .detail(Details.RESPONSE_TYPE, "token")
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+        Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+    }
+
+    @Test
+    public void offlineTokenServiceAccountFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        String serviceAccountUserId = keycloakRule.getUser("test",  ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client").getId();
+
+        events.expectClientLogin()
+                .client("offline-client")
+                .user(serviceAccountUserId)
+                .session(token.getSessionState())
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
+                .assertEvent();
+
+        Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
+
+
+        // Now retrieve another offline token and verify that previous offline token is not valid anymore
+        tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
+
+        AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString2 = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
+
+        events.expectClientLogin()
+                .client("offline-client")
+                .user(serviceAccountUserId)
+                .session(token2.getSessionState())
+                .detail(Details.TOKEN_ID, token2.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
+                .assertEvent();
+
+        // Refresh with old offline token should fail
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
+        Assert.assertEquals(400, response.getStatusCode());
+        Assert.assertEquals("invalid_grant", response.getError());
+
+        events.expectRefresh(offlineToken.getId(), offlineToken.getSessionState())
+                .error(Errors.INVALID_TOKEN)
+                .client("offline-client")
+                .user(serviceAccountUserId)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .removeDetail(Details.TOKEN_ID)
+                .detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+
+        // Refresh with new offline token is ok
+        testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
+    }
+
+    @Test
+    public void testServlet() {
+        OfflineTokenServlet.tokenInfo = null;
+
+        String servletUri = UriBuilder.fromUri(offlineClientAppUri)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        driver.navigate().to(servletUri);
+        loginPage.login("test-user@localhost", "password");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+
+        Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+        Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getExpiration(), 0);
+
+        String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
+        String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.getId();
+
+        // Assert access token will be refreshed, but offline token will be still the same
+        Time.setOffset(9999);
+        driver.navigate().to(offlineClientAppUri);
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+        Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId);
+        Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId);
+
+        // Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
+        driver.navigate().to(offlineClientAppUri + "/logout");
+        loginPage.assertCurrent();
+        driver.navigate().to(offlineClientAppUri);
+        loginPage.assertCurrent();
+
+        Time.setOffset(0);
+        events.clear();
+    }
+
+    @Test
+    public void testServletWithRevoke() {
+        // Login to servlet first with offline token
+        String servletUri = UriBuilder.fromUri(offlineClientAppUri)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        driver.navigate().to(servletUri);
+        loginPage.login("test-user@localhost", "password");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+
+        Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+
+        // Assert refresh works with increased time
+        Time.setOffset(9999);
+        driver.navigate().to(offlineClientAppUri);
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+        Time.setOffset(0);
+
+        events.clear();
+
+        // Go to account service and revoke grant
+        accountAppPage.open();
+        List<String> additionalGrants = accountAppPage.getApplications().get("offline-client").getAdditionalGrants();
+        Assert.assertEquals(additionalGrants.size(), 1);
+        Assert.assertEquals(additionalGrants.get(0), "Offline Access");
+        accountAppPage.revokeGrant("offline-client");
+        Assert.assertEquals(accountAppPage.getApplications().get("offline-client").getAdditionalGrants().size(), 0);
+
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "offline-client").assertEvent();
+
+        // Assert refresh doesn't work now (increase time one more time)
+        Time.setOffset(9999);
+        driver.navigate().to(offlineClientAppUri);
+        Assert.assertFalse(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+        loginPage.assertCurrent();
+        Time.setOffset(0);
+    }
+
+    public static class OfflineTokenServlet extends HttpServlet {
+
+        private static TokenInfo tokenInfo;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+            if (req.getRequestURI().endsWith("logout")) {
+
+                UriBuilder redirectUriBuilder = UriBuilder.fromUri(offlineClientAppUri);
+                if (req.getParameter(OAuth2Constants.SCOPE) != null) {
+                    redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, req.getParameter(OAuth2Constants.SCOPE));
+                }
+                String redirectUri = redirectUriBuilder.build().toString();
+
+                String origin = UriUtils.getOrigin(req.getRequestURL().toString());
+                String serverLogoutRedirect = UriBuilder.fromUri(origin + "/auth/realms/test/protocol/openid-connect/logout")
+                        .queryParam("redirect_uri", redirectUri)
+                        .build().toString();
+
+                resp.sendRedirect(serverLogoutRedirect);
+                return;
+            }
+
+            StringBuilder response = new StringBuilder("<html><head><title>Offline token servlet</title></head><body><pre>");
+            RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+            String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
+            RefreshToken refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
+            String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
+
+            response = response.append(accessTokenPretty)
+                    .append(refreshTokenPretty)
+                    .append("</pre></body></html>");
+            resp.getWriter().println(response.toString());
+
+            tokenInfo = new TokenInfo(ctx.getToken(), refreshToken);
+        }
+
+    }
+
+    private static class TokenInfo {
+
+        private final AccessToken accessToken;
+        private final RefreshToken refreshToken;
+
+        public TokenInfo(AccessToken accessToken, RefreshToken refreshToken) {
+            this.accessToken = accessToken;
+            this.refreshToken = refreshToken;
+        }
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index b06e433..087f82f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -76,6 +76,8 @@ public class OAuthClient {
 
     private String state = "mystate";
 
+    private String scope;
+
     private String uiLocales = null;
 
     private PublicKey realmPublicKey;
@@ -192,6 +194,9 @@ public class OAuthClient {
             if (clientSessionHost != null) {
                 parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
             }
+            if (scope != null) {
+                parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
+            }
 
             UrlEncodedFormEntity formEntity;
             try {
@@ -218,6 +223,10 @@ public class OAuthClient {
             List<NameValuePair> parameters = new LinkedList<NameValuePair>();
             parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
 
+            if (scope != null) {
+                parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
+            }
+
             UrlEncodedFormEntity formEntity;
             try {
                 formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
@@ -390,6 +399,9 @@ public class OAuthClient {
         if(uiLocales != null){
             b.queryParam(LocaleHelper.UI_LOCALES_PARAM, uiLocales);
         }
+        if (scope != null) {
+            b.queryParam(OAuth2Constants.SCOPE, scope);
+        }
         return b.build(realm).toString();
     }
 
@@ -452,6 +464,11 @@ public class OAuthClient {
         return this;
     }
 
+    public OAuthClient scope(String scope) {
+        this.scope = scope;
+        return this;
+    }
+
     public OAuthClient uiLocales(String uiLocales){
         this.uiLocales = uiLocales;
         return this;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
index f88db27..b48559c 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
@@ -77,6 +77,15 @@ public class AccountApplicationsPage extends AbstractAccountPage {
                             currentEntry.addMapper(protMapper);
                         }
                         break;
+                    case 5:
+                        String additionalGrant = col.getText();
+                        if (additionalGrant.isEmpty()) break;
+                        String[] grants = additionalGrant.split(",");
+                        for (String grant : grants) {
+                            grant = grant.trim();
+                            currentEntry.addAdditionalGrant(grant);
+                        }
+                        break;
                 }
             }
         }
@@ -89,6 +98,7 @@ public class AccountApplicationsPage extends AbstractAccountPage {
         private final List<String> rolesAvailable = new ArrayList<String>();
         private final List<String> rolesGranted = new ArrayList<String>();
         private final List<String> protocolMappersGranted = new ArrayList<String>();
+        private final List<String> additionalGrants = new ArrayList<>();
 
         private void addAvailableRole(String role) {
             rolesAvailable.add(role);
@@ -102,6 +112,10 @@ public class AccountApplicationsPage extends AbstractAccountPage {
             protocolMappersGranted.add(protocolMapper);
         }
 
+        private void addAdditionalGrant(String grant) {
+            additionalGrants.add(grant);
+        }
+
         public List<String> getRolesGranted() {
             return rolesGranted;
         }
@@ -113,5 +127,9 @@ public class AccountApplicationsPage extends AbstractAccountPage {
         public List<String> getProtocolMappersGranted() {
             return protocolMappersGranted;
         }
+
+        public List<String> getAdditionalGrants() {
+            return additionalGrants;
+        }
     }
 }
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index acbbdf5..55a75b5 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -164,6 +164,7 @@
             "name": "Other Application",
             "enabled": true,
             "serviceAccountsEnabled": true,
+            "offlineTokensEnabled": true,
             "clientAuthenticatorType": "client-jwt",
             "protocolMappers" : [
                 {
diff --git a/testsuite/integration/src/test/resources/oidc/offline-client-keycloak.json b/testsuite/integration/src/test/resources/oidc/offline-client-keycloak.json
new file mode 100644
index 0000000..bc7be17
--- /dev/null
+++ b/testsuite/integration/src/test/resources/oidc/offline-client-keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm": "test",
+  "resource": "offline-client",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url": "http://localhost:8081/auth",
+  "ssl-required" : "external",
+  "credentials": {
+    "secret": "secret1"
+  }
+}
\ No newline at end of file