keycloak-uncached

Details

diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
index f3e4990..96854c5 100755
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
@@ -16,33 +16,22 @@
  */
 package org.keycloak.social.google;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.OAuthErrorException;
-import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
-import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
-import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+import org.keycloak.broker.provider.AuthenticationRequest;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
-import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.util.KeycloakUriBuilder;
-import org.keycloak.events.Details;
-import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
-import org.keycloak.representations.AccessTokenResponse;
-import org.keycloak.representations.IDToken;
 import org.keycloak.representations.JsonWebToken;
-import org.keycloak.services.ErrorResponseException;
 
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import java.io.IOException;
+import javax.ws.rs.core.UriBuilder;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -54,6 +43,8 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
     public static final String PROFILE_URL = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
     public static final String DEFAULT_SCOPE = "openid profile email";
 
+    private static final String OIDC_PARAMETER_HOSTED_DOMAINS = "hd";
+
     public GoogleIdentityProvider(KeycloakSession session, GoogleIdentityProviderConfig config) {
         super(session, config);
         config.setAuthorizationUrl(AUTH_URL);
@@ -99,5 +90,38 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
         return exchangeExternalUserInfoValidationOnly(event, params);
     }
 
+    @Override
+    protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
+        UriBuilder uriBuilder = super.createAuthorizationUrl(request);
+        String hostedDomain = ((GoogleIdentityProviderConfig) getConfig()).getHostedDomain();
+
+        if (hostedDomain != null) {
+            uriBuilder.queryParam(OIDC_PARAMETER_HOSTED_DOMAINS, hostedDomain);
+        }
+
+        return uriBuilder;
+    }
+
+    @Override
+    protected JsonWebToken validateToken(final String encodedToken, final boolean ignoreAudience) {
+        JsonWebToken token = super.validateToken(encodedToken, ignoreAudience);
+        String hostedDomain = ((GoogleIdentityProviderConfig) getConfig()).getHostedDomain();
+
+        if (hostedDomain == null) {
+            return token;
+        }
+
+        Object receivedHdParam = token.getOtherClaims().get(OIDC_PARAMETER_HOSTED_DOMAINS);
+
+        if (receivedHdParam == null) {
+            throw new IdentityBrokerException("Identity token does not contain hosted domain parameter.");
+        }
+
+        if (hostedDomain.equals("*") || hostedDomain.equals(receivedHdParam))  {
+            return token;
+        }
+
+        throw new IdentityBrokerException("Hosted domain does not match.");
+    }
 
 }
diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderConfig.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderConfig.java
index 859bacd..e95f5ec 100644
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderConfig.java
@@ -16,7 +16,6 @@
  */
 package org.keycloak.social.google;
 
-import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
 import org.keycloak.models.IdentityProviderModel;
 
@@ -38,4 +37,14 @@ public class GoogleIdentityProviderConfig extends OIDCIdentityProviderConfig {
         getConfig().put("userIp", String.valueOf(ip));
     }
 
+    public String getHostedDomain() {
+        String hostedDomain = getConfig().get("hostedDomain");
+
+        return hostedDomain == null || hostedDomain.isEmpty() ? null : hostedDomain;
+    }
+
+    public void setHostedDomain(final String hostedDomain) {
+        getConfig().put("hostedDomain", hostedDomain);
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GoogleLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GoogleLoginPage.java
index dee2ae6..3268f7a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GoogleLoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GoogleLoginPage.java
@@ -22,31 +22,42 @@ import org.openqa.selenium.NoSuchElementException;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
 
+import static org.keycloak.testsuite.util.UIUtils.clickLink;
+import static org.keycloak.testsuite.util.UIUtils.performOperationWithPageReload;
+import static org.keycloak.testsuite.util.URLUtils.navigateToUri;
+
 /**
  * @author Vaclav Muzikar <vmuzikar@redhat.com>
  */
 public class GoogleLoginPage extends AbstractSocialLoginPage {
-    @FindBy(xpath = ".//p[@role='heading'][1]")
-    private WebElement firstAccount;
-
     @FindBy(id = "identifierId")
     private WebElement emailInput;
 
     @FindBy(xpath = ".//input[@type='password']")
     private WebElement passwordInput;
 
+    @FindBy(id = "identifierLink")
+    private WebElement useAnotherAccountLink;
+
     @Override
     public void login(String user, String password) {
         try {
-            firstAccount.click();
+            clickLink(useAnotherAccountLink);
         }
         catch (NoSuchElementException e) {
-            emailInput.clear();
-            emailInput.sendKeys(user);
-            emailInput.sendKeys(Keys.RETURN);
+            // nothing to do
         }
 
+        emailInput.clear();
+        emailInput.sendKeys(user);
+        performOperationWithPageReload(() -> emailInput.sendKeys(Keys.RETURN));
         passwordInput.sendKeys(password);
         passwordInput.sendKeys(Keys.RETURN);
     }
+
+    @Override
+    public void logout() {
+        log.info("performing logout from Google");
+        navigateToUri("https://www.google.com/accounts/Logout", false);
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
index f40fb88..7b5e277 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
@@ -51,6 +51,7 @@ import org.keycloak.testsuite.util.URLUtils;
 import org.keycloak.testsuite.util.WaitUtils;
 import org.keycloak.util.BasicAuthHelper;
 import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
@@ -73,6 +74,8 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB_PRIVATE_EMAIL;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_HOSTED_DOMAIN;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_NON_MATCHING_HOSTED_DOMAIN;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT;
@@ -100,6 +103,8 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     public enum Provider {
         GOOGLE("google", GoogleLoginPage.class),
+        GOOGLE_HOSTED_DOMAIN("google", "google-hosted-domain", GoogleLoginPage.class),
+        GOOGLE_NON_MATCHING_HOSTED_DOMAIN("google", "google-hosted-domain", GoogleLoginPage.class),
         FACEBOOK("facebook", FacebookLoginPage.class),
         GITHUB("github", GitHubLoginPage.class),
         GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class),
@@ -238,6 +243,32 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     @Test
+    public void googleHostedDomainLogin() throws InterruptedException {
+        setTestProvider(GOOGLE_HOSTED_DOMAIN);
+        navigateToLoginPage();
+        assertTrue(driver.getCurrentUrl().contains("hd=" + getConfig(GOOGLE_HOSTED_DOMAIN, "hostedDomain")));
+        doLogin();
+        assertAccount();
+        testTokenExchange();
+    }
+
+    @Test
+    public void googleNonMatchingHostedDomainLogin() throws InterruptedException {
+        setTestProvider(GOOGLE_NON_MATCHING_HOSTED_DOMAIN);
+        navigateToLoginPage();
+        assertTrue(driver.getCurrentUrl().contains("hd=non-matching-hosted-domain"));
+        doLogin();
+
+        // Just to be sure there's no redirect in progress
+        WaitUtils.waitForPageToLoad();
+
+        WebElement errorMessage = driver.findElement(By.xpath(".//p[@class='instruction']"));
+
+        assertTrue(errorMessage.isDisplayed());
+        assertEquals("Unexpected error when authenticating with identity provider", errorMessage.getText());
+    }
+
+    @Test
     public void bitbucketLogin() throws InterruptedException {
         setTestProvider(BITBUCKET);
         performLogin();
@@ -328,6 +359,19 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         idp.setStoreToken(true);
         idp.getConfig().put("clientId", getConfig(provider, "clientId"));
         idp.getConfig().put("clientSecret", getConfig(provider, "clientSecret"));
+
+        if (provider == GOOGLE_HOSTED_DOMAIN) {
+            final String hostedDomain = getConfig(provider, "hostedDomain");
+            if (hostedDomain == null) {
+                throw new IllegalArgumentException("'hostedDomain' for Google IdP must be specified");
+            }
+            idp.getConfig().put("hostedDomain", hostedDomain);
+        }
+        if (provider == GOOGLE_NON_MATCHING_HOSTED_DOMAIN) {
+            idp.getConfig().put("hostedDomain", "non-matching-hosted-domain");
+        }
+
+
         if (provider == STACKOVERFLOW) {
             idp.getConfig().put("key", getConfig(provider, "clientKey"));
         }
@@ -350,6 +394,11 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     private void performLogin() {
+        navigateToLoginPage();
+        doLogin();
+    }
+
+    private void navigateToLoginPage() {
         currentSocialLoginPage.logout(); // try to logout first to be sure we're not logged in
         accountPage.navigateTo();
         loginPage.clickSocial(currentTestProvider.id());
@@ -357,7 +406,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         // Just to be sure there's no redirect in progress
         WaitUtils.pause(3000);
         WaitUtils.waitForPageToLoad();
+    }
 
+    private void doLogin() {
         // Only when there's not active session for the social provider, i.e. login is required
         if (URLUtils.currentUrlDoesntStartWith(getAuthServerRoot().toASCIIString())) {
             log.infof("current URL: %s", driver.getCurrentUrl());
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 65017c8..d9fb412 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
@@ -518,6 +518,8 @@ disableUserInfo=Disable User Info
 identity-provider.disableUserInfo.tooltip=Disable usage of User Info service to obtain additional user information?  Default is to use this OIDC service.
 userIp=Use userIp Param
 identity-provider.google-userIp.tooltip=Set 'userIp' query parameter when invoking on Google's User Info service.  This will use the user's ip address.  Useful if Google is throttling access to the User Info service.
+hostedDomain=Hosted Domain
+identity-provider.google-hostedDomain.tooltip=Set 'hd' query parameter when logging in with Google. Google will only list accounts for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered any hosted account can be used.
 sandbox=Target Sandbox
 identity-provider.paypal-sandbox.tooltip=Target PayPal's sandbox environment
 update-profile-on-first-login=Update Profile on First Login
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 9bd365e..3164a0f 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -747,8 +747,6 @@ module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, N
 });
 
 module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, $http, $route, realm, instance, providerFactory, IdentityProvider, serverInfo, authFlows, $location, Notifications, Dialog) {
-    console.log('RealmIdentityProviderCtrl');
-
     $scope.realm = angular.copy(realm);
 
     $scope.initSamlProvider = function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html
index 9d6632d..a0242fc 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html
@@ -1,7 +1,14 @@
 <div class="form-group">
+    <label class="col-md-2 control-label" for="hostedDomain">{{:: 'hostedDomain' | translate}}</label>
+    <div class="col-md-6">
+        <input ng-model="identityProvider.config.hostedDomain" id="hostedDomain" class="form-control" />
+    </div>
+    <kc-tooltip>{{:: 'identity-provider.google-hostedDomain.tooltip' | translate}}</kc-tooltip>
+</div>
+<div class="form-group">
     <label class="col-md-2 control-label" for="userIp">{{:: 'userIp' | translate}}</label>
     <div class="col-md-6">
         <input ng-model="identityProvider.config.userIp" id="userIp" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
     </div>
     <kc-tooltip>{{:: 'identity-provider.google-userIp.tooltip' | translate}}</kc-tooltip>
-</div>
\ No newline at end of file
+</div>