keycloak-uncached
Changes
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GoogleLoginPage.java 25(+18 -7)
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>