Merge pull request #1638 from mposolda/master
KEYCLOAK-904 Consents support. Added scopeParamRequired flag to RoleM…
This commit is contained in:
commit
22f7efd977
57 changed files with 514 additions and 177 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ public interface RoleModel {
|
|||
|
||||
void setName(String name);
|
||||
|
||||
boolean isScopeParamRequired();
|
||||
|
||||
void setScopeParamRequired(boolean scopeParamRequired);
|
||||
|
||||
boolean isComposite();
|
||||
|
||||
void addCompositeRole(RoleModel role);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -483,17 +483,6 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> 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();
|
||||
|
|
|
@ -68,6 +68,17 @@ public class RoleAdapter extends AbstractMongoAdapter<MongoRoleEntity> 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;
|
||||
|
|
|
@ -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,12 +271,14 @@ 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;
|
||||
|
||||
if (client.isFullScopeAllowed()) {
|
||||
requestedRoles = roleMappings;
|
||||
} else {
|
||||
|
||||
Set<RoleModel> scopeMappings = client.getScopeMappings();
|
||||
scopeMappings.addAll(client.getRoles());
|
||||
|
@ -285,10 +289,44 @@ public class TokenManager {
|
|||
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();
|
||||
}
|
||||
|
||||
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());
|
||||
|
|
|
@ -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);
|
||||
|
@ -217,11 +223,13 @@ public class RealmManager implements RealmImporter {
|
|||
RoleModel createRealmRole = realm.addRole(AdminRoles.CREATE_REALM);
|
||||
adminRole.addCompositeRole(createRealmRole);
|
||||
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/>
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
|
||||
public static Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
|
||||
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
|
||||
Set<ClientModel> 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<OfflineClientSessionModel> 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<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
|
||||
|
||||
for (OfflineClientSessionModel clientSession : clientSessions) {
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
],
|
||||
"realmRoles": ["user"],
|
||||
"realmRoles": ["user", "offline_access"],
|
||||
"clientRoles": {
|
||||
"test-app": [ "customer-user" ],
|
||||
"account": [ "view-profile", "manage-account" ]
|
||||
|
|
Loading…
Reference in a new issue