keycloak-aplcache

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

9/23/2015 10:30:36 AM

Changes

Details

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
index 061eaa3..24902d5 100644
--- 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
@@ -2,8 +2,8 @@
 <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">
+        <addColumn tableName="KEYCLOAK_ROLE">
+            <column name="SCOPE_PARAM_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
                 <constraints nullable="false"/>
             </column>
         </addColumn>
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 42618a1..020445e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -24,7 +24,6 @@ public class ClientRepresentation {
     protected Boolean bearerOnly;
     protected Boolean consentRequired;
     protected Boolean serviceAccountsEnabled;
-    protected Boolean offlineTokensEnabled;
     protected Boolean directGrantsOnly;
     protected Boolean publicClient;
     protected Boolean frontchannelLogout;
@@ -163,14 +162,6 @@ 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/idm/RoleRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
index c335ab4..4100785 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
@@ -12,6 +12,7 @@ public class RoleRepresentation {
     protected String id;
     protected String name;
     protected String description;
+    protected Boolean scopeParamRequired;
     protected boolean composite;
     protected Composites composites;
 
@@ -46,9 +47,10 @@ public class RoleRepresentation {
     public RoleRepresentation() {
     }
 
-    public RoleRepresentation(String name, String description) {
+    public RoleRepresentation(String name, String description, boolean scopeParamRequired) {
         this.name = name;
         this.description = description;
+        this.scopeParamRequired = scopeParamRequired;
     }
 
     public String getId() {
@@ -75,6 +77,14 @@ public class RoleRepresentation {
         this.description = description;
     }
 
+    public Boolean isScopeParamRequired() {
+        return scopeParamRequired;
+    }
+
+    public void setScopeParamRequired(Boolean scopeParamRequired) {
+        this.scopeParamRequired = scopeParamRequired;
+    }
+
     public Composites getComposites() {
         return composites;
     }
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 63e04db..371b5d0 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
@@ -5,7 +5,6 @@ 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;
@@ -13,7 +12,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.services.offline.OfflineTokenUtils;
 import org.keycloak.util.MultivaluedHashMap;
 
 /**
@@ -25,7 +24,7 @@ public class ApplicationsBean {
 
     public ApplicationsBean(RealmModel realm, UserModel user) {
 
-        Set<ClientModel> offlineClients = new OfflineUserSessionManager().findClientsWithOfflineToken(realm, user);
+        Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(realm, user);
 
         List<ClientModel> realmClients = realm.getClients();
         for (ClientModel client : realmClients) {
@@ -34,7 +33,7 @@ public class ApplicationsBean {
                 continue;
             }
 
-            Set<RoleModel> availableRoles = TokenManager.getAccess(null, client, user);
+            Set<RoleModel> availableRoles = TokenManager.getAccess(null, false, client, user);
             // Don't show applications, which user doesn't have access into (any available roles)
             if (availableRoles.isEmpty()) {
                 continue;
@@ -60,7 +59,7 @@ public class ApplicationsBean {
 
             List<String> additionalGrants = new ArrayList<>();
             if (offlineClients.contains(client)) {
-                additionalGrants.add("${offlineAccess}");
+                additionalGrants.add("${offlineToken}");
             }
 
             ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client,
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 b0ea19c..23a112d 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
@@ -52,6 +52,7 @@ role_manage-events=Manage events
 role_view-profile=View profile
 role_manage-account=Manage account
 role_read-token=Read token
+role_offline-access=Offline access
 client_account=Account
 client_security-admin-console=Security Admin Console
 client_realm-management=Realm Management
@@ -89,7 +90,7 @@ additionalGrants=Additional Grants
 action=Action
 inResource=in
 fullAccess=Full Access
-offlineAccess=Offline Access
+offlineToken=Offline Token
 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 303e439..06c939b 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,13 +79,6 @@
                     <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/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
index c0ce766..f93c96e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html
@@ -32,6 +32,13 @@
                                                            required> -->
                 </div>
             </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="scopeParamRequired">Scope Param Required </label>
+                <kc-tooltip>This role will be granted just if scope parameter with role name is used during authorization/token request.</kc-tooltip>
+                <div class="col-md-6">
+                    <input ng-model="role.scopeParamRequired" name="scopeParamRequired" id="scopeParamRequired" onoffswitch />
+                </div>
+            </div>
             <div class="form-group clearfix block" data-ng-hide="create">
                 <label class="col-md-2 control-label" for="compositeSwitch" class="control-label">Composite Roles</label>
                 <div class="col-md-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
index 49d09c8..6bf854e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-detail.html
@@ -28,6 +28,13 @@
                     <textarea class="form-control" rows="5" cols="50" id="description" name="description" data-ng-model="role.description"></textarea>
                 </div>
             </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="scopeParamRequired">Scope Param Required </label>
+                <kc-tooltip>This role will be granted just if scope parameter with role name is used during authorization/token request.</kc-tooltip>
+                <div class="col-md-6">
+                    <input ng-model="role.scopeParamRequired" name="scopeParamRequired" id="scopeParamRequired" onoffswitch />
+                </div>
+            </div>
             <div class="form-group" data-ng-hide="create">
                 <label class="col-md-2 control-label" for="compositeSwitch" class="control-label">Composite Roles</label>
                 <div class="col-md-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 3dfb8ea..94b0627 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -104,6 +104,7 @@ role_manage-events=Manage events
 role_view-profile=View profile
 role_manage-account=Manage account
 role_read-token=Read token
+role_offline-access=Offline access
 client_account=Account
 client_security-admin-console=Security Admin Console
 client_realm-management=Realm Management
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java
index 792822e..beb2f98 100755
--- a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java
@@ -11,7 +11,7 @@ public interface MigrationModel {
     /**
      * Must have the form of major.minor.micro as the version is parsed and numbers are compared
      */
-    public static final String LATEST_VERSION = "1.5.0";
+    public static final String LATEST_VERSION = "1.6.0";
 
     String getStoredVersion();
     void setStoredVersion(String version);
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
index 61b6244..e5c9484 100755
--- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
 import org.keycloak.migration.migrators.MigrateTo1_3_0;
 import org.keycloak.migration.migrators.MigrateTo1_4_0;
 import org.keycloak.migration.migrators.MigrateTo1_5_0;
+import org.keycloak.migration.migrators.MigrateTo1_6_0;
 import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
 import org.keycloak.models.KeycloakSession;
 
@@ -47,6 +48,12 @@ public class MigrationModelManager {
             }
             new MigrateTo1_5_0().migrate(session);
         }
+        if (stored == null || stored.lessThan(MigrateTo1_6_0.VERSION)) {
+            if (stored != null) {
+                logger.debug("Migrating older model to 1.6.0 updates");
+            }
+            new MigrateTo1_6_0().migrate(session);
+        }
 
         model.setStoredVersion(MigrationModel.LATEST_VERSION);
     }
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
new file mode 100644
index 0000000..8010586
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
@@ -0,0 +1,25 @@
+package org.keycloak.migration.migrators;
+
+import java.util.List;
+
+import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MigrateTo1_6_0 {
+
+    public static final ModelVersion VERSION = new ModelVersion("1.6.0");
+
+    public void migrate(KeycloakSession session) {
+        List<RealmModel> realms = session.realms().getRealms();
+        for (RealmModel realm : realms) {
+            KeycloakModelUtils.setupOfflineTokens(realm);
+        }
+
+    }
+
+}
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_CR1.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_CR1.java
index c9e10ad..1aa4929 100755
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_CR1.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_CR1.java
@@ -5,6 +5,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import java.util.List;
@@ -26,7 +27,9 @@ public class MigrationTo1_2_0_CR1 {
             client.setFullScopeAllowed(false);
 
             for (String role : Constants.BROKER_SERVICE_ROLES) {
-                client.addRole(role).setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
+                RoleModel roleModel = client.addRole(role);
+                roleModel.setDescription("${role_" + role.toLowerCase().replaceAll("_", "-") + "}");
+                roleModel.setScopeParamRequired(false);
             }
         }
     }
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 6461a33..daccf8e 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -109,9 +109,6 @@ 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/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index b1adbfa..5fe3189 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -1,5 +1,7 @@
 package org.keycloak.models;
 
+import org.keycloak.OAuth2Constants;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -16,4 +18,5 @@ public interface Constants {
     String INSTALLED_APP_URL = "http://localhost";
     String READ_TOKEN_ROLE = "read-token";
     String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
+    String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
 }
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 83fa12d..52e9721 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,7 +28,6 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     private boolean bearerOnly;
     private boolean consentRequired;
     private boolean serviceAccountsEnabled;
-    private boolean offlineTokensEnabled;
     private boolean directGrantsOnly;
     private int nodeReRegistrationTimeout;
 
@@ -229,14 +228,6 @@ 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/RoleEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RoleEntity.java
index a610d39..4b4551e 100644
--- a/model/api/src/main/java/org/keycloak/models/entities/RoleEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RoleEntity.java
@@ -9,6 +9,7 @@ public class RoleEntity extends AbstractIdentifiableEntity {
 
     private String name;
     private String description;
+    private boolean scopeParamRequired;
 
     private List<String> compositeRoleIds;
 
@@ -31,6 +32,14 @@ public class RoleEntity extends AbstractIdentifiableEntity {
         this.description = description;
     }
 
+    public boolean isScopeParamRequired() {
+        return scopeParamRequired;
+    }
+
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        this.scopeParamRequired = scopeParamRequired;
+    }
+
     public List<String> getCompositeRoleIds() {
         return compositeRoleIds;
     }
diff --git a/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java b/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
index 274e7a6..54f212a 100755
--- a/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
+++ b/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
@@ -26,6 +26,7 @@ public class ImpersonationConstants {
         if (realmAdminApp.getRole(IMPERSONATION_ROLE) != null) return;
         RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ROLE);
         impersonationRole.setDescription("${role_" + IMPERSONATION_ROLE + "}");
+        impersonationRole.setScopeParamRequired(false);
         adminRole.addCompositeRole(impersonationRole);
     }
 
@@ -36,6 +37,7 @@ public class ImpersonationConstants {
         if (realmAdminApp.getRole(IMPERSONATION_ROLE) != null) return;
         RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ROLE);
         impersonationRole.setDescription("${role_" + IMPERSONATION_ROLE + "}");
+        impersonationRole.setScopeParamRequired(false);
         RoleModel adminRole = realmAdminApp.getRole(AdminRoles.REALM_ADMIN);
         adminRole.addCompositeRole(impersonationRole);
     }
diff --git a/model/api/src/main/java/org/keycloak/models/RoleModel.java b/model/api/src/main/java/org/keycloak/models/RoleModel.java
index c296795..9904a16 100755
--- a/model/api/src/main/java/org/keycloak/models/RoleModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RoleModel.java
@@ -17,6 +17,10 @@ public interface RoleModel {
 
     void setName(String name);
 
+    boolean isScopeParamRequired();
+
+    void setScopeParamRequired(boolean scopeParamRequired);
+
     boolean isComposite();
 
     void addCompositeRole(RoleModel role);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index b64ebb0..846269e 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -4,6 +4,7 @@ import org.bouncycastle.openssl.PEMWriter;
 import org.keycloak.constants.KerberosConstants;
 import org.keycloak.constants.ServiceAccountConstants;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.KeycloakSessionTask;
@@ -360,4 +361,13 @@ public final class KeycloakModelUtils {
     public static String toLowerCaseSafe(String str) {
         return str==null ? null : str.toLowerCase();
     }
+
+    public static void setupOfflineTokens(RealmModel realm) {
+        if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) {
+            RoleModel role = realm.addRole(Constants.OFFLINE_ACCESS_ROLE);
+            role.setDescription("${role_offline-access}");
+            role.setScopeParamRequired(true);
+            realm.addDefaultRole(Constants.OFFLINE_ACCESS_ROLE);
+        }
+    }
 }
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 a382880..b71fce9 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
@@ -89,6 +89,7 @@ public class ModelToRepresentation {
         rep.setId(role.getId());
         rep.setName(role.getName());
         rep.setDescription(role.getDescription());
+        rep.setScopeParamRequired(role.isScopeParamRequired());
         rep.setComposite(role.isComposite());
         return rep;
     }
@@ -303,7 +304,6 @@ 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 3907f96..77ae66e 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
@@ -181,6 +181,8 @@ public class RepresentationToModel {
                         // Application role may already exists (for example if it is defaultRole)
                         RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
                         role.setDescription(roleRep.getDescription());
+                        boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
+                        role.setScopeParamRequired(scopeParamRequired);
                     }
                 }
             }
@@ -633,6 +635,8 @@ public class RepresentationToModel {
     public static void createRole(RealmModel newRealm, RoleRepresentation roleRep) {
         RoleModel role = roleRep.getId()!=null ? newRealm.addRole(roleRep.getId(), roleRep.getName()) : newRealm.addRole(roleRep.getName());
         if (roleRep.getDescription() != null) role.setDescription(roleRep.getDescription());
+        boolean scopeParamRequired = roleRep.isScopeParamRequired() == null ? false : roleRep.isScopeParamRequired();
+        role.setScopeParamRequired(scopeParamRequired);
     }
 
     private static void addComposites(RoleModel role, RoleRepresentation roleRep, RealmModel realm) {
@@ -692,7 +696,6 @@ 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());
@@ -789,7 +792,6 @@ 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/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 366dc31..8003b70 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,16 +462,6 @@ 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/RoleAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RoleAdapter.java
index 9448def..7706459 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RoleAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RoleAdapter.java
@@ -89,6 +89,16 @@ public class RoleAdapter implements RoleModel {
     }
 
     @Override
+    public boolean isScopeParamRequired() {
+        return role.isScopeParamRequired();
+    }
+
+    @Override
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        role.setScopeParamRequired(scopeParamRequired);
+    }
+
+    @Override
     public boolean isComposite() {
         return role.getCompositeRoleIds() != null && role.getCompositeRoleIds().size() > 0;
     }
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 582e5f1..9f79b15 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,18 +431,6 @@ 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/RoleAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index 536c2ef..861c252 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -60,6 +60,18 @@ public class RoleAdapter implements RoleModel {
     }
 
     @Override
+    public boolean isScopeParamRequired() {
+        if (updated != null) return updated.isScopeParamRequired();
+        return cached.isScopeParamRequired();
+    }
+
+    @Override
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        getDelegateForUpdate();
+        updated.setScopeParamRequired(scopeParamRequired);
+    }
+
+    @Override
     public String getId() {
         if (updated != null) return updated.getId();
         return cached.getId();
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 b90c077..11447d0 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,7 +47,6 @@ 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;
@@ -82,7 +81,6 @@ 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));
@@ -191,10 +189,6 @@ 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/CachedRole.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRole.java
index 6afef48..1bbaa5c 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRole.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRole.java
@@ -17,6 +17,7 @@ public class CachedRole implements Serializable {
     final protected String name;
     final protected String realm;
     final protected String description;
+    final protected Boolean scopeParamRequired;
     final protected boolean composite;
     final protected Set<String> composites = new HashSet<String>();
 
@@ -25,6 +26,7 @@ public class CachedRole implements Serializable {
         description = model.getDescription();
         id = model.getId();
         name = model.getName();
+        scopeParamRequired = model.isScopeParamRequired();
         this.realm = realm.getId();
         if (composite) {
             for (RoleModel child : model.getComposites()) {
@@ -50,6 +52,10 @@ public class CachedRole implements Serializable {
         return description;
     }
 
+    public Boolean isScopeParamRequired() {
+        return scopeParamRequired;
+    }
+
     public boolean isComposite() {
         return composite;
     }
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 9fc7377..b0acc41 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,16 +482,6 @@ 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 d853cb3..1f6ac25 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,9 +100,6 @@ 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;
 
@@ -319,14 +316,6 @@ 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/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
index 4c9edb7..437867e 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
@@ -37,6 +37,8 @@ public class RoleEntity {
     private String name;
     @Column(name = "DESCRIPTION")
     private String description;
+    @Column(name = "SCOPE_PARAM_REQUIRED")
+    private boolean scopeParamRequired;
 
     // hax! couldn't get constraint to work properly
     @Column(name = "REALM_ID")
@@ -93,6 +95,14 @@ public class RoleEntity {
         this.description = description;
     }
 
+    public boolean isScopeParamRequired() {
+        return scopeParamRequired;
+    }
+
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        this.scopeParamRequired = scopeParamRequired;
+    }
+
     public Collection<RoleEntity> getCompositeRoles() {
         return compositeRoles;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
index 4ab8d33..0e49d59 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
@@ -50,6 +50,16 @@ public class RoleAdapter implements RoleModel {
     }
 
     @Override
+    public boolean isScopeParamRequired() {
+        return role.isScopeParamRequired();
+    }
+
+    @Override
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        role.setScopeParamRequired(scopeParamRequired);
+    }
+
+    @Override
     public String getId() {
         return role.getId();
     }
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 2cb863e..26effca 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,17 +484,6 @@ 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/RoleAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
index 91ef361..f3d918b 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
@@ -69,6 +69,17 @@ public class RoleAdapter extends AbstractMongoAdapter<MongoRoleEntity> implement
     }
 
     @Override
+    public boolean isScopeParamRequired() {
+        return role.isScopeParamRequired();
+    }
+
+    @Override
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        role.setScopeParamRequired(scopeParamRequired);
+        updateRole();
+    }
+
+    @Override
     public boolean isComposite() {
         return role.getCompositeRoleIds() != null && role.getCompositeRoleIds().size() > 0;
     }
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 46d995f..c43140a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -2,6 +2,7 @@ package org.keycloak.protocol.oidc;
 
 import org.jboss.logging.Logger;
 import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.OAuthErrorException;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
@@ -30,7 +31,7 @@ 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.services.offline.OfflineTokenUtils;
 import org.keycloak.util.RefreshTokenUtil;
 import org.keycloak.util.Time;
 
@@ -38,6 +39,9 @@ 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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -92,13 +96,9 @@ public class TokenManager {
         UserSessionModel userSession = null;
         ClientSessionModel clientSession = null;
         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");
-                }
 
+            clientSession = OfflineTokenUtils.findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
+            if (clientSession != null) {
                 userSession = clientSession.getUserSession();
             }
         } else {
@@ -136,7 +136,8 @@ public class TokenManager {
 
 
         // recreate token.
-        Set<RoleModel> requestedRoles = TokenManager.getAccess(null, clientSession.getClient(), user);
+        String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
+        Set<RoleModel> requestedRoles = TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user);
         AccessToken newToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
         verifyAccess(oldToken, newToken);
 
@@ -233,7 +234,8 @@ public class TokenManager {
         clientSession.setUserSession(session);
         Set<String> requestedRoles = new HashSet<String>();
         // todo scope param protocol independent
-        for (RoleModel r : TokenManager.getAccess(null, clientSession.getClient(), user)) {
+        String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
+        for (RoleModel r : TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user)) {
             requestedRoles.add(r.getId());
         }
         clientSession.setRoles(requestedRoles);
@@ -269,26 +271,62 @@ public class TokenManager {
         }
     }
 
-    public static Set<RoleModel> getAccess(String scopeParam, ClientModel client, UserModel user) {
-        // todo scopeParam is ignored until we figure out a scheme that fits with openid connect
+    public static Set<RoleModel> getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) {
         Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
 
         Set<RoleModel> roleMappings = user.getRoleMappings();
-        if (client.isFullScopeAllowed()) return roleMappings;
 
-        Set<RoleModel> scopeMappings = client.getScopeMappings();
-        scopeMappings.addAll(client.getRoles());
+        if (client.isFullScopeAllowed()) {
+            requestedRoles = roleMappings;
+        } else {
+
+            Set<RoleModel> scopeMappings = client.getScopeMappings();
+            scopeMappings.addAll(client.getRoles());
+
+            for (RoleModel role : roleMappings) {
+                for (RoleModel desiredRole : scopeMappings) {
+                    Set<RoleModel> visited = new HashSet<RoleModel>();
+                    applyScope(role, desiredRole, visited, requestedRoles);
+                }
+            }
+        }
+
+        if (applyScopeParam) {
+            Collection<String> scopeParamRoles;
+            if (scopeParam != null) {
+                String[] scopes = scopeParam.split(" ");
+                scopeParamRoles = Arrays.asList(scopes);
+            } else {
+                scopeParamRoles = Collections.emptyList();
+            }
 
-        for (RoleModel role : roleMappings) {
-            for (RoleModel desiredRole : scopeMappings) {
-                Set<RoleModel> visited = new HashSet<RoleModel>();
-                applyScope(role, desiredRole, visited, requestedRoles);
+            Set<RoleModel> roles = new HashSet<>();
+            for (RoleModel role : requestedRoles) {
+                String roleName = getRoleNameForScopeParam(role);
+                if (!role.isScopeParamRequired() || scopeParamRoles.contains(roleName)) {
+                    roles.add(role);
+                } else {
+                    if (logger.isTraceEnabled()) {
+                        logger.tracef("Role '%s' excluded by scope param. Client is '%s', User is '%s', Scope param is '%s' ", role.getName(), client.getClientId(), user.getUsername(), scopeParam);
+                    }
+                }
             }
+            requestedRoles = roles;
         }
 
         return requestedRoles;
     }
 
+    // For now, just use "roleName" for realm roles and "clientId/roleName" for client roles
+    private static String getRoleNameForScopeParam(RoleModel role) {
+        if (role.getContainer() instanceof RealmModel) {
+            return role.getName();
+        } else {
+            ClientModel client = (ClientModel) role.getContainer();
+            return client.getClientId() + "/" + role.getName();
+        }
+    }
+
     public void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException {
         if (token.getRealmAccess() != null) {
             if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles");
@@ -437,7 +475,7 @@ public class TokenManager {
         public AccessTokenResponseBuilder generateAccessToken() {
             UserModel user = userSession.getUser();
             String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
-            Set<RoleModel> requestedRoles = getAccess(scopeParam, client, user);
+            Set<RoleModel> requestedRoles = getAccess(scopeParam, true, client, user);
             accessToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
             return this;
         }
@@ -450,14 +488,14 @@ public class TokenManager {
             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);
+                if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) {
+                    event.error(Errors.NOT_ALLOWED);
+                    throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST);
                 }
 
                 refreshToken = new RefreshToken(accessToken);
                 refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
-                new OfflineUserSessionManager().persistOfflineSession(clientSession, userSession);
+                OfflineTokenUtils.persistOfflineSession(clientSession, userSession);
             } else {
                 refreshToken = new RefreshToken(accessToken);
                 refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index fb0578e..958cd3e 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -27,6 +27,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.OAuthClientRepresentation;
 import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.timer.TimerProvider;
 
 import java.util.Collections;
@@ -95,6 +96,7 @@ public class RealmManager implements RealmImporter {
         setupImpersonationService(realm);
         setupAuthenticationFlows(realm);
         setupRequiredActions(realm);
+        setupOfflineTokens(realm);
 
         return realm;
     }
@@ -107,6 +109,10 @@ public class RealmManager implements RealmImporter {
         if (realm.getRequiredActionProviders().size() == 0) DefaultRequiredActions.addActions(realm);
     }
 
+    protected void setupOfflineTokens(RealmModel realm) {
+        KeycloakModelUtils.setupOfflineTokens(realm);
+    }
+
     protected void setupAdminConsole(RealmModel realm) {
         ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
         if (adminConsole == null) adminConsole = new ClientManager(this).createClient(realm, Constants.ADMIN_CONSOLE_CLIENT_ID);
@@ -216,12 +222,14 @@ public class RealmManager implements RealmImporter {
 
             RoleModel createRealmRole = realm.addRole(AdminRoles.CREATE_REALM);
             adminRole.addCompositeRole(createRealmRole);
-            createRealmRole.setDescription("${role_"+AdminRoles.CREATE_REALM+"}");
+            createRealmRole.setDescription("${role_" + AdminRoles.CREATE_REALM + "}");
+            createRealmRole.setScopeParamRequired(false);
         } else {
             adminRealm = model.getRealmByName(Config.getAdminRealm());
             adminRole = adminRealm.getRole(AdminRoles.ADMIN);
         }
         adminRole.setDescription("${role_"+AdminRoles.ADMIN+"}");
+        adminRole.setScopeParamRequired(false);
 
         ClientModel realmAdminApp = KeycloakModelUtils.createClient(adminRealm, KeycloakModelUtils.getMasterRealmAdminApplicationClientId(realm.getName()));
         // No localized name for now
@@ -232,6 +240,7 @@ public class RealmManager implements RealmImporter {
         for (String r : AdminRoles.ALL_REALM_ROLES) {
             RoleModel role = realmAdminApp.addRole(r);
             role.setDescription("${role_"+r+"}");
+            role.setScopeParamRequired(false);
             adminRole.addCompositeRole(role);
         }
     }
@@ -249,12 +258,14 @@ public class RealmManager implements RealmImporter {
         }
         RoleModel adminRole = realmAdminClient.addRole(AdminRoles.REALM_ADMIN);
         adminRole.setDescription("${role_" + AdminRoles.REALM_ADMIN + "}");
+        adminRole.setScopeParamRequired(false);
         realmAdminClient.setBearerOnly(true);
         realmAdminClient.setFullScopeAllowed(false);
 
         for (String r : AdminRoles.ALL_REALM_ROLES) {
             RoleModel role = realmAdminClient.addRole(r);
             role.setDescription("${role_"+r+"}");
+            role.setScopeParamRequired(false);
             adminRole.addCompositeRole(role);
         }
     }
@@ -274,7 +285,9 @@ public class RealmManager implements RealmImporter {
 
             for (String role : AccountRoles.ALL) {
                 client.addDefaultRole(role);
-                client.getRole(role).setDescription("${role_"+role+"}");
+                RoleModel roleModel = client.getRole(role);
+                roleModel.setDescription("${role_" + role + "}");
+                roleModel.setScopeParamRequired(false);
             }
         }
     }
@@ -292,7 +305,9 @@ public class RealmManager implements RealmImporter {
             client.setFullScopeAllowed(false);
 
             for (String role : Constants.BROKER_SERVICE_ROLES) {
-                client.addRole(role).setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
+                RoleModel roleModel = client.addRole(role);
+                roleModel.setDescription("${role_"+ role.toLowerCase().replaceAll("_", "-") +"}");
+                roleModel.setScopeParamRequired(false);
             }
         }
     }
@@ -329,6 +344,7 @@ public class RealmManager implements RealmImporter {
 
         if (!hasBrokerClient(rep)) setupBrokerService(realm);
         if (!hasAdminConsoleClient(rep)) setupAdminConsole(realm);
+        if (!hasRealmRole(rep, Constants.OFFLINE_ACCESS_ROLE)) setupOfflineTokens(realm);
 
         RepresentationToModel.importRealm(session, rep, realm);
 
@@ -409,6 +425,20 @@ public class RealmManager implements RealmImporter {
         return false;
     }
 
+    private boolean hasRealmRole(RealmRepresentation rep, String roleName) {
+        if (rep.getRoles() == null || rep.getRoles().getRealm() == null) {
+            return false;
+        }
+
+        for (RoleRepresentation role : rep.getRoles().getRealm()) {
+            if (roleName.equals(role.getName())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Query users based on a search string:
      * <p/>
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 14d3639..3b6edea 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -46,7 +46,6 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.CredentialValidation;
 import org.keycloak.models.utils.FormMessage;
 import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.representations.idm.CredentialRepresentation;
@@ -58,7 +57,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.offline.OfflineTokenUtils;
 import org.keycloak.services.util.ResolveRelative;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.util.UriUtils;
@@ -487,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
         // Revoke grant in UserModel
         UserModel user = auth.getUser();
         user.revokeConsentForClient(client.getId());
-        new OfflineUserSessionManager().revokeOfflineToken(user, client);
+        OfflineTokenUtils.revokeOfflineToken(user, client);
 
         // Logout clientSessions for this user and client
         AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
index 168ff47..879ff6f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
@@ -82,6 +82,8 @@ public class RoleContainerResource extends RoleResource {
         try {
             RoleModel role = roleContainer.addRole(rep.getName());
             role.setDescription(rep.getDescription());
+            boolean scopeParamRequired = rep.isScopeParamRequired()==null ? false : rep.isScopeParamRequired();
+            role.setScopeParamRequired(scopeParamRequired);
 
             adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, role.getId()).representation(rep).success();
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
index 827b817..3635da8 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
@@ -38,6 +38,7 @@ public abstract class RoleResource {
     protected void updateRole(RoleRepresentation rep, RoleModel role) {
         role.setName(rep.getName());
         role.setDescription(rep.getDescription());
+        if (rep.isScopeParamRequired() != null) role.setScopeParamRequired(rep.isScopeParamRequired());
     }
 
     protected void addComposites(AdminEventBuilder adminEvent, UriInfo uriInfo, List<RoleRepresentation> roles, RoleModel role) {
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 1f401fa..9458998 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
@@ -668,7 +668,8 @@ public class AccountTest {
         Assert.assertTrue(accountEntry.getProtocolMappersGranted().contains("Full Access"));
 
         AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app");
-        Assert.assertEquals(4, testAppEntry.getRolesAvailable().size());
+        Assert.assertEquals(5, testAppEntry.getRolesAvailable().size());
+        Assert.assertTrue(testAppEntry.getRolesAvailable().contains("Offline access"));
         Assert.assertTrue(testAppEntry.getRolesGranted().contains("Full Access"));
         Assert.assertTrue(testAppEntry.getProtocolMappersGranted().contains("Full Access"));
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
index bae16d5..aaed86e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
@@ -85,7 +85,7 @@ public class AdminAPITest {
             ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
             clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
             UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
-            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
+            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
             return tm.encodeToken(adminRealm, token);
         } finally {
             keycloakRule.stopSession(session, true);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
index c30ee36..2097175 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
@@ -108,7 +108,7 @@ public class ClientTest extends AbstractClientTest {
         response.close();
         String id = ApiUtil.getCreatedId(response);
 
-        RoleRepresentation role = new RoleRepresentation("test", "test");
+        RoleRepresentation role = new RoleRepresentation("test", "test", false);
         realm.clients().get(id).roles().create(role);
 
         rep = realm.clients().get(id).toRepresentation();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java
index 0de7dd6..0f1510b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java
@@ -123,7 +123,7 @@ public class ImpersonationTest {
             ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
             clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/" + realm);
             UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
-            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
+            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
             return tm.encodeToken(adminRealm, token);
         } finally {
             keycloakRule.stopSession(session, true);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
index 6cf6427..8208aa2 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
@@ -112,7 +112,7 @@ public class RealmTest extends AbstractClientTest {
     @Test
     // KEYCLOAK-1110
     public void deleteDefaultRole() {
-        RoleRepresentation role = new RoleRepresentation("test", "test");
+        RoleRepresentation role = new RoleRepresentation("test", "test", false);
         realm.roles().create(role);
 
         assertNotNull(realm.roles().get("test").toRepresentation());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AbstractModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AbstractModelTest.java
index a3e0976..d6d8724 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AbstractModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AbstractModelTest.java
@@ -117,6 +117,7 @@ public class AbstractModelTest {
         Assert.assertEquals(expected.getId(), actual.getId());
         Assert.assertEquals(expected.getName(), actual.getName());
         Assert.assertEquals(expected.getDescription(), actual.getDescription());
+        Assert.assertEquals(expected.isScopeParamRequired(), actual.isScopeParamRequired());
         Assert.assertEquals(expected.getContainer(), actual.getContainer());
         Assert.assertEquals(expected.getComposites().size(), actual.getComposites().size());
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 48ed318..4e771df 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -68,8 +68,8 @@ public class AdapterTest extends AbstractModelTest {
         Assert.assertEquals(realmModel.getName(), "JUGGLER");
         Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
         Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
-        Assert.assertEquals(1, realmModel.getDefaultRoles().size());
-        Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0));
+        Assert.assertEquals(2, realmModel.getDefaultRoles().size());
+        Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
     }
 
     @Test
@@ -94,8 +94,8 @@ public class AdapterTest extends AbstractModelTest {
         Assert.assertEquals(realmModel.getName(), "JUGGLER");
         Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
         Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
-        Assert.assertEquals(1, realmModel.getDefaultRoles().size());
-        Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0));
+        Assert.assertEquals(2, realmModel.getDefaultRoles().size());
+        Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
 
         realmModel.getId();
 
@@ -444,7 +444,7 @@ public class AdapterTest extends AbstractModelTest {
         realmModel.addRole("admin");
         realmModel.addRole("user");
         Set<RoleModel> roles = realmModel.getRoles();
-        Assert.assertEquals(3, roles.size());
+        Assert.assertEquals(4, roles.size());
         UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
         RoleModel realmUserRole = realmModel.getRole("user");
         user.grantRole(realmUserRole);
@@ -470,7 +470,7 @@ public class AdapterTest extends AbstractModelTest {
         user.grantRole(application.getRole("user"));
 
         roles = user.getRealmRoleMappings();
-        Assert.assertEquals(roles.size(), 2);
+        Assert.assertEquals(roles.size(), 3);
         assertRolesContains(realmUserRole, roles);
         Assert.assertTrue(user.hasRole(realmUserRole));
         // Role "foo" is default realm role
@@ -485,13 +485,13 @@ public class AdapterTest extends AbstractModelTest {
         // Test that application role 'user' don't clash with realm role 'user'
         Assert.assertNotEquals(realmModel.getRole("user").getId(), application.getRole("user").getId());
 
-        Assert.assertEquals(6, user.getRoleMappings().size());
+        Assert.assertEquals(7, user.getRoleMappings().size());
 
         // Revoke some roles
         user.deleteRoleMapping(realmModel.getRole("foo"));
         user.deleteRoleMapping(appBarRole);
         roles = user.getRoleMappings();
-        Assert.assertEquals(4, roles.size());
+        Assert.assertEquals(5, roles.size());
         assertRolesContains(realmUserRole, roles);
         assertRolesContains(application.getRole("user"), roles);
         Assert.assertFalse(user.hasRole(appBarRole));
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 d56056d..6f42fd5 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
@@ -79,7 +79,7 @@ public class ImportTest extends AbstractModelTest {
         Assert.assertEquals(1, creds.size());
         RequiredCredentialModel cred = creds.get(0);
         Assert.assertEquals("password", cred.getFormLabel());
-        Assert.assertEquals(2, realm.getDefaultRoles().size());
+        Assert.assertEquals(3, realm.getDefaultRoles().size());
 
         Assert.assertNotNull(realm.getRole("foo"));
         Assert.assertNotNull(realm.getRole("bar"));
@@ -132,6 +132,10 @@ public class ImportTest extends AbstractModelTest {
         Assert.assertTrue(allRoles.contains(application.getRole("app-admin")));
         Assert.assertTrue(allRoles.contains(otherApp.getRole("otherapp-admin")));
 
+        Assert.assertTrue(application.getRole("app-admin").isScopeParamRequired());
+        Assert.assertFalse(otherApp.getRole("otherapp-admin").isScopeParamRequired());
+        Assert.assertFalse(otherApp.getRole("otherapp-user").isScopeParamRequired());
+
         UserModel wburke =  session.users().getUserByUsername("wburke", realm);
         // user with creation timestamp in import
         Assert.assertEquals(new Long(123654), wburke.getCreatedTimestamp());
@@ -326,8 +330,6 @@ 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/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 2363305..059cdd7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -34,6 +34,7 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
@@ -290,4 +291,71 @@ public class OAuthGrantTest {
         });
     }
 
+    @Test
+    public void oauthGrantScopeParamRequired() throws Exception {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ClientModel thirdParty = appRealm.getClientByClientId("third-party");
+                RoleModel barAppRole = thirdParty.addRole("bar-role");
+                barAppRole.setScopeParamRequired(true);
+
+                RoleModel fooRole = appRealm.addRole("foo-role");
+                fooRole.setScopeParamRequired(true);
+                thirdParty.addScopeMapping(fooRole);
+
+                UserModel testUser = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+                testUser.grantRole(fooRole);
+                testUser.grantRole(barAppRole);
+            }
+
+        });
+
+        // Assert roles not on grant screen when not requested
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
+        grantPage.assertCurrent();
+        Assert.assertFalse(driver.getPageSource().contains("foo-role"));
+        Assert.assertFalse(driver.getPageSource().contains("bar-role"));
+        grantPage.cancel();
+
+        events.expectLogin()
+                .client("third-party")
+                .error("rejected_by_user")
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+        oauth.scope("foo-role third-party/bar-role");
+        oauth.doLoginGrant("test-user@localhost", "password");
+        grantPage.assertCurrent();
+        Assert.assertTrue(driver.getPageSource().contains("foo-role"));
+        Assert.assertTrue(driver.getPageSource().contains("bar-role"));
+        grantPage.accept();
+
+        events.expectLogin()
+                .client("third-party")
+                .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
+                .assertEvent();
+
+        // Revoke
+        accountAppsPage.open();
+        accountAppsPage.revokeGrant("third-party");
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "third-party").assertEvent();
+
+        // cleanup
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.removeRole(appRealm.getRole("foo-role"));
+                ClientModel thirdparty = appRealm.getClientByClientId("third-party");
+                thirdparty.removeRole(thirdparty.getRole("bar-role"));
+            }
+
+        });
+
+    }
+
 }
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
index 33e6ff4..8eab300 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -26,6 +26,7 @@ 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.Constants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
@@ -37,6 +38,7 @@ 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.pages.OAuthGrantPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.WebResource;
 import org.keycloak.testsuite.rule.WebRule;
@@ -74,7 +76,6 @@ public class OfflineTokenTest {
             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");
@@ -102,6 +103,9 @@ public class OfflineTokenTest {
     protected LoginPage loginPage;
 
     @WebResource
+    protected OAuthGrantPage oauthGrantPage;
+
+    @WebResource
     protected AccountApplicationsPage accountAppPage;
 
     @Rule
@@ -115,23 +119,80 @@ public class OfflineTokenTest {
 
     @Test
     public void offlineTokenDisabledForClient() throws Exception {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("offline-client").setFullScopeAllowed(false);
+            }
+
+        });
+
         oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
         oauth.doLogin("test-user@localhost", "password");
 
-        Event loginEvent = events.expectLogin().assertEvent();
+        Event loginEvent = events.expectLogin()
+                .client("offline-client")
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .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, "secret1");
+
+        assertEquals(400, tokenResponse.getStatusCode());
+        assertEquals("not_allowed", tokenResponse.getError());
+
+        events.expectCodeToToken(codeId, sessionId)
+                .client("offline-client")
+                .error("not_allowed")
+                .clearDetails()
+                .assertEvent();
+
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("offline-client").setFullScopeAllowed(true);
+            }
+
+        });
+    }
+
+    @Test
+    public void offlineTokenUserNotAllowed() throws Exception {
+        String userId = keycloakRule.getUser("test", "keycloak-user@localhost").getId();
+
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("offline-client");
+        oauth.redirectUri(offlineClientAppUri);
+        oauth.doLogin("keycloak-user@localhost", "password");
+
+        Event loginEvent = events.expectLogin()
+                .client("offline-client")
+                .user(userId)
+                .detail(Details.REDIRECT_URI, offlineClientAppUri)
+                .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");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
 
         assertEquals(400, tokenResponse.getStatusCode());
-        assertEquals("invalid_client", tokenResponse.getError());
+        assertEquals("not_allowed", tokenResponse.getError());
 
         events.expectCodeToToken(codeId, sessionId)
-                .error("invalid_client")
+                .client("offline-client")
+                .user(userId)
+                .error("not_allowed")
                 .clearDetails()
                 .assertEvent();
     }
@@ -206,8 +267,9 @@ public class OfflineTokenTest {
 
         Assert.assertEquals(userId, refreshedToken.getSubject());
 
-        Assert.assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
+        Assert.assertEquals(2, refreshedToken.getRealmAccess().getRoles().size());
         Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE));
 
         Assert.assertEquals(1, refreshedToken.getResourceAccess("test-app").getRoles().size());
         Assert.assertTrue(refreshedToken.getResourceAccess("test-app").isUserInRole("customer-user"));
@@ -374,7 +436,7 @@ public class OfflineTokenTest {
         accountAppPage.open();
         List<String> additionalGrants = accountAppPage.getApplications().get("offline-client").getAdditionalGrants();
         Assert.assertEquals(additionalGrants.size(), 1);
-        Assert.assertEquals(additionalGrants.get(0), "Offline Access");
+        Assert.assertEquals(additionalGrants.get(0), "Offline Token");
         accountAppPage.revokeGrant("offline-client");
         Assert.assertEquals(accountAppPage.getApplications().get("offline-client").getAdditionalGrants().size(), 0);
 
@@ -389,6 +451,55 @@ public class OfflineTokenTest {
         Time.setOffset(0);
     }
 
+    @Test
+    public void testServletWithConsent() {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("offline-client").setConsentRequired(true);
+            }
+
+        });
+
+        // Assert grant page doesn't have 'Offline Access' role when offline token is not requested
+        driver.navigate().to(offlineClientAppUri);
+        loginPage.login("test-user@localhost", "password");
+        oauthGrantPage.assertCurrent();
+        Assert.assertFalse(driver.getPageSource().contains("Offline access"));
+        oauthGrantPage.cancel();
+
+        // Assert grant page has 'Offline Access' role now
+        String servletUri = UriBuilder.fromUri(offlineClientAppUri)
+                .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
+                .build().toString();
+        driver.navigate().to(servletUri);
+        loginPage.login("test-user@localhost", "password");
+        oauthGrantPage.assertCurrent();
+        Assert.assertTrue(driver.getPageSource().contains("Offline access"));
+        oauthGrantPage.accept();
+
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
+        Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+
+        accountAppPage.open();
+        AccountApplicationsPage.AppEntry offlineClient = accountAppPage.getApplications().get("offline-client");
+        Assert.assertTrue(offlineClient.getRolesGranted().contains("Offline access"));
+        Assert.assertTrue(offlineClient.getAdditionalGrants().contains("Offline Token"));
+
+        events.clear();
+
+        // Revert change
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.getClientByClientId("offline-client").setConsentRequired(false);
+            }
+
+        });
+    }
+
     public static class OfflineTokenServlet extends HttpServlet {
 
         private static TokenInfo tokenInfo;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
index d556a83..2d2ab44 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
@@ -457,7 +457,7 @@ public class SamlBindingTest {
             ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
             clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
             UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
-            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
+            AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
             return tm.encodeToken(adminRealm, token);
         } finally {
             keycloakRule.stopSession(session, true);
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index 55a75b5..f7c8cdd 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -164,7 +164,6 @@
             "name": "Other Application",
             "enabled": true,
             "serviceAccountsEnabled": true,
-            "offlineTokensEnabled": true,
             "clientAuthenticatorType": "client-jwt",
             "protocolMappers" : [
                 {
@@ -199,7 +198,8 @@
         "application" : {
             "Application" : [
                 {
-                    "name": "app-admin"
+                    "name": "app-admin",
+                    "scopeParamRequired": true
                 },
                 {
                     "name": "app-user"
@@ -207,7 +207,8 @@
             ],
             "OtherApp" : [
                 {
-                    "name": "otherapp-admin"
+                    "name": "otherapp-admin",
+                    "scopeParamRequired": false
                 },
                 {
                     "name": "otherapp-user"
diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json
index 5344ad0..e16e3e2 100755
--- a/testsuite/integration/src/test/resources/testrealm.json
+++ b/testsuite/integration/src/test/resources/testrealm.json
@@ -26,7 +26,7 @@
                 { "type" : "password",
                   "value" : "password" }
             ],
-            "realmRoles": ["user"],
+            "realmRoles": ["user", "offline_access"],
             "clientRoles": {
                 "test-app": [ "customer-user" ],
                 "account": [ "view-profile", "manage-account" ]
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/roles/RoleForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/roles/RoleForm.java
index ac1db15..b4f974a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/roles/RoleForm.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/roles/RoleForm.java
@@ -18,6 +18,9 @@ public class RoleForm extends Form {
     @FindBy(id = "description")
     private WebElement descriptionInput;
 
+    @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='scopeParamRequired']]")
+    private OnOffSwitch scopeParamRequired;
+
     @FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='compositeSwitch']]")
     private OnOffSwitch compositeSwitch;
 
@@ -28,7 +31,7 @@ public class RoleForm extends Form {
     private WebElement removeIcon;
 
     public RoleRepresentation getRole() {
-        RoleRepresentation role = new RoleRepresentation(getName(), getDescription());
+        RoleRepresentation role = new RoleRepresentation(getName(), getDescription(), isScopeParamRequired());
         role.setComposite(isComposite());
         if (role.isComposite()) {
             role.setComposites(compositeRoles.getComposites());
@@ -44,6 +47,7 @@ public class RoleForm extends Form {
         RoleRepresentation role = new RoleRepresentation();
         role.setName(getName());
         role.setDescription(getDescription());
+        role.setScopeParamRequired(isScopeParamRequired());
         role.setComposite(isComposite());
         log.info(role.getName() + ": " + role.getDescription() + ", comp: " + role.isComposite());
         return role;
@@ -52,6 +56,7 @@ public class RoleForm extends Form {
     public void setBasicAttributes(RoleRepresentation role) {
         setName(role.getName());
         setDescription(role.getDescription());
+        setScopeParamRequired(role.isScopeParamRequired());
         if (role.isComposite()) {
             setCompositeRoles(role);
         }
@@ -82,6 +87,14 @@ public class RoleForm extends Form {
         return getInputValue(descriptionInput);
     }
 
+    public void setScopeParamRequired(boolean scopeParamRequired) {
+        this.scopeParamRequired.setOn(scopeParamRequired);
+    }
+
+    public boolean isScopeParamRequired() {
+        return scopeParamRequired.isOn();
+    }
+
     public void setComposite(boolean composite) {
         compositeSwitch.setOn(composite);
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/ClientRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/ClientRolesTest.java
index e005d60..a8a4c35 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/ClientRolesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/ClientRolesTest.java
@@ -44,7 +44,7 @@ public class ClientRolesTest extends AbstractClientTest {
     @Test
     public void testAddClientRole() {
         ClientRepresentation newClient = createClientRepresentation("test-client1", "http://example.com/*");
-        RoleRepresentation newRole = new RoleRepresentation("client-role", "");
+        RoleRepresentation newRole = new RoleRepresentation("client-role", "", false);
 
         createClient(newClient);
         assertFlashMessageSuccess();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/DefaultRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/DefaultRolesTest.java
index 2f3bd8e..e156c39 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/DefaultRolesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/DefaultRolesTest.java
@@ -30,7 +30,7 @@ public class DefaultRolesTest extends AbstractRolesTest {
     @Before
     public void beforeDefaultRolesTest() {
         // create a role via admin client
-        defaultRoleRep = new RoleRepresentation("default-role", "");
+        defaultRoleRep = new RoleRepresentation("default-role", "", false);
         rolesPage.rolesResource().create(defaultRoleRep);
 
         defaultRolesPage.navigateTo();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/RealmRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/RealmRolesTest.java
index f92ccac..0df9c15 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/RealmRolesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/roles/RealmRolesTest.java
@@ -32,7 +32,7 @@ public class RealmRolesTest extends AbstractRolesTest {
     
     @Before
     public void beforeTestAddNewRole() {
-        testRole = new RoleRepresentation("test_role", "role description");
+        testRole = new RoleRepresentation("test_role", "role description", false);
         realmRolesPage.navigateTo();
     }
     
@@ -104,7 +104,7 @@ public class RealmRolesTest extends AbstractRolesTest {
     @Ignore
     public void testAddRoleWithLongName() {
         String name = "hjewr89y1894yh98(*&*&$jhjkashd)*(&y8934h*&@#hjkahsdj";
-        addRole(new RoleRepresentation(name, ""));
+        addRole(new RoleRepresentation(name, "", false));
         assertNotNull(realmRolesPage.table().findRole(name));
     }
     
@@ -124,7 +124,7 @@ public class RealmRolesTest extends AbstractRolesTest {
         Timer.time();
         for (int i = 0; i < count; i++) {
             String roleName = String.format("%s%02d", namePrefix, i);
-            RoleRepresentation rr = new RoleRepresentation(roleName, "");
+            RoleRepresentation rr = new RoleRepresentation(roleName, "", false);
             testRealmResource().roles().create(rr);
         }
         Timer.time("create " + count + " roles");