keycloak-uncached
Changes
services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java 13(+10 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java 7(+5 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java 114(+114 -0)
Details
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 8431493..7c754c2 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -39,6 +39,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.services.ErrorPage;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
@@ -225,12 +226,18 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
 
 
     protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
-        return UriBuilder.fromUri(getConfig().getAuthorizationUrl())
+        final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl())
                 .queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
                 .queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncodedState())
                 .queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
                 .queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
                 .queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
+
+        String loginHint = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
+        if (getConfig().isLoginHint() && loginHint != null) {
+            uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
+        }
+        return uriBuilder;
     }
 
     /**
                diff --git a/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java
index 12a3745..13dbcdd 100644
--- a/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java
@@ -74,4 +74,12 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
     public void setDefaultScope(String defaultScope) {
         getConfig().put("defaultScope", defaultScope);
     }
+
+    public boolean isLoginHint() {
+        return Boolean.valueOf(getConfig().get("loginHint"));
+    }
+
+    public void setLoginHint(boolean loginHint) {
+        getConfig().put("loginHint", String.valueOf(loginHint));
+    }
 }
\ No newline at end of file
                diff --git a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
index e706f54..3c88286 100755
--- a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
+++ b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
@@ -24,7 +24,6 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -121,10 +120,18 @@ public class AbstractOAuth2IdentityProviderTest {
 		Assert.assertEquals("458rtf", fi.getId());
 	}
 
-	private TestProvider getTested() {
+	private TestProvider getTested() {		
+		return new TestProvider(getConfig(null, null, null, Boolean.FALSE));
+	}
+
+	private OAuth2IdentityProviderConfig getConfig(final String autorizationUrl, final String defaultScope, final String clientId, final Boolean isLoginHint) {
 		IdentityProviderModel model = new IdentityProviderModel();
 		OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(model);
-		return new TestProvider(config);
+		config.setAuthorizationUrl(autorizationUrl);
+		config.setDefaultScope(defaultScope);
+		config.setClientId(clientId);
+		config.setLoginHint(isLoginHint);
+		return config;
 	}
 
 	private static class TestProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
index 61664a9..2f15ff6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
@@ -105,7 +105,12 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
         IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
 
         Map<String, String> config = idp.getConfig();
+        applyDefaultConfiguration(suiteContext, config);
 
+        return idp;
+    }
+
+    protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map<String, String> config) {
         config.put("clientId", CLIENT_ID);
         config.put("clientSecret", CLIENT_SECRET);
         config.put("prompt", "login");
@@ -115,8 +120,6 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
         config.put("userInfoUrl", getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo");
         config.put("defaultScope", "email profile");
         config.put("backchannelSupported", "true");
-
-        return idp;
     }
 
     @Override
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java
new file mode 100644
index 0000000..49ed362
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java
@@ -0,0 +1,114 @@
+package org.keycloak.testsuite.broker;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.USER_EMAIL;
+import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
+import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper;
+import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.arquillian.SuiteContext;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+public class KcOidcBrokerLoginHintTest extends AbstractBrokerTest {
+
+    @Override
+    protected BrokerConfiguration getBrokerConfiguration() {
+        return new KcOidcBrokerConfigurationWithLoginHint();
+    }
+
+    @Override
+    protected String getAccountUrl(String realmName) {
+        return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/account";
+    }
+
+    @Override
+    protected Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers() {
+        IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation();
+        attrMapper1.setName("manager-role-mapper");
+        attrMapper1.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID);
+        attrMapper1.setConfig(ImmutableMap.<String,String>builder()
+                .put("external.role", "manager")
+                .put("role", "manager")
+                .build());
+
+        IdentityProviderMapperRepresentation attrMapper2 = new IdentityProviderMapperRepresentation();
+        attrMapper2.setName("user-role-mapper");
+        attrMapper2.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID);
+        attrMapper2.setConfig(ImmutableMap.<String,String>builder()
+                .put("external.role", "user")
+                .put("role", "user")
+                .build());
+
+        return Lists.newArrayList(attrMapper1, attrMapper2);
+    }
+    
+    private class KcOidcBrokerConfigurationWithLoginHint extends KcOidcBrokerConfiguration {
+        
+        @Override
+        public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
+            IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
+
+            Map<String, String> config = idp.getConfig();
+            applyDefaultConfiguration(suiteContext, config);
+            config.put("loginHint", "true");
+            return idp;
+        }
+    }
+
+    @Override
+    protected void loginUser() {
+        driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
+        
+        driver.navigate().to(driver.getCurrentUrl() + "&login_hint=" + USER_EMAIL);
+
+        log.debug("Clicking social " + bc.getIDPAlias());
+        accountLoginPage.clickSocial(bc.getIDPAlias());
+
+        waitForPage(driver, "log in to");
+
+        Assert.assertTrue("Driver should be on the provider realm page right now",
+                driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
+
+        Assert.assertTrue("User identifiant should be fullfilled",
+                accountLoginPage.getUsername().equalsIgnoreCase(USER_EMAIL));
+        
+        log.debug("Logging in");
+        accountLoginPage.login(bc.getUserPassword());
+
+        waitForPage(driver, "update account information");
+
+        updateAccountInformationPage.assertCurrent();
+        Assert.assertTrue("We must be on correct realm right now",
+                driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
+
+        log.debug("Updating info on updateAccount page");
+        updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
+
+        UsersResource consumerUsers = adminClient.realm(bc.consumerRealmName()).users();
+
+        int userCount = consumerUsers.count();
+        Assert.assertTrue("There must be at least one user", userCount > 0);
+
+        List<UserRepresentation> users = consumerUsers.search("", 0, userCount);
+
+        boolean isUserFound = false;
+        for (UserRepresentation user : users) {
+            if (user.getUsername().equals(bc.getUserLogin()) && user.getEmail().equals(bc.getUserEmail())) {
+                isUserFound = true;
+                break;
+            }
+        }
+
+        Assert.assertTrue("There must be user " + bc.getUserLogin() + " in realm " + bc.consumerRealmName(),
+                isUserFound);
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java
new file mode 100644
index 0000000..d812ea4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java
@@ -0,0 +1,85 @@
+package org.keycloak.testsuite.broker;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.USER_EMAIL;
+import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
+import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
+import org.apache.commons.lang3.StringUtils;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.arquillian.SuiteContext;
+
+public class KcOidcBrokerNoLoginHintTest extends KcOidcBrokerLoginHintTest {
+
+    @Override
+    protected BrokerConfiguration getBrokerConfiguration() {
+        return new KcOidcBrokerConfigurationWithNoLoginHint();
+    }
+
+    private class KcOidcBrokerConfigurationWithNoLoginHint extends KcOidcBrokerConfiguration {
+        
+        @Override
+        public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
+            IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
+
+            Map<String, String> config = idp.getConfig();
+            applyDefaultConfiguration(suiteContext, config);
+            config.put("loginHint", "false");
+            return idp;
+        }
+    }
+
+    @Override
+    protected void loginUser() {
+        driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
+        
+        driver.navigate().to(driver.getCurrentUrl() + "&login_hint=" + USER_EMAIL);
+
+        log.debug("Clicking social " + bc.getIDPAlias());
+        accountLoginPage.clickSocial(bc.getIDPAlias());
+
+        waitForPage(driver, "log in to");
+
+        Assert.assertTrue("Driver should be on the provider realm page right now",
+                driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
+
+        Assert.assertTrue("User identifiant should not be fullfilled",
+                StringUtils.isBlank(accountLoginPage.getUsername()));
+        
+        log.debug("Logging in");
+        accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());
+
+        waitForPage(driver, "update account information");
+
+        updateAccountInformationPage.assertCurrent();
+        Assert.assertTrue("We must be on correct realm right now",
+                driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
+
+        log.debug("Updating info on updateAccount page");
+        updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
+
+        UsersResource consumerUsers = adminClient.realm(bc.consumerRealmName()).users();
+
+        int userCount = consumerUsers.count();
+        Assert.assertTrue("There must be at least one user", userCount > 0);
+
+        List<UserRepresentation> users = consumerUsers.search("", 0, userCount);
+
+        boolean isUserFound = false;
+        for (UserRepresentation user : users) {
+            if (user.getUsername().equals(bc.getUserLogin()) && user.getEmail().equals(bc.getUserEmail())) {
+                isUserFound = true;
+                break;
+            }
+        }
+
+        Assert.assertTrue("There must be user " + bc.getUserLogin() + " in realm " + bc.consumerRealmName(),
+                isUserFound);
+    }
+}
                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 b857d94..04b7f7a 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
@@ -504,6 +504,8 @@ authorization-url=Authorization URL
 authorization-url.tooltip=The Authorization Url.
 token-url=Token URL
 token-url.tooltip=The Token URL.
+loginHint=Pass login_hint
+loginHint.tooltip=Pass login_hint to identity provider.
 logout-url=Logout URL
 identity-provider.logout-url.tooltip=End session endpoint to use to logout user from external IDP.
 backchannel-logout=Backchannel Logout
                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 68a35ef..ee815a3 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
@@ -120,6 +120,13 @@
                 </div>
                 <kc-tooltip>{{:: 'authorization-url.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group">
+                <label class="col-sm-2 control-label" for="loginHint">{{:: 'loginHint' | translate}}</label>
+                <div class="col-sm-4">
+                    <input ng-model="identityProvider.config.loginHint" id="loginHint" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'loginHint.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group clearfix">
                 <label class="col-md-2 control-label" for="tokenUrl"><span class="required">*</span> {{:: 'token-url' | translate}}</label>
                 <div class="col-md-6">