From 046edbbd54dd28dbed8f1defe04b4b0106f9e4fe Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 23 Sep 2015 12:52:37 +0200 Subject: [PATCH] KEYCLOAK-904 Consents support. Added scopeParamRequired flag to RoleModel --- .../META-INF/jpa-changelog-1.6.0.xml | 4 +- .../idm/ClientRepresentation.java | 9 -- .../idm/RoleRepresentation.java | 12 +- .../freemarker/model/ApplicationsBean.java | 9 +- .../account/messages/messages_en.properties | 3 +- .../resources/partials/client-detail.html | 7 - .../partials/client-role-detail.html | 7 + .../admin/resources/partials/role-detail.html | 7 + .../login/messages/messages_en.properties | 1 + .../keycloak/migration/MigrationModel.java | 2 +- .../migration/MigrationModelManager.java | 7 + .../migration/migrators/MigrateTo1_6_0.java | 25 ++++ .../migrators/MigrationTo1_2_0_CR1.java | 5 +- .../java/org/keycloak/models/ClientModel.java | 3 - .../java/org/keycloak/models/Constants.java | 3 + .../models/ImpersonationConstants.java | 2 + .../java/org/keycloak/models/RoleModel.java | 4 + .../models/entities/ClientEntity.java | 9 -- .../keycloak/models/entities/RoleEntity.java | 9 ++ .../models/utils/KeycloakModelUtils.java | 10 ++ .../models/utils/ModelToRepresentation.java | 2 +- .../models/utils/RepresentationToModel.java | 6 +- .../models/file/adapter/ClientAdapter.java | 10 -- .../models/file/adapter/RoleAdapter.java | 10 ++ .../cache/infinispan/ClientAdapter.java | 12 -- .../models/cache/infinispan/RoleAdapter.java | 12 ++ .../models/cache/entities/CachedClient.java | 6 - .../models/cache/entities/CachedRole.java | 6 + .../keycloak/models/jpa/ClientAdapter.java | 10 -- .../org/keycloak/models/jpa/RoleAdapter.java | 10 ++ .../models/jpa/entities/ClientEntity.java | 11 -- .../models/jpa/entities/RoleEntity.java | 10 ++ .../keycloak/adapters/ClientAdapter.java | 11 -- .../mongo/keycloak/adapters/RoleAdapter.java | 11 ++ .../keycloak/protocol/oidc/TokenManager.java | 84 ++++++++---- .../services/managers/RealmManager.java | 36 ++++- ...ionManager.java => OfflineTokenUtils.java} | 33 +++-- .../services/resources/AccountService.java | 5 +- .../admin/RoleContainerResource.java | 2 + .../resources/admin/RoleResource.java | 1 + .../testsuite/account/AccountTest.java | 3 +- .../testsuite/admin/AdminAPITest.java | 2 +- .../keycloak/testsuite/admin/ClientTest.java | 2 +- .../testsuite/admin/ImpersonationTest.java | 2 +- .../keycloak/testsuite/admin/RealmTest.java | 2 +- .../testsuite/model/AbstractModelTest.java | 1 + .../keycloak/testsuite/model/AdapterTest.java | 16 +-- .../keycloak/testsuite/model/ImportTest.java | 8 +- .../testsuite/oauth/OAuthGrantTest.java | 68 ++++++++++ .../testsuite/oauth/OfflineTokenTest.java | 125 +++++++++++++++++- .../testsuite/saml/SamlBindingTest.java | 2 +- .../src/test/resources/model/testrealm.json | 7 +- .../src/test/resources/testrealm.json | 2 +- 53 files changed, 495 insertions(+), 171 deletions(-) create mode 100644 model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java rename services/src/main/java/org/keycloak/services/offline/{OfflineUserSessionManager.java => OfflineTokenUtils.java} (85%) 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 061eaa3e559..24902d56da3 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 @@ - - + + 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 42618a14e5b..020445e4066 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 c335ab44c4d..41007856130 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 63e04db10d1..371b5d0201c 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 offlineClients = new OfflineUserSessionManager().findClientsWithOfflineToken(realm, user); + Set offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(realm, user); List realmClients = realm.getClients(); for (ClientModel client : realmClients) { @@ -34,7 +33,7 @@ public class ApplicationsBean { continue; } - Set availableRoles = TokenManager.getAccess(null, client, user); + Set 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 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 b0ea19ccd5d..23a112d28a8 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 303e4391a76..06c939bc4c4 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 @@ -
- - 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. -
- -
-
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 c0ce7663017..f93c96eb069 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> -->
+
+ + This role will be granted just if scope parameter with role name is used during authorization/token request. +
+ +
+
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 49d09c835e7..6bf854ec570 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 @@
+
+ + This role will be granted just if scope parameter with role name is used during authorization/token request. +
+ +
+
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 3dfb8eab9ac..94b06272f2d 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 792822e7315..beb2f984be4 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 61b6244ff9d..e5c94840f79 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 00000000000..80105861975 --- /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 Marek Posolda + */ +public class MigrateTo1_6_0 { + + public static final ModelVersion VERSION = new ModelVersion("1.6.0"); + + public void migrate(KeycloakSession session) { + List 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 c9e10ad232b..1aa4929eb75 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 6461a339e7b..daccf8e0aa3 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 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 b1adbfa6a4f..5fe3189e3fc 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 Bill Burke * @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/ImpersonationConstants.java b/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java index 274e7a60601..54f212a8dd2 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 c296795b537..9904a1696c5 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/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 83fa12dc4eb..52e972196ba 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 a610d39d696..4b4551e7ebc 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 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 getCompositeRoleIds() { return compositeRoleIds; } 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 b64ebb04b62..846269ef8f7 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 a3828803f29..b71fce9ff4f 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 3907f966480..77ae66e8735 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 366dc31e143..8003b70dedc 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 @@ -461,16 +461,6 @@ public class ClientAdapter implements ClientModel { entity.setServiceAccountsEnabled(serviceAccountsEnabled); } - @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 9448defce71..7706459df76 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 @@ -88,6 +88,16 @@ public class RoleAdapter implements RoleModel { role.setDescription(description); } + @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 582e5f1f1cf..9f79b157ed8 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 @@ -430,18 +430,6 @@ public class ClientAdapter implements ClientModel { updated.setServiceAccountsEnabled(serviceAccountsEnabled); } - @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); 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 536c2ef3164..861c25274e7 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 @@ -59,6 +59,18 @@ public class RoleAdapter implements RoleModel { updated.setDescription(description); } + @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(); 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 b90c0773af6..11447d02d7c 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 roles = new HashMap(); private int nodeReRegistrationTimeout; private Map 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 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 6afef48c48a..1bbaa5c3da3 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 composites = new HashSet(); @@ -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 9fc7377a19a..b0acc417fd1 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 @@ -481,16 +481,6 @@ public class ClientAdapter implements ClientModel { entity.setServiceAccountsEnabled(serviceAccountsEnabled); } - @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/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java index 4ab8d331760..0e49d59aa2d 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 @@ -49,6 +49,16 @@ public class RoleAdapter implements RoleModel { role.setDescription(description); } + @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/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 d853cb3f7cc..1f6ac251b37 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 4c9edb77248..437867e7e96 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 getCompositeRoles() { return compositeRoles; } 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 2cb863e550f..26effca9c33 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 @@ -483,17 +483,6 @@ public class ClientAdapter extends AbstractMongoAdapter imple updateMongoEntity(); } - @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 91ef361da2a..f3d918befdc 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 @@ -68,6 +68,17 @@ public class RoleAdapter extends AbstractMongoAdapter implement updateRole(); } + @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 46d995f4caa..c43140a1bf8 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 requestedRoles = TokenManager.getAccess(null, clientSession.getClient(), user); + String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE); + Set 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 requestedRoles = new HashSet(); // 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 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 getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) { Set requestedRoles = new HashSet(); Set roleMappings = user.getRoleMappings(); - if (client.isFullScopeAllowed()) return roleMappings; - Set scopeMappings = client.getScopeMappings(); - scopeMappings.addAll(client.getRoles()); + if (client.isFullScopeAllowed()) { + requestedRoles = roleMappings; + } else { - for (RoleModel role : roleMappings) { - for (RoleModel desiredRole : scopeMappings) { - Set visited = new HashSet(); - applyScope(role, desiredRole, visited, requestedRoles); + Set scopeMappings = client.getScopeMappings(); + scopeMappings.addAll(client.getRoles()); + + for (RoleModel role : roleMappings) { + for (RoleModel desiredRole : scopeMappings) { + Set visited = new HashSet(); + applyScope(role, desiredRole, visited, requestedRoles); + } } } + if (applyScopeParam) { + Collection scopeParamRoles; + if (scopeParam != null) { + String[] scopes = scopeParam.split(" "); + scopeParamRoles = Arrays.asList(scopes); + } else { + scopeParamRoles = Collections.emptyList(); + } + + Set 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 requestedRoles = getAccess(scopeParam, client, user); + Set 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 fb0578e8f37..958cd3e3aa7 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: *

diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java similarity index 85% rename from services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java rename to services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java index 5e1a0dce655..b4900c1a114 100644 --- a/services/src/main/java/org/keycloak/services/offline/OfflineUserSessionManager.java +++ b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java @@ -3,18 +3,17 @@ 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.Constants; import org.keycloak.models.ModelException; import org.keycloak.models.OfflineClientSessionModel; import org.keycloak.models.OfflineUserSessionModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.util.JsonSerialization; @@ -24,11 +23,11 @@ import org.keycloak.util.JsonSerialization; * * @author Marek Posolda */ -public class OfflineUserSessionManager { +public class OfflineTokenUtils { - protected static Logger logger = Logger.getLogger(OfflineUserSessionManager.class); + protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class); - public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { + public static void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { UserModel user = userSession.getUser(); ClientModel client = clientSession.getClient(); @@ -61,7 +60,7 @@ public class OfflineUserSessionManager { } // 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) { + public static ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) { OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId); if (clientSession == null) { return null; @@ -85,7 +84,7 @@ public class OfflineUserSessionManager { return clientSessionAdapter; } - public Set findClientsWithOfflineToken(RealmModel realm, UserModel user) { + public static Set findClientsWithOfflineToken(RealmModel realm, UserModel user) { Collection clientSessions = user.getOfflineClientSessions(); Set clients = new HashSet<>(); for (OfflineClientSessionModel clientSession : clientSessions) { @@ -95,7 +94,7 @@ public class OfflineUserSessionManager { return clients; } - public boolean revokeOfflineToken(UserModel user, ClientModel client) { + public static boolean revokeOfflineToken(UserModel user, ClientModel client) { Collection clientSessions = user.getOfflineClientSessions(); boolean anyRemoved = false; for (OfflineClientSessionModel clientSession : clientSessions) { @@ -114,7 +113,17 @@ public class OfflineUserSessionManager { return anyRemoved; } - private void createOfflineUserSession(UserModel user, UserSessionModel userSession) { + public static boolean isOfflineTokenAllowed(RealmModel realm, ClientSessionModel clientSession) { + RoleModel offlineAccessRole = realm.getRole(Constants.OFFLINE_ACCESS_ROLE); + if (offlineAccessRole == null) { + logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE); + return false; + } + + return clientSession.getRoles().contains(offlineAccessRole.getId()); + } + + private static void createOfflineUserSession(UserModel user, UserSessionModel userSession) { if (logger.isTraceEnabled()) { logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername()); } @@ -138,7 +147,7 @@ public class OfflineUserSessionManager { } } - private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) { + private static 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()); @@ -165,7 +174,7 @@ public class OfflineUserSessionManager { } // Check if userSession has any offline clientSessions attached to it. Remove userSession if not - private void checkUserSessionHasClientSessions(UserModel user, String userSessionId) { + private static void checkUserSessionHasClientSessions(UserModel user, String userSessionId) { Collection clientSessions = user.getOfflineClientSessions(); for (OfflineClientSessionModel clientSession : clientSessions) { 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 14d3639c2b6..3b6edea1909 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 168ff471c61..879ff6f12e5 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 827b817dd8a..3635da8a957 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 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 1f401fa5dec..9458998b194 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 bae16d5a9b1..aaed86e578e 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 c30ee360b24..209717571ec 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 0de7dd6dc6a..0f1510b2309 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 6cf642728ac..8208aa2fb7b 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 a3e09760439..d6d87245ee9 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 48ed318e374..4e771dfefd6 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 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 d56056d1520..6f42fd5a963 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 236330559eb..059cdd72866 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 33e6ff41711..8eab300ede7 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"); @@ -101,6 +102,9 @@ public class OfflineTokenTest { @WebResource protected LoginPage loginPage; + @WebResource + protected OAuthGrantPage oauthGrantPage; + @WebResource protected AccountApplicationsPage accountAppPage; @@ -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, "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") + .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, "secret1"); + + assertEquals(400, tokenResponse.getStatusCode()); + assertEquals("not_allowed", tokenResponse.getError()); + + events.expectCodeToToken(codeId, sessionId) + .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 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 d556a83a8a5..2d2ab44bb27 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 55a75b5038a..f7c8cdd2e1f 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 5344ad0e741..e16e3e2f88f 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" ]