keycloak-uncached
Changes
testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java 96(+96 -0)
testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ServletTestUtils.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java 7(+6 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java 5(+3 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java 16(+11 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java 509(+509 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdapterServletDeployment.java 93(+93 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/childrealm.json 38(+38 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/META-INF/context.xml 20(+20 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/jetty-web.xml 46(+46 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/web.xml 54(+54 -0)
themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html 7(+7 -0)
themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html 7(+7 -0)
Details
diff --git a/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java b/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java
new file mode 100644
index 0000000..0a421b8
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/HttpPostRedirect.java
@@ -0,0 +1,89 @@
+/*
+ * 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.common.util;
+
+import java.util.Map;
+
+
+/**
+ * Helper class to do a browser redirect via a POST.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class HttpPostRedirect {
+
+ /**
+ * Generate an HTML page that does a browser redirect via a POST. The HTML document uses Javascript to automatically
+ * submit a FORM post when loaded.
+ *
+ * This is similar to what the SAML Post Binding does.
+ *
+ * Here's an example
+ *
+ * <pre>
+ * {@code
+ * <HTML>
+ * <HEAD>
+ * <TITLE>title</TITLE>
+ * </HEAD>
+ * <BODY Onload="document.forms[0].submit()">
+ * <FORM METHOD="POST" ACTION="actionUrl">
+ * <INPUT TYPE="HIDDEN" NAME="param" VALUE="value"/>
+ * <NOSCRIPT>
+ * <P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>
+ * <INPUT TYPE="SUBMIT" VALUE="CONTINUE"/>
+ * </NOSCRIPT>
+ * </FORM>
+ * </BODY>
+ * </HTML>
+ * }
+ * </pre>
+
+ *
+ * @param title may be null. Just the title of the HTML document
+ * @param actionUrl URL to redirect to
+ * @param params must be encoded so that they can be placed in an HTML form hidden INPUT field value
+ * @return
+ */
+ public String buildHtml(String title, String actionUrl, Map<String, String> params) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("<HTML>")
+ .append("<HEAD>");
+ if (title != null) {
+ builder.append("<TITLE>SAML HTTP Post Binding</TITLE>");
+ }
+ builder.append("</HEAD>")
+ .append("<BODY Onload=\"document.forms[0].submit()\">")
+
+ .append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">");
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(param.getKey()).append("\"").append(" VALUE=\"").append(param.getValue()).append("\"/>");
+ }
+
+
+ builder.append("<NOSCRIPT>")
+ .append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
+ .append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
+ .append("</NOSCRIPT>")
+
+ .append("</FORM></BODY></HTML>");
+
+ return builder.toString();
+ }
+
+}
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 40b5301..65b94c6 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<String, String> config = new HashMap<String, String>();
@@ -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/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
index d8a9d7e..e63b7f2 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/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 1a1c1e9..90fa8d7 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<RealmEntity> {
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<RealmEntity> {
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<RealmEntity> {
entity.setAddReadTokenRoleOnCreate(identityProvider.isAddReadTokenRoleOnCreate());
entity.setStoreToken(identityProvider.isStoreToken());
entity.setConfig(identityProvider.getConfig());
+ entity.setLinkOnly(identityProvider.isLinkOnly());
}
}
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 0000000..5ac745c
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.0.0.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ 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.
+ -->
+
+<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="bburke@redhat.com" id="3.0.0">
+ <addColumn tableName="IDENTITY_PROVIDER">
+ <column name="LINK_ONLY" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false" />
+ </column>
+ </addColumn>
+
+ </changeSet>
+
+</databaseChangeLog>
\ 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 0a26548..59855ec 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 @@
<include file="META-INF/jpa-changelog-2.4.0.xml"/>
<include file="META-INF/jpa-changelog-2.5.0.xml"/>
<include file="META-INF/jpa-changelog-2.5.1.xml"/>
+ <include file="META-INF/jpa-changelog-3.0.0.xml"/>
</databaseChangeLog>
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 083ec42..9aec8ff 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/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index ea2b887..e17743e 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -75,4 +75,7 @@ public interface Errors {
String PASSWORD_CONFIRM_ERROR = "password_confirm_error";
String PASSWORD_MISSING = "password_missing";
String PASSWORD_REJECTED = "password_rejected";
+ String NOT_LOGGED_IN = "not_logged_in";
+ String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
+ String ILLEGAL_ORIGIN = "illegal_origin";
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index f97b713..f77137f 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -113,7 +113,10 @@ public enum EventType {
CLIENT_UPDATE(true),
CLIENT_UPDATE_ERROR(true),
CLIENT_DELETE(true),
- CLIENT_DELETE_ERROR(true);
+ CLIENT_DELETE_ERROR(true),
+
+ CLIENT_INITIATED_ACCOUNT_LINKING(true),
+ CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true);
private boolean saveByDefault;
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 d4949d5..83cdd84 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 0000000..db068fa
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 7c21292..e8e3509 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 7130255..3090c6e 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 2fc5136..2557bfd 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<String>(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<String, String> 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<String>(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<ProtocolMapperModel> 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<String, String> 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());
-
- 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;
+ 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());
+ }
+
+ return identityProviderModel;
}
public static ProtocolMapperModel toModel(ProtocolMapperRepresentation rep) {
@@ -1906,7 +1945,7 @@ public class RepresentationToModel {
if (roles != null && !roles.isEmpty()) {
try {
- List<Map> rolesMap = (List<Map>)JsonSerialization.readValue(roles, List.class);
+ List<Map> rolesMap = (List<Map>) 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/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 170925b..eb0dd69 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -216,8 +216,10 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
if (error != null) {
//logger.error("Failed " + getConfig().getAlias() + " broker login: " + error);
if (error.equals(ACCESS_DENIED)) {
+ logger.error(ACCESS_DENIED + " for broker login " + getConfig().getProviderId());
return callback.cancelled(state);
} else {
+ logger.error(error + " for broker login " + getConfig().getProviderId());
return callback.error(state, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
}
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 f682a3f..696e19b 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<IdentityProvider> 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/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 9646ecc..cf7d73c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -699,7 +699,7 @@ public class AuthenticationManager {
}
- protected static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
+ public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers) {
try {
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
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 cb80590..0921c60 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 a3c3fcb..e0e5f8b 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 162de45..c950ebe 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -17,6 +17,7 @@
package org.keycloak.services.resources;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
@@ -34,8 +35,10 @@ import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
+import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.Time;
+import org.keycloak.common.util.UriUtils;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@@ -55,17 +58,20 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.AccessToken;
-import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
@@ -88,6 +94,9 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -95,8 +104,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;
@@ -140,6 +151,182 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
this.event = new EventBuilder(realmModel, session, clientConnection).event(EventType.IDENTITY_PROVIDER_LOGIN);
}
+ private void checkRealm() {
+ if (!realmModel.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
+ }
+ }
+
+ private ClientModel checkClient(String clientId) {
+ if (clientId == null) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
+ }
+
+ event.client(clientId);
+
+ ClientModel client = realmModel.getClientByClientId(clientId);
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+ }
+
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+ }
+ return client;
+
+ }
+
+ /**
+ * Closes off CORS preflight requests for account linking
+ *
+ * @param providerId
+ * @return
+ */
+ @OPTIONS
+ @Path("/{provider_id}/link")
+ public Response clientIntiatedAccountLinkingPreflight(@PathParam("provider_id") String providerId) {
+ return Response.status(403).build(); // don't allow preflight
+ }
+
+
+ @GET
+ @NoCache
+ @Path("/{provider_id}/link")
+ public Response clientInitiatedAccountLinking(@PathParam("provider_id") String providerId,
+ @QueryParam("redirect_uri") String redirectUri,
+ @QueryParam("client_id") String clientId,
+ @QueryParam("nonce") String nonce,
+ @QueryParam("hash") String hash
+ ) {
+ this.event.event(EventType.CLIENT_INITIATED_ACCOUNT_LINKING);
+ checkRealm();
+ ClientModel client = checkClient(clientId);
+ AuthenticationManager authenticationManager = new AuthenticationManager();
+ redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realmModel, client);
+ if (redirectUri == null) {
+ event.error(Errors.INVALID_REDIRECT_URI);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+ }
+
+ if (nonce == null || hash == null) {
+ event.error(Errors.INVALID_REDIRECT_URI);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+
+ }
+
+ // only allow origins from client. Not sure we need this as I don't believe cookies can be
+ // sent if CORS preflight requests can't execute.
+ String origin = headers.getRequestHeaders().getFirst("Origin");
+ if (origin != null) {
+ String redirectOrigin = UriUtils.getOrigin(redirectUri);
+ if (!redirectOrigin.equals(origin)) {
+ event.error(Errors.ILLEGAL_ORIGIN);
+ throw new ErrorPageException(session, Messages.INVALID_REQUEST);
+
+ }
+ }
+
+ 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(errorParam, Errors.NOT_LOGGED_IN)
+ .queryParam("nonce", nonce);
+
+ return Response.status(302).location(builder.build()).build();
+ }
+
+
+
+ ClientSessionModel clientSession = null;
+ for (ClientSessionModel cs : cookieResult.getSession().getClientSessions()) {
+ if (cs.getClient().getClientId().equals(clientId)) {
+ byte[] decoded = Base64Url.decode(hash);
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new ErrorPageException(session, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST);
+ }
+ String input = nonce + cookieResult.getSession().getId() + cs.getId() + providerId;
+ byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
+ if (MessageDigest.isEqual(decoded, check)) {
+ clientSession = cs;
+ }
+ break;
+ }
+ }
+ if (clientSession == null) {
+ event.error(Errors.INVALID_TOKEN);
+ 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(errorParam, Errors.UNKNOWN_IDENTITY_PROVIDER)
+ .queryParam("nonce", nonce);
+ return Response.status(302).location(builder.build()).build();
+
+ }
+
+
+
+ ClientSessionCode clientSessionCode = new ClientSessionCode(session, realmModel, clientSession);
+ clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ clientSessionCode.getCode();
+ clientSession.setRedirectUri(redirectUri);
+ clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString());
+
+ event.success();
+
+
+ try {
+ IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId);
+ Response response = identityProvider.performLogin(createAuthenticationRequest(providerId, clientSessionCode));
+
+ if (response != null) {
+ if (isDebugEnabled()) {
+ logger.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider, response);
+ }
+ return response;
+ }
+ } catch (IdentityBrokerException e) {
+ return redirectToErrorPage(Messages.COULD_NOT_SEND_AUTHENTICATION_REQUEST, e, providerId);
+ } catch (Exception e) {
+ return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST, e, providerId);
+ }
+
+ return redirectToErrorPage(Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST);
+
+ }
+
+
@POST
@Path("/{provider_id}/login")
public Response performPostLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code) {
@@ -147,6 +334,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
@GET
+ @NoCache
@Path("/{provider_id}/login")
public Response performLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code) {
this.event.detail(Details.IDENTITY_PROVIDER, providerId);
@@ -162,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) {
@@ -198,11 +397,18 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
@GET
+ @NoCache
@Path("{provider_id}/token")
public Response retrieveToken(@PathParam("provider_id") String providerId) {
return getToken(providerId, false);
}
+ private boolean canReadBrokerToken(AccessToken token) {
+ Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
+ AccessToken.Access brokerRoles = resourceAccess == null ? null : resourceAccess.get(Constants.BROKER_SERVICE_CLIENT_ID);
+ return brokerRoles != null && brokerRoles.isUserInRole(Constants.READ_TOKEN_ROLE);
+ }
+
private Response getToken(String providerId, boolean forceRetrieval) {
this.event.event(EventType.IDENTITY_PROVIDER_RETRIEVE_TOKEN);
@@ -226,9 +432,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return corsResponse(forbidden("Realm has not migrated to support the broker token exchange service"), clientModel);
}
- Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
- AccessToken.Access brokerRoles = resourceAccess == null ? null : resourceAccess.get(Constants.BROKER_SERVICE_CLIENT_ID);
- if (brokerRoles == null || !brokerRoles.isUserInRole(Constants.READ_TOKEN_ROLE)) {
+ if (!canReadBrokerToken(token)) {
return corsResponse(forbidden("Client [" + clientModel.getClientId() + "] not authorized to retrieve tokens from identity provider [" + providerId + "]."), clientModel);
}
@@ -366,6 +570,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
// Callback from LoginActionsService after first login with broker was done and Keycloak account is successfully linked/created
@GET
+ @NoCache
@Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam("code") String code) {
ParsedCodeContext parsedCode = parseClientSessionCode(code);
@@ -487,6 +692,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
// Callback from LoginActionsService after postBrokerLogin flow is finished
@GET
+ @NoCache
@Path("/after-post-broker-login")
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code) {
ParsedCodeContext parsedCode = parseClientSessionCode(code);
@@ -612,11 +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) {
- return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias());
+ 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);
+
+ 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());
+ }
}
- UserModel authenticatedUser = clientSession.getUserSession().getUser();
if (isDebugEnabled()) {
logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", federatedIdentityModel, context.getIdpConfig().getAlias(), authenticatedUser);
@@ -645,15 +868,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
// Skip DB write if tokens are null or equal
- if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) {
- federatedIdentityModel.setToken(context.getToken());
-
- 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());
- }
- }
+ updateToken(context, federatedUser, federatedIdentityModel);
context.getIdp().updateBrokeredUser(session, realmModel, federatedUser, context);
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
if (mappers != null) {
@@ -666,6 +881,18 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
+ private void updateToken(BrokeredIdentityContext context, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
+ if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) {
+ federatedIdentityModel.setToken(context.getToken());
+
+ 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());
+ }
+ }
+ }
+
private ParsedCodeContext parseClientSessionCode(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index f3beb54..a23a26a 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/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 6e3da8d..4a2ce96 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -104,21 +104,6 @@ public class AdapterTest {
Thread.sleep(1000000000);
}
- public static class MySuper {
-
- }
-
- public static class Base extends MySuper {
- public Class superClass() {
- return super.getClass();
- }
- }
-
- @Test
- public void testBase() {
- System.out.println(new Base().superClass().getName());
- }
-
@Test
public void testLoginSSOAndLogout() throws Exception {
testStrategy.testLoginSSOMax();
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
new file mode 100644
index 0000000..d0678cf
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java
@@ -0,0 +1,96 @@
+/*
+ * 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.testsuite.adapter.servlet;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.representations.AccessToken;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+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");
+ KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AccessToken token = session.getToken();
+ String clientSessionId = token.getClientSession();
+ String nonce = UUID.randomUUID().toString();
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ String input = nonce + token.getSessionState() + clientSessionId + provider;
+ byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
+ String hash = Base64Url.encode(check);
+ request.getSession().setAttribute("hash", hash);
+ String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
+ .replaceQuery(null)
+ .queryParam("response", "true").build().toString();
+ String accountLinkUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase())
+ .path("/auth/realms/{realm}/broker/{provider}/link")
+ .queryParam("nonce", nonce)
+ .queryParam("hash", hash)
+ .queryParam("client_id", token.getIssuedFor())
+ .queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
+ resp.setStatus(302);
+ resp.setHeader("Location", accountLinkUrl);
+ } else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) {
+ resp.setStatus(200);
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
+ String error = request.getParameter("link_error");
+ if (error != null) {
+ pw.println("Link error: " + error);
+ } else {
+ pw.println("Account Linked");
+ }
+ pw.print("</body></html>");
+ pw.flush();
+ } else {
+ resp.setStatus(200);
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
+ pw.println("Unknown request: " + request.getRequestURL().toString());
+ pw.print("</body></html>");
+ pw.flush();
+
+ }
+
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ServletTestUtils.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ServletTestUtils.java
index 8097c4f..8a064a6 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ServletTestUtils.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ServletTestUtils.java
@@ -51,6 +51,6 @@ public class ServletTestUtils {
return System.getProperty("auth.server.ssl.base.url", "https://localhost:8543");
}
- return System.getProperty("auth.server.base.url");
+ return System.getProperty("auth.server.base.url", "http://localhost:8180");
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
index 94a8fb6..11d8fb2 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -144,7 +144,12 @@ public class LoginPage extends AbstractPage {
public boolean isCurrent() {
- return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test");
+ String realm = "test";
+ return isCurrent(realm);
+ }
+
+ public boolean isCurrent(String realm) {
+ return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
}
public void clickRegister() {
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 40fd420..75794eb 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
@@ -885,7 +885,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 6405d8c..1038491 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
@@ -422,7 +422,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);
@@ -437,7 +438,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 c5b7b31..40c1f70 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<String, String> 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
new file mode 100644
index 0000000..e639127
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java
@@ -0,0 +1,509 @@
+/*
+ * 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.testsuite.broker;
+
+import org.apache.http.client.utils.URIBuilder;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+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;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.adapter.page.AppServerContextRoot;
+import org.keycloak.testsuite.adapter.servlet.ClientInitiatedAccountLinkServlet;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProvider;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
+import org.keycloak.testsuite.util.AdapterServletDeployment;
+
+import javax.ws.rs.core.UriBuilder;
+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;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@AppServerContainer("auth-server-undertow")
+public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest {
+ public static final String CHILD_IDP = "child";
+ public static final String PARENT_IDP = "parent-idp";
+ public static final String PARENT_USERNAME = "parent";
+
+ @Page
+ protected UpdateAccountInformationPage profilePage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected AppServerContextRoot appServerContextRootPage;
+
+ public boolean isRelative() {
+ return testContext.isRelativeAdapterTest();
+ }
+
+ public static class ClientApp extends AbstractPageWithInjectedUrl {
+
+ public static final String DEPLOYMENT_NAME = "client-linking";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Override
+ public URL getInjectedUrl() {
+ return url;
+ }
+
+ }
+
+ @Page
+ protected ClientApp appPage;
+
+ @Override
+ protected boolean isImportAfterEachMethod() {
+ return true;
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = new RealmRepresentation();
+ realm.setRealm(CHILD_IDP);
+ realm.setEnabled(true);
+ ClientRepresentation servlet = new ClientRepresentation();
+ servlet.setClientId("client-linking");
+ servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ String uri = "/client-linking";
+ if (!isRelative()) {
+ uri = appServerContextRootPage.toString() + uri;
+ }
+ servlet.setAdminUrl(uri);
+ servlet.setBaseUrl(uri);
+ servlet.setRedirectUris(new LinkedList<>());
+ servlet.getRedirectUris().add(uri + "/*");
+ servlet.setSecret("password");
+ servlet.setFullScopeAllowed(true);
+ realm.setClients(new LinkedList<>());
+ realm.getClients().add(servlet);
+ testRealms.add(realm);
+
+
+ realm = new RealmRepresentation();
+ realm.setRealm(PARENT_IDP);
+ realm.setEnabled(true);
+
+ testRealms.add(realm);
+
+ }
+
+ @Deployment(name = "client-linking")
+ public static WebArchive customerPortal() {
+ return AdapterServletDeployment.oidcDeployment("client-linking", "/account-link-test", ClientInitiatedAccountLinkServlet.class);
+ }
+
+
+ @Before
+ public void addIdpUser() {
+ RealmResource realm = adminClient.realms().realm(PARENT_IDP);
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername(PARENT_USERNAME);
+ user.setEnabled(true);
+ String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+ }
+
+ private String childUserId = null;
+
+ @Before
+ public void addChildUser() {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ UserRepresentation user = new UserRepresentation();
+ user.setUsername("child");
+ user.setEnabled(true);
+ childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+ // 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<RoleRepresentation> roles = new LinkedList<>();
+ roles.add(role);
+ realm.users().get(childUserId).roles().realmLevel().add(roles);
+
+ }
+
+ @Before
+ public void createBroker() {
+ createParentChild();
+ }
+
+ public void createParentChild() {
+ BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
+ }
+
+ //@Test
+ public void testUi() throws Exception {
+ Thread.sleep(1000000000);
+
+ }
+
+ @Test
+ public void testErrorConditions() throws Exception {
+
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List<FederatedIdentityRepresentation> 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");
+
+ UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth")
+ .path("realms/child/broker/{provider}/link")
+ .queryParam("client_id", "client-linking")
+ .queryParam("redirect_uri", redirectUri.build())
+ .queryParam("hash", Base64Url.encode("crap".getBytes()))
+ .queryParam("nonce", UUID.randomUUID().toString());
+
+ 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("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.
+
+ driver.navigate().to(linkUrl);
+
+ 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<RoleRepresentation> 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() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List<FederatedIdentityRepresentation> 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));
+ Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
+ 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();
+
+
+ }
+
+ 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<FederatedIdentityRepresentation> 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/java/org/keycloak/testsuite/util/AdapterServletDeployment.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdapterServletDeployment.java
new file mode 100644
index 0000000..d8328c2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/AdapterServletDeployment.java
@@ -0,0 +1,93 @@
+/*
+ * 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.testsuite.util;
+
+import org.apache.commons.io.IOUtils;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * Expects a structure like adapter-test directory
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class AdapterServletDeployment {
+
+ public static final String JBOSS_DEPLOYMENT_STRUCTURE_XML = "jboss-deployment-structure.xml";
+ public static final String TOMCAT_CONTEXT_XML = "context.xml";
+
+ // hardcoded for now
+ public static final URL tomcatContext = AdapterServletDeployment.class
+ .getResource("/adapter-test/" + TOMCAT_CONTEXT_XML);
+
+ public static WebArchive oidcDeployment(String name, String configRoot, Class... servletClasses) {
+ return oidcDeployment(name, configRoot, "keycloak.json");
+
+ }
+
+
+ public static WebArchive oidcDeployment(String name, String configRoot, String adapterConfigFilename, Class... servletClasses) {
+ String configPath = configRoot + "/" + name;
+ String webInfPath = configPath + "/WEB-INF/";
+
+ URL keycloakJSON = AdapterServletDeployment.class.getResource(webInfPath + adapterConfigFilename);
+ URL webXML = AdapterServletDeployment.class.getResource(webInfPath + "web.xml");
+
+ WebArchive deployment = ShrinkWrap.create(WebArchive.class, name + ".war")
+ .addClasses(servletClasses)
+ .addAsWebInfResource(webXML, "web.xml");
+
+ URL keystore = AdapterServletDeployment.class.getResource(webInfPath + "keystore.jks");
+ if (keystore != null) {
+ deployment.addAsWebInfResource(keystore, "classes/keystore.jks");
+ }
+
+ if (keycloakJSON != null) {
+ deployment.addAsWebInfResource(keycloakJSON, "keycloak.json");
+ }
+
+ URL jbossDeploymentStructure = AdapterServletDeployment.class.getResource(webInfPath + JBOSS_DEPLOYMENT_STRUCTURE_XML);
+ if (jbossDeploymentStructure == null) {
+ jbossDeploymentStructure = AdapterServletDeployment.class.getResource(configRoot + "/" + JBOSS_DEPLOYMENT_STRUCTURE_XML);
+ }
+ if (jbossDeploymentStructure != null) deployment.addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML);
+
+ addContextXml(deployment, name);
+
+ return deployment;
+ }
+
+
+ public static void addContextXml(Archive archive, String contextPath) {
+ // hardcoded for now
+ try {
+ String contextXmlContent = IOUtils.toString(tomcatContext.openStream())
+ .replace("%CONTEXT_PATH%", contextPath);
+ archive.add(new StringAsset(contextXmlContent), "/META-INF/context.xml");
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/childrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/childrealm.json
new file mode 100644
index 0000000..57cc7e8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/childrealm.json
@@ -0,0 +1,38 @@
+{
+ "id": "child",
+ "realm": "child",
+ "enabled": true,
+ "accessTokenLifespan": 600,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "bburke@redhat.com",
+ "enabled": true,
+ "email" : "bburke@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ]
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "client-linking",
+ "enabled": true,
+ "adminUrl": "/client-linking",
+ "baseUrl": "/client-linking",
+ "redirectUris": [
+ "/client-linking/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/META-INF/context.xml
new file mode 100644
index 0000000..b4ddcce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/META-INF/context.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ 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.
+ -->
+
+<Context path="/customer-portal">
+ <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+ ~ 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.
+ -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+ <Get name="securityHandler">
+ <Set name="authenticator">
+ <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+ <!--
+ <Set name="adapterConfig">
+ <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+ <Set name="realm">tomcat</Set>
+ <Set name="resource">customer-portal</Set>
+ <Set name="authServerUrl">http://localhost:8180/auth</Set>
+ <Set name="sslRequired">external</Set>
+ <Set name="credentials">
+ <Map>
+ <Entry>
+ <Item>secret</Item>
+ <Item>password</Item>
+ </Entry>
+ </Map>
+ </Set>
+ <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+ </New>
+ </Set>
+ -->
+ </New>
+ </Set>
+ </Get>
+</Configure>
\ No newline at end of file
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
new file mode 100644
index 0000000..64c3157
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "child",
+ "resource" : "client-linking",
+ "auth-server-url" : "http://localhost:8180/auth",
+ "ssl-required" : "external",
+ "min-time-between-jwks-requests" : 0,
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/web.xml
new file mode 100644
index 0000000..8142588
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/account-link-test/client-linking/WEB-INF/web.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+
+ <module-name>client-linking</module-name>
+
+ <servlet>
+ <servlet-name>Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.adapter.servlet.ClientInitiatedAccountLinkServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>Servlet</servlet-name>
+ <url-pattern>/*</url-pattern>
+ </servlet-mapping>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>Users</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>user</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <login-config>
+ <auth-method>KEYCLOAK</auth-method>
+ <realm-name>child</realm-name>
+ </login-config>
+
+ <security-role>
+ <role-name>user</role-name>
+ </security-role>
+</web-app>
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 90b10de..6e72cd1 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 2ff5f96..05f7f04 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.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
index 22ac869..d764b90 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 @@
<th>{{:: 'provider' | translate}}</th>
<th>{{:: 'enabled' | translate}}</th>
<th>{{:: 'hidden' | translate}}</th>
+ <th>{{:: 'link-only-column' | translate}}</th>
<th width="15%">{{:: 'gui-order' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
@@ -64,6 +65,7 @@
<td>{{identityProvider.providerId}}</td>
<td translate="{{identityProvider.enabled}}"></td>
<td translate="{{identityProvider.config.hideOnLoginPage == 'true'}}"></td>
+ <td translate="{{identityProvider.linkOnly}}"></td>
<td>{{identityProvider.config.guiOrder}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-show="access.manageIdentityProviders" data-ng-click="removeIdentityProvider(identityProvider)">{{:: 'delete' | translate}}</td>
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 6a04d37..b456dd2 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
@@ -65,6 +65,13 @@
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'linkOnly.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
<label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | 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 83010b6..36e036e 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
@@ -62,6 +62,13 @@
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'linkOnly.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
<label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | 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 8e200e7..64dfcbe 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
@@ -79,6 +79,13 @@
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="linkOnly">{{:: 'link-only' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.linkOnly" name="identityProvider.trustEmail" id="linkOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'linkOnly.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
<label class="col-md-2 control-label" for="hideOnLoginPage">{{:: 'hide-on-login-page' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.hideOnLoginPage" name="identityProvider.config.hideOnLoginPage" id="hideOnLoginPage" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | 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 823c4af..6c168ae 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