Merge pull request #3762 from sldab/hide-providers

KEYCLOAK-4224 Allow hiding identity providers on login page
This commit is contained in:
Stian Thorgersen 2017-02-17 12:04:35 +01:00 committed by GitHub
commit 3653d7ed9a
13 changed files with 170 additions and 15 deletions

View file

@ -27,6 +27,7 @@ import java.net.URI;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
@ -63,10 +64,13 @@ public class IdentityProviderBean {
private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) { private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString(); String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider); String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
Map<String, String> config = identityProvider.getConfig();
orderedSet.add(new IdentityProvider(identityProvider.getAlias(), boolean hideOnLoginPage = config != null && Boolean.parseBoolean(config.get("hideOnLoginPage"));
displayName, identityProvider.getProviderId(), loginUrl, if (!hideOnLoginPage) {
identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null)); orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
displayName, identityProvider.getProviderId(), loginUrl,
config != null ? config.get("guiOrder") : null));
}
} }
public List<IdentityProvider> getProviders() { public List<IdentityProvider> getProviders() {

View file

@ -50,13 +50,23 @@ public class AccountFederatedIdentityPage extends AbstractAccountPage {
public boolean isCurrent() { public boolean isCurrent() {
return driver.getTitle().contains("Account Management") && driver.getPageSource().contains("Federated Identities"); return driver.getTitle().contains("Account Management") && driver.getPageSource().contains("Federated Identities");
} }
public void clickAddProvider(String providerId) { public WebElement findAddProviderButton(String alias) {
driver.findElement(By.id("add-" + providerId)).click(); return driver.findElement(By.id("add-" + alias));
}
public WebElement findRemoveProviderButton(String alias) {
return driver.findElement(By.id("remove-" + alias));
} }
public void clickRemoveProvider(String providerId) { public void clickAddProvider(String alias) {
driver.findElement(By.id("remove-" + providerId)).click(); WebElement addButton = findAddProviderButton(alias);
addButton.click();
}
public void clickRemoveProvider(String alias) {
WebElement addButton = findRemoveProviderButton(alias);
addButton.click();
} }
public String getError() { public String getError() {

View file

@ -40,6 +40,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.drone.Different; import org.keycloak.testsuite.drone.Different;
import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
import org.keycloak.testsuite.pages.AccountLogPage; import org.keycloak.testsuite.pages.AccountLogPage;
import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountSessionsPage; import org.keycloak.testsuite.pages.AccountSessionsPage;
@ -95,6 +96,12 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
.alias("myoidc") .alias("myoidc")
.displayName("MyOIDC") .displayName("MyOIDC")
.build()); .build());
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("oidc")
.alias("myhiddenoidc")
.displayName("MyHiddenOIDC")
.hideOnLoginPage()
.build());
RealmBuilder.edit(testRealm) RealmBuilder.edit(testRealm)
.user(user2); .user(user2);
@ -138,6 +145,9 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
@Page @Page
protected AccountApplicationsPage applicationsPage; protected AccountApplicationsPage applicationsPage;
@Page
protected AccountFederatedIdentityPage federatedIdentityPage;
@Page @Page
protected ErrorPage errorPage; protected ErrorPage errorPage;
@ -918,7 +928,13 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText()); Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText()); Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText());
Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText()); Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
}
@Test
public void testIdentityProviderHiddenOnLoginPageIsVisbleInAccount(){
federatedIdentityPage.open();
loginPage.login("test-user@localhost", "password");
Assert.assertNotNull(federatedIdentityPage.findAddProviderButton("myhiddenoidc"));
} }
@Test @Test

View file

@ -0,0 +1,65 @@
/*
* 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.forms;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
public class HiddenProviderTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPage loginPage;
@Override
protected RealmResource testRealm() {
return adminClient.realm("realm-with-broker");
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("oidc")
.alias("visible-oidc")
.displayName("VisibleOIDC")
.build());
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
.providerId("oidc")
.alias("hidden-oidc")
.displayName("HiddenOIDC")
.hideOnLoginPage()
.build());
}
@Test
public void testVisibleProviderButton() {
loginPage.open();
Assert.assertNotNull(loginPage.findSocialButton("visible-oidc"));
}
@Test(expected=org.openqa.selenium.NoSuchElementException.class)
public void testHiddenProviderButton() {
loginPage.open();
Assert.assertNull(loginPage.findSocialButton("hidden-oidc"));
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.util; package org.keycloak.testsuite.util;
import java.util.HashMap;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
/** /**
@ -48,6 +49,14 @@ public class IdentityProviderBuilder {
rep.setDisplayName(displayName); rep.setDisplayName(displayName);
return this; return this;
} }
public IdentityProviderBuilder hideOnLoginPage() {
if (rep.getConfig() == null) {
rep.setConfig(new HashMap<>());
}
rep.getConfig().put("hideOnLoginPage", "true");
return this;
}
public IdentityProviderRepresentation build() { public IdentityProviderRepresentation build() {
return rep; return rep;

View file

@ -87,6 +87,13 @@ public class IdentityProviderHintTest {
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
assertTrue(this.driver.getPageSource().contains("idToken")); assertTrue(this.driver.getPageSource().contains("idToken"));
} }
@Test
public void testSuccessfulRedirectToProviderHiddenOnLoginPage() {
this.driver.navigate().to("http://localhost:8081/test-app?kc_idp_hint=kc-oidc-idp-hidden");
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
}
@Test @Test
public void testInvalidIdentityProviderHint() { public void testInvalidIdentityProviderHint() {

View file

@ -143,13 +143,13 @@ public class LoginPage extends AbstractPage {
registerLink.click(); registerLink.click();
} }
public void clickSocial(String providerId) { public void clickSocial(String alias) {
WebElement socialButton = findSocialButton(providerId); WebElement socialButton = findSocialButton(alias);
socialButton.click(); socialButton.click();
} }
public WebElement findSocialButton(String providerId) { public WebElement findSocialButton(String alias) {
String id = "zocial-" + providerId; String id = "zocial-" + alias;
return this.driver.findElement(By.id(id)); return this.driver.findElement(By.id(id));
} }

View file

@ -205,6 +205,24 @@
"defaultScope": "email profile", "defaultScope": "email profile",
"backchannelSupported": "true" "backchannelSupported": "true"
} }
},
{
"alias" : "kc-oidc-idp-hidden",
"providerId" : "keycloak-oidc",
"enabled": true,
"storeToken" : true,
"addReadTokenRoleOnCreate": true,
"config": {
"clientId": "broker-app",
"clientSecret": "secret",
"authorizationUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-hidden/protocol/openid-connect/auth",
"tokenUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-hidden/protocol/openid-connect/token",
"userInfoUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-hidden/protocol/openid-connect/userinfo",
"logoutUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-hidden/protocol/openid-connect/logout",
"defaultScope": "email profile",
"backchannelSupported": "true",
"hideOnLoginPage": true
}
} }
], ],
"identityProviderMappers": [ "identityProviderMappers": [

View file

@ -2,6 +2,7 @@ consoleTitle=Keycloak Admin Console
# Common messages # Common messages
enabled=Enabled enabled=Enabled
hidden=Hidden
name=Name name=Name
displayName=Display name displayName=Display name
displayNameHtml=HTML Display name displayNameHtml=HTML Display name
@ -466,6 +467,8 @@ off=Off
update-profile-on-first-login.tooltip=Define conditions under which a user has to update their profile during first-time login. 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=Trust Email
trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm. trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm.
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). gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page).
first-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account. first-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.
post-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it. post-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.

View file

@ -64,6 +64,13 @@
</div> </div>
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div> </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}}" />
</div>
<kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label> <label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
<div class="col-md-6"> <div class="col-md-6">

View file

@ -61,6 +61,13 @@
</div> </div>
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div> </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}}" />
</div>
<kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label> <label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
<div class="col-md-6"> <div class="col-md-6">

View file

@ -78,6 +78,13 @@
</div> </div>
<kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'trust-email.tooltip' | translate}}</kc-tooltip>
</div> </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}}" />
</div>
<kc-tooltip>{{:: 'hide-on-login-page.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label> <label class="col-md-2 control-label" for="guiOrder">{{:: 'gui-order' | translate}}</label>
<div class="col-md-6"> <div class="col-md-6">

View file

@ -33,7 +33,7 @@
<caption class="hidden">{{:: 'table-of-identity-providers' | translate}}</caption> <caption class="hidden">{{:: 'table-of-identity-providers' | translate}}</caption>
<thead> <thead>
<tr> <tr>
<th colspan="6" class="kc-table-actions"> <th colspan="7" class="kc-table-actions">
<div class="dropdown pull-right" data-ng-show="access.manageIdentityProviders"> <div class="dropdown pull-right" data-ng-show="access.manageIdentityProviders">
<select class="form-control" ng-model="provider" <select class="form-control" ng-model="provider"
ng-options="p.name group by p.groupName for p in allProviders track by p.id" ng-options="p.name group by p.groupName for p in allProviders track by p.id"
@ -47,6 +47,7 @@
<th>{{:: 'name' | translate}}</th> <th>{{:: 'name' | translate}}</th>
<th>{{:: 'provider' | translate}}</th> <th>{{:: 'provider' | translate}}</th>
<th>{{:: 'enabled' | translate}}</th> <th>{{:: 'enabled' | translate}}</th>
<th>{{:: 'hidden' | translate}}</th>
<th width="15%">{{:: 'gui-order' | translate}}</th> <th width="15%">{{:: 'gui-order' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th> <th colspan="2">{{:: 'actions' | translate}}</th>
</tr> </tr>
@ -62,6 +63,7 @@
</td> </td>
<td>{{identityProvider.providerId}}</td> <td>{{identityProvider.providerId}}</td>
<td translate="{{identityProvider.enabled}}"></td> <td translate="{{identityProvider.enabled}}"></td>
<td translate="{{identityProvider.config.hideOnLoginPage == 'true'}}"></td>
<td>{{identityProvider.config.guiOrder}}</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" 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> <td class="kc-action-cell" data-ng-show="access.manageIdentityProviders" data-ng-click="removeIdentityProvider(identityProvider)">{{:: 'delete' | translate}}</td>