diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java index 40b530181e..65b94c65d9 100755 --- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -53,6 +53,7 @@ public class IdentityProviderRepresentation { protected boolean storeToken; protected boolean addReadTokenRoleOnCreate; protected boolean authenticateByDefault; + protected boolean linkOnly; protected String firstBrokerLoginFlowAlias; protected String postBrokerLoginFlowAlias; protected Map config = new HashMap(); @@ -97,6 +98,14 @@ public class IdentityProviderRepresentation { this.enabled = enabled; } + public boolean isLinkOnly() { + return linkOnly; + } + + public void setLinkOnly(boolean linkOnly) { + this.linkOnly = linkOnly; + } + /** * * Deprecated because replaced by {@link #updateProfileFirstLoginMode}. Kept here to allow import of old realms. diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 1a1c1e9253..90fa8d7ba5 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1008,6 +1008,7 @@ public class RealmAdapter implements RealmModel, JpaModel { copy.putAll(config); identityProviderModel.setConfig(copy); identityProviderModel.setEnabled(entity.isEnabled()); + identityProviderModel.setLinkOnly(entity.isLinkOnly()); identityProviderModel.setTrustEmail(entity.isTrustEmail()); identityProviderModel.setAuthenticateByDefault(entity.isAuthenticateByDefault()); identityProviderModel.setFirstBrokerLoginFlowId(entity.getFirstBrokerLoginFlowId()); @@ -1044,6 +1045,7 @@ public class RealmAdapter implements RealmModel, JpaModel { entity.setFirstBrokerLoginFlowId(identityProvider.getFirstBrokerLoginFlowId()); entity.setPostBrokerLoginFlowId(identityProvider.getPostBrokerLoginFlowId()); entity.setConfig(identityProvider.getConfig()); + entity.setLinkOnly(identityProvider.isLinkOnly()); realm.addIdentityProvider(entity); @@ -1098,6 +1100,7 @@ public class RealmAdapter implements RealmModel, JpaModel { entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate()); entity.setStoreToken(identityProvider.isStoreToken()); entity.setConfig(identityProvider.getConfig()); + entity.setLinkOnly(identityProvider.isLinkOnly()); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java index d8a9d7eb78..e63b7f25e0 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java @@ -70,6 +70,9 @@ public class IdentityProviderEntity { @Column(name="STORE_TOKEN") private boolean storeToken; + @Column(name="LINK_ONLY") + private boolean linkOnly; + @Column(name="ADD_TOKEN_ROLE") protected boolean addReadTokenRoleOnCreate; @@ -144,6 +147,14 @@ public class IdentityProviderEntity { this.authenticateByDefault = authenticateByDefault; } + public boolean isLinkOnly() { + return linkOnly; + } + + public void setLinkOnly(boolean linkOnly) { + this.linkOnly = linkOnly; + } + public String getFirstBrokerLoginFlowId() { return firstBrokerLoginFlowId; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.0.0.xml new file mode 100755 index 0000000000..5ac745cd71 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.0.0.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 0a26548a30..59855ec614 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -46,4 +46,5 @@ + diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java index d4949d5011..83cdd84e5e 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -32,6 +32,7 @@ import org.keycloak.migration.migrators.MigrateTo2_1_0; import org.keycloak.migration.migrators.MigrateTo2_2_0; import org.keycloak.migration.migrators.MigrateTo2_3_0; import org.keycloak.migration.migrators.MigrateTo2_5_0; +import org.keycloak.migration.migrators.MigrateTo3_0_0; import org.keycloak.migration.migrators.Migration; import org.keycloak.models.KeycloakSession; @@ -56,7 +57,8 @@ public class MigrationModelManager { new MigrateTo2_1_0(), new MigrateTo2_2_0(), new MigrateTo2_3_0(), - new MigrateTo2_5_0() + new MigrateTo2_5_0(), + new MigrateTo3_0_0() }; public static void migrate(KeycloakSession session) { diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java new file mode 100644 index 0000000000..db068fa61a --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.migration.migrators; + + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.utils.DefaultKeyProviders; + +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; +import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; + +/** + * @author Marek Posolda + */ +public class MigrateTo3_0_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("2.5.0"); + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealms().stream().forEach( + r -> DefaultKeyProviders.createSecretProvider(r) + ); + + for (RealmModel realm : session.realms().getRealms()) { + ClientModel client = realm.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID); + if (client == null) continue; + RoleModel linkRole = client.getRole(MANAGE_ACCOUNT_LINKS); + if (linkRole == null) { + client.addRole(MANAGE_ACCOUNT_LINKS); + } + RoleModel manageAccount = client.getRole(AccountRoles.MANAGE_ACCOUNT); + if (manageAccount == null) continue; + RoleModel manageAccountLinks = client.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS); + manageAccount.addCompositeRole(manageAccountLinks); + + } + + } + + @Override + public ModelVersion getVersion() { + return VERSION; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java index 7c21292e1d..e8e3509f9d 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java +++ b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java @@ -24,6 +24,7 @@ public interface AccountRoles { String VIEW_PROFILE = "view-profile"; String MANAGE_ACCOUNT = "manage-account"; + String MANAGE_ACCOUNT_LINKS = "manage-account-links"; String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT}; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 713025597f..3090c6e13f 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -575,6 +575,7 @@ public class ModelToRepresentation { providerRep.setAlias(identityProviderModel.getAlias()); providerRep.setDisplayName(identityProviderModel.getDisplayName()); providerRep.setEnabled(identityProviderModel.isEnabled()); + providerRep.setLinkOnly(identityProviderModel.isLinkOnly()); providerRep.setStoreToken(identityProviderModel.isStoreToken()); providerRep.setTrustEmail(identityProviderModel.isTrustEmail()); providerRep.setAuthenticateByDefault(identityProviderModel.isAuthenticateByDefault()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 2fc5136414..2557bfd085 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -118,6 +118,7 @@ import java.util.stream.Collectors; public class RepresentationToModel { private static Logger logger = Logger.getLogger(RepresentationToModel.class); + public static OTPPolicy toPolicy(RealmRepresentation rep) { OTPPolicy policy = new OTPPolicy(); if (rep.getOtpPolicyType() != null) policy.setType(rep.getOtpPolicyType()); @@ -129,6 +130,7 @@ public class RepresentationToModel { return policy; } + public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm, boolean skipUserDependent) { convertDeprecatedSocialProviders(rep); convertDeprecatedApplications(session, rep); @@ -139,16 +141,19 @@ public class RepresentationToModel { if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled()); if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected()); if (rep.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds()); - if (rep.getMinimumQuickLoginWaitSeconds() != null) newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds()); + if (rep.getMinimumQuickLoginWaitSeconds() != null) + newRealm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds()); if (rep.getWaitIncrementSeconds() != null) newRealm.setWaitIncrementSeconds(rep.getWaitIncrementSeconds()); - if (rep.getQuickLoginCheckMilliSeconds() != null) newRealm.setQuickLoginCheckMilliSeconds(rep.getQuickLoginCheckMilliSeconds()); + if (rep.getQuickLoginCheckMilliSeconds() != null) + newRealm.setQuickLoginCheckMilliSeconds(rep.getQuickLoginCheckMilliSeconds()); if (rep.getMaxDeltaTimeSeconds() != null) newRealm.setMaxDeltaTimeSeconds(rep.getMaxDeltaTimeSeconds()); if (rep.getFailureFactor() != null) newRealm.setFailureFactor(rep.getFailureFactor()); if (rep.isEventsEnabled() != null) newRealm.setEventsEnabled(rep.isEventsEnabled()); if (rep.getEventsExpiration() != null) newRealm.setEventsExpiration(rep.getEventsExpiration()); if (rep.getEventsListeners() != null) newRealm.setEventsListeners(new HashSet<>(rep.getEventsListeners())); if (rep.isAdminEventsEnabled() != null) newRealm.setAdminEventsEnabled(rep.isAdminEventsEnabled()); - if (rep.isAdminEventsDetailsEnabled() != null) newRealm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); + if (rep.isAdminEventsDetailsEnabled() != null) + newRealm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore()); @@ -158,14 +163,17 @@ public class RepresentationToModel { if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); else newRealm.setAccessTokenLifespan(300); - if (rep.getAccessTokenLifespanForImplicitFlow() != null) newRealm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow()); - else newRealm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT); + if (rep.getAccessTokenLifespanForImplicitFlow() != null) + newRealm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow()); + else + newRealm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT); if (rep.getSsoSessionIdleTimeout() != null) newRealm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout()); else newRealm.setSsoSessionIdleTimeout(1800); if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan()); else newRealm.setSsoSessionMaxLifespan(36000); - if (rep.getOfflineSessionIdleTimeout() != null) newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); + if (rep.getOfflineSessionIdleTimeout() != null) + newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); @@ -179,7 +187,8 @@ public class RepresentationToModel { newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); else newRealm.setAccessCodeLifespanLogin(1800); - if (rep.getSslRequired() != null) newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); + if (rep.getSslRequired() != null) + newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); if (rep.isRegistrationEmailAsUsername() != null) newRealm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); @@ -203,7 +212,8 @@ public class RepresentationToModel { newRealm.addRequiredCredential(CredentialRepresentation.PASSWORD); } - if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy())); + if (rep.getPasswordPolicy() != null) + newRealm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy())); if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep)); else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); @@ -328,16 +338,16 @@ public class RepresentationToModel { importRealmAuthorizationSettings(rep, newRealm, session); } - if(rep.isInternationalizationEnabled() != null){ + if (rep.isInternationalizationEnabled() != null) { newRealm.setInternationalizationEnabled(rep.isInternationalizationEnabled()); } - if(rep.getSupportedLocales() != null){ + if (rep.getSupportedLocales() != null) { newRealm.setSupportedLocales(new HashSet(rep.getSupportedLocales())); } - if(rep.getDefaultLocale() != null){ + if (rep.getDefaultLocale() != null) { newRealm.setDefaultLocale(rep.getDefaultLocale()); } - + // import attributes if (rep.getAttributes() != null) { @@ -434,9 +444,9 @@ public class RepresentationToModel { } for (RoleRepresentation roleRep : entry.getValue()) { // 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()); + 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(); + boolean scopeParamRequired = roleRep.isScopeParamRequired() == null ? false : roleRep.isScopeParamRequired(); role.setScopeParamRequired(scopeParamRequired); } } @@ -612,6 +622,7 @@ public class RepresentationToModel { identityProvider.setAlias(providerId); identityProvider.setProviderId(providerId); identityProvider.setEnabled(true); + identityProvider.setLinkOnly(false); identityProvider.setUpdateProfileFirstLogin(updateProfileFirstLogin); Map config = new HashMap<>(); @@ -776,13 +787,16 @@ public class RepresentationToModel { if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled()); if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected()); if (rep.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds()); - if (rep.getMinimumQuickLoginWaitSeconds() != null) realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds()); + if (rep.getMinimumQuickLoginWaitSeconds() != null) + realm.setMinimumQuickLoginWaitSeconds(rep.getMinimumQuickLoginWaitSeconds()); if (rep.getWaitIncrementSeconds() != null) realm.setWaitIncrementSeconds(rep.getWaitIncrementSeconds()); - if (rep.getQuickLoginCheckMilliSeconds() != null) realm.setQuickLoginCheckMilliSeconds(rep.getQuickLoginCheckMilliSeconds()); + if (rep.getQuickLoginCheckMilliSeconds() != null) + realm.setQuickLoginCheckMilliSeconds(rep.getQuickLoginCheckMilliSeconds()); if (rep.getMaxDeltaTimeSeconds() != null) realm.setMaxDeltaTimeSeconds(rep.getMaxDeltaTimeSeconds()); if (rep.getFailureFactor() != null) realm.setFailureFactor(rep.getFailureFactor()); if (rep.isRegistrationAllowed() != null) realm.setRegistrationAllowed(rep.isRegistrationAllowed()); - if (rep.isRegistrationEmailAsUsername() != null) realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); + if (rep.isRegistrationEmailAsUsername() != null) + realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe()); if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isLoginWithEmailAllowed() != null) realm.setLoginWithEmailAllowed(rep.isLoginWithEmailAllowed()); @@ -791,15 +805,19 @@ public class RepresentationToModel { if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed()); if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); - if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); - if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); + if (rep.getAccessCodeLifespanUserAction() != null) + realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); + if (rep.getAccessCodeLifespanLogin() != null) + realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore()); if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken()); if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); - if (rep.getAccessTokenLifespanForImplicitFlow() != null) realm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow()); + if (rep.getAccessTokenLifespanForImplicitFlow() != null) + realm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow()); if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout()); if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan()); - if (rep.getOfflineSessionIdleTimeout() != null) realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); + if (rep.getOfflineSessionIdleTimeout() != null) + realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); if (rep.getRequiredCredentials() != null) { realm.updateRequiredCredentials(rep.getRequiredCredentials()); } @@ -814,10 +832,12 @@ public class RepresentationToModel { if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes())); if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled()); - if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); + if (rep.isAdminEventsDetailsEnabled() != null) + realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled()); - if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy())); + if (rep.getPasswordPolicy() != null) + realm.setPasswordPolicy(PasswordPolicy.parse(session, rep.getPasswordPolicy())); if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep)); if (rep.getDefaultRoles() != null) { @@ -837,13 +857,13 @@ public class RepresentationToModel { realm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders()); } - if(rep.isInternationalizationEnabled() != null){ + if (rep.isInternationalizationEnabled() != null) { realm.setInternationalizationEnabled(rep.isInternationalizationEnabled()); } - if(rep.getSupportedLocales() != null){ + if (rep.getSupportedLocales() != null) { realm.setSupportedLocales(new HashSet(rep.getSupportedLocales())); } - if(rep.getDefaultLocale() != null){ + if (rep.getDefaultLocale() != null) { realm.setDefaultLocale(rep.getDefaultLocale()); } if (rep.getBrowserFlow() != null) { @@ -904,7 +924,7 @@ public class RepresentationToModel { // Roles public static void createRole(RealmModel newRealm, RoleRepresentation roleRep) { - RoleModel role = roleRep.getId()!=null ? newRealm.addRole(roleRep.getId(), roleRep.getName()) : newRealm.addRole(roleRep.getName()); + 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); @@ -927,7 +947,8 @@ public class RepresentationToModel { } for (String roleStr : entry.getValue()) { RoleModel clientRole = client.getRole(roleStr); - if (clientRole == null) throw new RuntimeException("Unable to find composite client role: " + roleStr); + if (clientRole == null) + throw new RuntimeException("Unable to find composite client role: " + roleStr); role.addCompositeRole(clientRole); } } @@ -957,9 +978,9 @@ public class RepresentationToModel { public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation resourceRep, boolean addDefaultRoles) { logger.debug("Create client: {0}" + resourceRep.getClientId()); - ClientModel client = resourceRep.getId()!=null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId()); + ClientModel client = resourceRep.getId() != null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId()); if (resourceRep.getName() != null) client.setName(resourceRep.getName()); - if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); + if (resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled()); client.setManagementUrl(resourceRep.getAdminUrl()); if (resourceRep.isSurrogateAuthRequired() != null) @@ -976,13 +997,18 @@ public class RepresentationToModel { client.setDirectAccessGrantsEnabled(resourceRep.isDirectGrantsOnly()); } - if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled()); - if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled()); - if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled()); - if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); + if (resourceRep.isStandardFlowEnabled() != null) + client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled()); + if (resourceRep.isImplicitFlowEnabled() != null) + client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled()); + if (resourceRep.isDirectAccessGrantsEnabled() != null) + client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled()); + if (resourceRep.isServiceAccountsEnabled() != null) + client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient()); - if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); + if (resourceRep.isFrontchannelLogout() != null) + client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); if (resourceRep.getNodeReRegistrationTimeout() != null) { client.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout()); @@ -1030,7 +1056,7 @@ public class RepresentationToModel { logger.debugv("add redirect-uri to origin: {0}", redirectUri); if (redirectUri.startsWith("http")) { String origin = UriUtils.getOrigin(redirectUri); - logger.debugv("adding default client origin: {0}" , origin); + logger.debugv("adding default client origin: {0}", origin); origins.add(origin); } } @@ -1051,7 +1077,6 @@ public class RepresentationToModel { } - if (resourceRep.getProtocolMappers() != null) { // first, remove all default/built in mappers Set mappers = client.getProtocolMappers(); @@ -1091,7 +1116,8 @@ public class RepresentationToModel { if (resourceRep.isUseTemplateScope() != null) client.setUseTemplateScope(resourceRep.isUseTemplateScope()); else client.setUseTemplateScope(resourceRep.getClientTemplate() != null); - if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers()); + if (resourceRep.isUseTemplateMappers() != null) + client.setUseTemplateMappers(resourceRep.isUseTemplateMappers()); else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null); client.updateClient(); @@ -1108,7 +1134,8 @@ public class RepresentationToModel { if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired()); if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled()); if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled()); - if (rep.isDirectAccessGrantsEnabled() != null) resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled()); + if (rep.isDirectAccessGrantsEnabled() != null) + resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled()); if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled()); if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient()); if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed()); @@ -1117,8 +1144,10 @@ public class RepresentationToModel { if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl()); if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl()); if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); - if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout()); - if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType()); + if (rep.getNodeReRegistrationTimeout() != null) + resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout()); + if (rep.getClientAuthenticatorType() != null) + resource.setClientAuthenticatorType(rep.getClientAuthenticatorType()); if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); if (rep.getAttributes() != null) { @@ -1191,9 +1220,9 @@ public class RepresentationToModel { public static ClientTemplateModel createClientTemplate(KeycloakSession session, RealmModel realm, ClientTemplateRepresentation resourceRep) { logger.debug("Create client template: {0}" + resourceRep.getName()); - ClientTemplateModel client = resourceRep.getId()!=null ? realm.addClientTemplate(resourceRep.getId(), resourceRep.getName()) : realm.addClientTemplate(resourceRep.getName()); + ClientTemplateModel client = resourceRep.getId() != null ? realm.addClientTemplate(resourceRep.getId(), resourceRep.getName()) : realm.addClientTemplate(resourceRep.getName()); if (resourceRep.getName() != null) client.setName(resourceRep.getName()); - if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); + if (resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); if (resourceRep.isFullScopeAllowed() != null) client.setFullScopeAllowed(resourceRep.isFullScopeAllowed()); if (resourceRep.getProtocolMappers() != null) { @@ -1208,13 +1237,18 @@ public class RepresentationToModel { if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired()); - if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled()); - if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled()); - if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled()); - if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); + if (resourceRep.isStandardFlowEnabled() != null) + client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled()); + if (resourceRep.isImplicitFlowEnabled() != null) + client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled()); + if (resourceRep.isDirectAccessGrantsEnabled() != null) + client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled()); + if (resourceRep.isServiceAccountsEnabled() != null) + client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient()); - if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); + if (resourceRep.isFrontchannelLogout() != null) + client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); if (resourceRep.getAttributes() != null) { for (Map.Entry entry : resourceRep.getAttributes().entrySet()) { @@ -1240,7 +1274,8 @@ public class RepresentationToModel { if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired()); if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled()); if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled()); - if (rep.isDirectAccessGrantsEnabled() != null) resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled()); + if (rep.isDirectAccessGrantsEnabled() != null) + resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled()); if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled()); if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient()); if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed()); @@ -1391,7 +1426,8 @@ public class RepresentationToModel { if (client == null) { throw new RuntimeException("Unable to find client specified for service account link. Client: " + clientId); } - user.setServiceAccountClientLink(client.getId());; + user.setServiceAccountClientLink(client.getId()); + ; } if (userRep.getGroups() != null) { for (String path : userRep.getGroups()) { @@ -1406,7 +1442,7 @@ public class RepresentationToModel { return user; } - public static void createCredentials(UserRepresentation userRep, KeycloakSession session, RealmModel realm,UserModel user) { + public static void createCredentials(UserRepresentation userRep, KeycloakSession session, RealmModel realm, UserModel user) { if (userRep.getCredentials() != null) { for (CredentialRepresentation cred : userRep.getCredentials()) { updateCredential(session, realm, user, cred); @@ -1541,6 +1577,7 @@ public class RepresentationToModel { } } } + private static void importIdentityProviderMappers(RealmRepresentation rep, RealmModel newRealm) { if (rep.getIdentityProviderMappers() != null) { for (IdentityProviderMapperRepresentation representation : rep.getIdentityProviderMappers()) { @@ -1548,7 +1585,8 @@ public class RepresentationToModel { } } } - public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRepresentation representation) { + + public static IdentityProviderModel toModel(RealmModel realm, IdentityProviderRepresentation representation) { IdentityProviderModel identityProviderModel = new IdentityProviderModel(); identityProviderModel.setInternalId(representation.getInternalId()); @@ -1556,6 +1594,7 @@ public class RepresentationToModel { identityProviderModel.setDisplayName(representation.getDisplayName()); identityProviderModel.setProviderId(representation.getProviderId()); identityProviderModel.setEnabled(representation.isEnabled()); + identityProviderModel.setLinkOnly(representation.isLinkOnly()); identityProviderModel.setTrustEmail(representation.isTrustEmail()); identityProviderModel.setAuthenticateByDefault(representation.isAuthenticateByDefault()); identityProviderModel.setStoreToken(representation.isStoreToken()); @@ -1567,24 +1606,24 @@ public class RepresentationToModel { flowAlias = DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW; } - AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias); - if (flowModel == null) { - throw new ModelException("No available authentication flow with alias: " + flowAlias); - } - identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId()); + AuthenticationFlowModel flowModel = realm.getFlowByAlias(flowAlias); + if (flowModel == null) { + throw new ModelException("No available authentication flow with alias: " + flowAlias); + } + identityProviderModel.setFirstBrokerLoginFlowId(flowModel.getId()); - flowAlias = representation.getPostBrokerLoginFlowAlias(); - if (flowAlias == null || flowAlias.trim().length() == 0) { - identityProviderModel.setPostBrokerLoginFlowId(null); - } else { - flowModel = realm.getFlowByAlias(flowAlias); - if (flowModel == null) { - throw new ModelException("No available authentication flow with alias: " + flowAlias); - } - identityProviderModel.setPostBrokerLoginFlowId(flowModel.getId()); - } + flowAlias = representation.getPostBrokerLoginFlowAlias(); + if (flowAlias == null || flowAlias.trim().length() == 0) { + identityProviderModel.setPostBrokerLoginFlowId(null); + } else { + flowModel = realm.getFlowByAlias(flowAlias); + if (flowModel == null) { + throw new ModelException("No available authentication flow with alias: " + flowAlias); + } + identityProviderModel.setPostBrokerLoginFlowId(flowModel.getId()); + } - return identityProviderModel; + return identityProviderModel; } public static ProtocolMapperModel toModel(ProtocolMapperRepresentation rep) { @@ -1906,7 +1945,7 @@ public class RepresentationToModel { if (roles != null && !roles.isEmpty()) { try { - List rolesMap = (List)JsonSerialization.readValue(roles, List.class); + List rolesMap = (List) JsonSerialization.readValue(roles, List.class); config.put("roles", JsonSerialization.writeValueAsString(rolesMap.stream().map(roleConfig -> { String roleName = roleConfig.get("id").toString(); String clientId = null; @@ -2210,7 +2249,8 @@ public class RepresentationToModel { } } if (!hasPolicy) { - policy.removeAssociatedPolicy(policyModel);; + policy.removeAssociatedPolicy(policyModel); + ; } } @@ -2284,7 +2324,7 @@ public class RepresentationToModel { existing.setIconUri(resource.getIconUri()); existing.updateScopes(resource.getScopes().stream() - .map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization)) + .map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization)) .collect(Collectors.toSet())); return existing; } @@ -2353,7 +2393,7 @@ public class RepresentationToModel { } } if (userRep.getRequiredActions() != null) { - for (String action: userRep.getRequiredActions()) { + for (String action : userRep.getRequiredActions()) { federatedStorage.addRequiredAction(newRealm, userRep.getId(), action); } } diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java index 083ec42aff..9aec8ffe5e 100755 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -48,6 +48,9 @@ public class IdentityProviderModel implements Serializable { private boolean storeToken; protected boolean addReadTokenRoleOnCreate; + + protected boolean linkOnly; + /** * Specifies if particular provider should be used by default for authentication even before displaying login screen */ @@ -78,6 +81,7 @@ public class IdentityProviderModel implements Serializable { this.enabled = model.isEnabled(); this.trustEmail = model.isTrustEmail(); this.storeToken = model.isStoreToken(); + this.linkOnly = model.isLinkOnly(); this.authenticateByDefault = model.isAuthenticateByDefault(); this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate; this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId(); @@ -125,6 +129,14 @@ public class IdentityProviderModel implements Serializable { this.storeToken = storeToken; } + public boolean isLinkOnly() { + return linkOnly; + } + + public void setLinkOnly(boolean linkOnly) { + this.linkOnly = linkOnly; + } + @Deprecated public boolean isAuthenticateByDefault() { return authenticateByDefault; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java index f682a3f08c..696e19b5fc 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java @@ -49,7 +49,7 @@ public class IdentityProviderBean { if (!identityProviders.isEmpty()) { Set orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE); for (IdentityProviderModel identityProvider : identityProviders) { - if (identityProvider.isEnabled()) { + if (identityProvider.isEnabled() && !identityProvider.isLinkOnly()) { addIdentityProvider(orderedSet, realm, baseURI, identityProvider); } } diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index f3beb54055..a23a26a0e2 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -79,6 +79,14 @@ public class Urls { return uriBuilder.build(realmName, providerId); } + public static URI identityProviderLinkRequest(URI baseUri, String providerId, String realmName) { + UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService") + .replaceQuery(null) + .path(IdentityBrokerService.class, "clientInitiatedAccountLinking"); + + return uriBuilder.build(realmName, providerId); + } + public static URI identityProviderRetrieveToken(URI baseUri, String providerId, String realmName) { return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") .path(IdentityBrokerService.class, "retrieveToken") 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 cb80590b3b..0921c6099d 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -396,6 +396,11 @@ public class RealmManager { roleModel.setDescription("${role_" + role + "}"); roleModel.setScopeParamRequired(false); } + RoleModel manageAccountLinks = client.addRole(AccountRoles.MANAGE_ACCOUNT_LINKS); + manageAccountLinks.setDescription("${role_" + AccountRoles.MANAGE_ACCOUNT_LINKS + "}"); + manageAccountLinks.setScopeParamRequired(false); + RoleModel manageAccount = client.getRole(AccountRoles.MANAGE_ACCOUNT); + manageAccount.addCompositeRole(manageAccountLinks); } } 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 a3c3fcbf67..e0e5f8b55d 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -17,6 +17,7 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.UriUtils; import org.keycloak.credential.CredentialModel; import org.keycloak.events.Details; @@ -70,6 +71,9 @@ import javax.ws.rs.core.Variant; import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -771,14 +775,19 @@ public class AccountService extends AbstractSecuredLocalService { String redirectUri = UriBuilder.fromUri(Urls.accountFederatedIdentityPage(uriInfo.getBaseUri(), realm.getName())).build().toString(); try { - ClientSessionModel clientSession = auth.getClientSession(); - ClientSessionCode clientSessionCode = new ClientSessionCode(session, realm, clientSession); - clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setRedirectUri(redirectUri); - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString()); - - return Response.seeOther( - Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, realm.getName(), clientSessionCode.getCode())) + String nonce = UUID.randomUUID().toString(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + String input = nonce + auth.getSession().getId() + auth.getClientSession().getId() + providerId; + byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); + String hash = Base64Url.encode(check); + URI linkUrl = Urls.identityProviderLinkRequest(this.uriInfo.getBaseUri(), providerId, realm.getName()); + linkUrl = UriBuilder.fromUri(linkUrl) + .queryParam("nonce", nonce) + .queryParam("hash", hash) + .queryParam("client_id", client.getClientId()) + .queryParam("redirect_uri", redirectUri) + .build(); + return Response.seeOther(linkUrl) .build(); } catch (Exception spe) { setReferrerOnPage(); diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 3b4c01767a..c950ebe0ff 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -107,6 +107,7 @@ import java.util.Set; import java.util.UUID; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE; import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; @@ -230,10 +231,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } AuthResult cookieResult = authenticationManager.authenticateIdentityCookie(session, realmModel, true); + String errorParam = "link_error"; if (cookieResult == null) { event.error(Errors.NOT_LOGGED_IN); UriBuilder builder = UriBuilder.fromUri(redirectUri) - .queryParam("error", Errors.NOT_LOGGED_IN) + .queryParam(errorParam, Errors.NOT_LOGGED_IN) .queryParam("nonce", nonce); return Response.status(302).location(builder.build()).build(); @@ -264,11 +266,30 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal throw new ErrorPageException(session, Messages.INVALID_REQUEST); } + + + ClientModel accountService = this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID); + if (!accountService.getId().equals(client.getId())) { + RoleModel manageAccountRole = accountService.getRole(MANAGE_ACCOUNT); + + if (!clientSession.getRoles().contains(manageAccountRole.getId())) { + RoleModel linkRole = accountService.getRole(MANAGE_ACCOUNT_LINKS); + if (!clientSession.getRoles().contains(linkRole.getId())) { + event.error(Errors.NOT_ALLOWED); + UriBuilder builder = UriBuilder.fromUri(redirectUri) + .queryParam(errorParam, Errors.NOT_ALLOWED) + .queryParam("nonce", nonce); + return Response.status(302).location(builder.build()).build(); + } + } + } + + IdentityProviderModel identityProviderModel = realmModel.getIdentityProviderByAlias(providerId); if (identityProviderModel == null) { event.error(Errors.UNKNOWN_IDENTITY_PROVIDER); UriBuilder builder = UriBuilder.fromUri(redirectUri) - .queryParam("error", Errors.UNKNOWN_IDENTITY_PROVIDER) + .queryParam(errorParam, Errors.UNKNOWN_IDENTITY_PROVIDER) .queryParam("nonce", nonce); return Response.status(302).location(builder.build()).build(); @@ -329,7 +350,18 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } ClientSessionCode clientSessionCode = parsedCode.clientSessionCode; - IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); + IdentityProviderModel identityProviderModel = realmModel.getIdentityProviderByAlias(providerId); + if (identityProviderModel == null) { + throw new IdentityBrokerException("Identity Provider [" + providerId + "] not found."); + } + if (identityProviderModel.isLinkOnly()) { + throw new IdentityBrokerException("Identity Provider [" + providerId + "] is not allowed to perform a login."); + + } + IdentityProviderFactory providerFactory = getIdentityProviderFactory(session, identityProviderModel); + + IdentityProvider identityProvider = providerFactory.create(session, identityProviderModel); + Response response = identityProvider.performLogin(createAuthenticationRequest(providerId, clientSessionCode)); if (response != null) { @@ -786,23 +818,28 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel federatedIdentityModel, UserModel federatedUser) { this.event.event(EventType.FEDERATED_IDENTITY_LINK); + UserModel authenticatedUser = clientSession.getUserSession().getUser(); + if (federatedUser != null) { - // refresh the token - if (context.getIdpConfig().isStoreToken()) { - federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel); - if (!ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) { + if (authenticatedUser.getId().equals(federatedUser.getId())) { + // refresh the token + if (context.getIdpConfig().isStoreToken()) { + federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel); + if (!ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) { - this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel); + this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel); - if (isDebugEnabled()) { - logger.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, context.getIdpConfig().getAlias()); + if (isDebugEnabled()) { + logger.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, context.getIdpConfig().getAlias()); + } } } + return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); + } else { + return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); } - return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); } - UserModel authenticatedUser = clientSession.getUserSession().getUser(); if (isDebugEnabled()) { logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", federatedIdentityModel, context.getIdpConfig().getAlias(), authenticatedUser); diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java index 43095662c3..d0678cf1a4 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java @@ -39,6 +39,7 @@ import java.util.UUID; public class ClientInitiatedAccountLinkServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException { + resp.setHeader("Cache-Control", "no-cache"); if (request.getRequestURI().endsWith("/link") && request.getParameter("response") == null) { String provider = request.getParameter("provider"); String realm = request.getParameter("realm"); @@ -68,13 +69,11 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet { resp.setStatus(302); resp.setHeader("Location", accountLinkUrl); } else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) { - String hash = request.getSession().getAttribute("hash").toString(); - String hashParam = request.getParameter("hash"); resp.setStatus(200); resp.setContentType("text/html"); PrintWriter pw = resp.getWriter(); pw.printf("%s", "Client Linking"); - String error = request.getParameter("error"); + String error = request.getParameter("link_error"); if (error != null) { pw.println("Link error: " + error); } else { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index db5c4edd9c..cd3fdb85db 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -877,7 +877,8 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertThat(apps.keySet(), containsInAnyOrder("Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}")); AccountApplicationsPage.AppEntry accountEntry = apps.get("Account"); - Assert.assertEquals(2, accountEntry.getRolesAvailable().size()); + Assert.assertEquals(3, accountEntry.getRolesAvailable().size()); + Assert.assertTrue(accountEntry.getRolesAvailable().contains("Manage account links in Account")); Assert.assertTrue(accountEntry.getRolesAvailable().contains("Manage account in Account")); Assert.assertTrue(accountEntry.getRolesAvailable().contains("View profile in Account")); Assert.assertEquals(1, accountEntry.getRolesGranted().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index 35892914f4..1d15f395e8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -414,7 +414,8 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE); - Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT); + + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS); Assert.assertNames(scopesResource.getAll().getRealmMappings(), "role1"); Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE); @@ -429,7 +430,7 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.realmLevel().listEffective()); Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2"); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll()); - Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT); + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java index c5b7b315af..40c1f704fa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java @@ -38,7 +38,7 @@ public class BrokerTestTools { IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation(); identityProviderRepresentation.setAlias(alias); - identityProviderRepresentation.setDisplayName(providerId); + identityProviderRepresentation.setDisplayName(alias); identityProviderRepresentation.setProviderId(providerId); identityProviderRepresentation.setEnabled(true); @@ -84,7 +84,16 @@ public class BrokerTestTools { * @param suiteContext */ public static void createKcOidcBroker(Keycloak adminClient, String childRealm, String idpRealm, SuiteContext suiteContext) { - IdentityProviderRepresentation idp = createIdentityProvider(idpRealm, IDP_OIDC_PROVIDER_ID); + createKcOidcBroker(adminClient, childRealm, idpRealm, suiteContext, idpRealm, false); + + + + } + + public static void createKcOidcBroker(Keycloak adminClient, String childRealm, String idpRealm, SuiteContext suiteContext, String alias, boolean linkOnly) { + IdentityProviderRepresentation idp = createIdentityProvider(alias, IDP_OIDC_PROVIDER_ID); + idp.setLinkOnly(linkOnly); + Map config = idp.getConfig(); config.put("clientId", childRealm); @@ -109,8 +118,5 @@ public class BrokerTestTools { client.setAdminUrl(getAuthRoot(suiteContext) + "/auth/realms/" + childRealm + "/broker/" + idpRealm + "/endpoint"); adminClient.realm(idpRealm).clients().create(client); - - - } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java index 6c08851c05..bd17ad071c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java @@ -25,11 +25,15 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.util.Base64Url; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -50,8 +54,13 @@ import java.net.URL; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.junit.Assert.assertTrue; +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; +import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; /** @@ -153,7 +162,7 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { user.setEnabled(true); childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password"); - // have to add a role as stupid undertow auth manager doesn't like "*" + // have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions realm.roles().create(new RoleRepresentation("user", null, false)); RoleRepresentation role = realm.roles().get("user").toRepresentation(); List roles = new LinkedList<>(); @@ -171,12 +180,21 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext); } + //@Test + public void testUi() throws Exception { + Thread.sleep(1000000000); + + } + @Test - public void testErrorConditions() { + public void testErrorConditions() throws Exception { + RealmResource realm = adminClient.realms().realm(CHILD_IDP); List links = realm.users().get(childUserId).getFederatedIdentity(); Assert.assertTrue(links.isEmpty()); + ClientRepresentation client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0); + UriBuilder redirectUri = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) .path("link") .queryParam("response", "true"); @@ -190,19 +208,24 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { String linkUrl = directLinking .build(PARENT_IDP).toString(); + // test not logged in - driver.navigate().to(linkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); - Assert.assertTrue(driver.getCurrentUrl().contains("error=not_logged_in")); + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in")); + logoutAll(); // now log in driver.navigate().to( appPage.getInjectedUrl() + "/hello"); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); loginPage.login("child", "password"); Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello")); + Assert.assertTrue(driver.getPageSource().contains("Unknown request:")); // now test CSRF with bad hash. @@ -210,12 +233,140 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { Assert.assertTrue(driver.getPageSource().contains("We're sorry...")); + logoutAll(); + + // now log in again with client that does not have scope + + String accountId = adminClient.realms().realm(CHILD_IDP).clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId(); + RoleRepresentation manageAccount = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation(); + RoleRepresentation manageLinks = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation(); + RoleRepresentation userRole = adminClient.realms().realm(CHILD_IDP).roles().get("user").toRepresentation(); + + client.setFullScopeAllowed(false); + ClientResource clientResource = adminClient.realms().realm(CHILD_IDP).clients().get(client.getId()); + clientResource.update(client); + + List roles = new LinkedList<>(); + roles.add(userRole); + clientResource.getScopeMappings().realmLevel().add(roles); + + driver.navigate().to( appPage.getInjectedUrl() + "/hello"); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello")); + Assert.assertTrue(driver.getPageSource().contains("Unknown request:")); + + + UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) + .path("link"); + String clientLinkUrl = linkBuilder.clone() + .queryParam("realm", CHILD_IDP) + .queryParam("provider", PARENT_IDP).build().toString(); + + + driver.navigate().to(clientLinkUrl); + + Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed")); + + logoutAll(); + + // add MANAGE_ACCOUNT_LINKS scope should pass. + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + + roles = new LinkedList<>(); + roles.add(manageLinks); + clientResource.getScopeMappings().clientLevel(accountId).add(roles); + + driver.navigate().to(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); + loginPage.login(PARENT_USERNAME, "password"); + + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account Linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + clientResource.getScopeMappings().clientLevel(accountId).remove(roles); + + logoutAll(); + + driver.navigate().to(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed")); + + logoutAll(); + + // add MANAGE_ACCOUNT scope should pass + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + + roles = new LinkedList<>(); + roles.add(manageAccount); + clientResource.getScopeMappings().clientLevel(accountId).add(roles); + + driver.navigate().to(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); + loginPage.login(PARENT_USERNAME, "password"); + + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account Linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + clientResource.getScopeMappings().clientLevel(accountId).remove(roles); + + logoutAll(); + + driver.navigate().to(clientLinkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + + Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed")); + + logoutAll(); + + + // undo fullScopeAllowed + + client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0); + client.setFullScopeAllowed(true); + clientResource.update(client); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + + + + } @Test - public void testAccountLink() { + public void testAccountLink() throws Exception { RealmResource realm = adminClient.realms().realm(CHILD_IDP); List links = realm.users().get(childUserId).getFederatedIdentity(); Assert.assertTrue(links.isEmpty()); @@ -227,6 +378,7 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { .queryParam("provider", PARENT_IDP).build().toString(); driver.navigate().to(linkUrl); Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP)); loginPage.login("child", "password"); Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); loginPage.login(PARENT_USERNAME, "password"); @@ -237,9 +389,117 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { links = realm.users().get(childUserId).getFederatedIdentity(); Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + + + } + + public void logoutAll() { + String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString(); + driver.navigate().to(logoutUri); + logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString(); + driver.navigate().to(logoutUri); + } + + @Test + public void testLinkOnlyProvider() throws Exception { + RealmResource realm = adminClient.realms().realm(CHILD_IDP); + IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_IDP).toRepresentation(); + rep.setLinkOnly(true); + realm.identityProviders().get(PARENT_IDP).update(rep); + try { + + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) + .path("link"); + String linkUrl = linkBuilder.clone() + .queryParam("realm", CHILD_IDP) + .queryParam("provider", PARENT_IDP).build().toString(); + driver.navigate().to(linkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + + // should not be on login page. This is what we are testing + Assert.assertFalse(driver.getPageSource().contains(PARENT_IDP)); + + // now test that we can still link. + loginPage.login("child", "password"); + Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); + loginPage.login(PARENT_USERNAME, "password"); + System.out.println("After linking: " + driver.getCurrentUrl()); + System.out.println(driver.getPageSource()); + Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); + Assert.assertTrue(driver.getPageSource().contains("Account Linked")); + + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertFalse(links.isEmpty()); + + realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + logoutAll(); + + System.out.println("testing link-only attack"); + + driver.navigate().to(linkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + + System.out.println("login page uri is: " + driver.getCurrentUrl()); + + // ok, now scrape the code from page + String pageSource = driver.getPageSource(); + Pattern p = Pattern.compile("action=\"(.+)\""); + Matcher m = p.matcher(pageSource); + String action = null; + if (m.find()) { + action = m.group(1); + + } + System.out.println("action: " + action); + + p = Pattern.compile("code=(.+)&"); + m = p.matcher(action); + String code = null; + if (m.find()) { + code = m.group(1); + + } + System.out.println("code: " + code); + + // now try and use the code to login to remote link-only idp + + String uri = "/auth/realms/child/broker/parent-idp/login"; + + uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot()) + .path(uri) + .queryParam("code", code) + .build().toString(); + + System.out.println("hack uri: " + uri); + + driver.navigate().to(uri); + + Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider.")); + + + + + + } finally { + + rep.setLinkOnly(false); + realm.identityProviders().get(PARENT_IDP).update(rep); + } + + } - - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json index c447d58ed0..64c315749b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json @@ -3,6 +3,7 @@ "resource" : "client-linking", "auth-server-url" : "http://localhost:8180/auth", "ssl-required" : "external", + "min-time-between-jwks-requests" : 0, "credentials" : { "secret": "password" } diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 1edd186105..ea6253f5bf 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -57,7 +57,6 @@ tomcat8 jetty integration-arquillian - stress diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 90b10def8b..6e72cd1e56 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -51,6 +51,7 @@ role_manage-clients=Manage clients role_manage-events=Manage events role_view-profile=View profile role_manage-account=Manage account +role_manage-account-links=Manage account links role_read-token=Read token role_offline-access=Offline access role_uma_authorization=Obtain permissions diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 2ff5f9679e..05f7f0411a 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -3,6 +3,7 @@ consoleTitle=Keycloak Admin Console # Common messages enabled=Enabled hidden=Hidden +link-only-column=Link only name=Name displayName=Display name displayNameHtml=HTML Display name @@ -467,6 +468,8 @@ off=Off update-profile-on-first-login.tooltip=Define conditions under which a user has to update their profile during first-time login. trust-email=Trust Email trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm. +link-only=Account Linking Only +link-only.tooltip=If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider hide-on-login-page=Hide on Login Page hide-on-login-page.tooltip=If hidden, then login with this provider is possible only if requested explicitly, e.g. using the 'kc_idp_hint' parameter. gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page). diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html index 6a04d37288..b456dd2342 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html @@ -64,6 +64,13 @@ {{:: 'trust-email.tooltip' | translate}} +
+ +
+ +
+ {{:: 'linkOnly.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index 83010b62e6..36e036eed4 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -61,6 +61,13 @@
{{:: 'trust-email.tooltip' | translate}}
+
+ +
+ +
+ {{:: 'linkOnly.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html index 8e200e7028..64dfcbe448 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html @@ -78,6 +78,13 @@
{{:: 'trust-email.tooltip' | translate}}
+
+ +
+ +
+ {{:: 'linkOnly.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html index 22ac869164..d764b90c78 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html @@ -48,6 +48,7 @@ {{:: 'provider' | translate}} {{:: 'enabled' | translate}} {{:: 'hidden' | translate}} + {{:: 'link-only-column' | translate}} {{:: 'gui-order' | translate}} {{:: 'actions' | translate}} @@ -64,6 +65,7 @@ {{identityProvider.providerId}} + {{identityProvider.config.guiOrder}} {{:: 'edit' | translate}} {{:: 'delete' | translate}} diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 823c4af969..6c168aeff8 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -109,6 +109,7 @@ role_manage-clients=Manage clients role_manage-events=Manage events role_view-profile=View profile role_manage-account=Manage account +role_manage-account-links=Manage account links role_read-token=Read token role_offline-access=Offline access client_account=Account