KEYCLOAK-7270 - Support for automatically linking brokered identities

This commit is contained in:
slominskir 2018-09-11 12:17:12 -04:00 committed by Marek Posolda
parent 62c1ffcb52
commit c4a651bcac
5 changed files with 243 additions and 0 deletions

View file

@ -0,0 +1,67 @@
/*
* 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.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import static org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator.getExistingUser;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:Ryan.Slominski@gmail.com">Ryan Slominski</a>
*/
public class IdpAutoLinkAuthenticator extends AbstractIdpAuthenticator {
private static Logger logger = Logger.getLogger(IdpAutoLinkAuthenticator.class);
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
UserModel existingUser = getExistingUser(session, realm, authSession);
logger.debugf("User '%s' will auto link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
context.setUser(existingUser);
context.success();
}
@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
authenticateImpl(context, serializedCtx, brokerContext);
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}
}

View file

@ -0,0 +1,100 @@
/*
* 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.authentication.authenticators.broker;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
/**
* @author <a href="mailto:Ryan.Slominski@gmail.com">Ryan Slominski</a>
*/
public class IdpAutoLinkAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "idp-auto-link";
static IdpAutoLinkAuthenticator SINGLETON = new IdpAutoLinkAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return "autoLink";
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Automatically link brokered account";
}
@Override
public String getHelpText() {
return "Automatically link brokered account without any verification";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -33,6 +33,7 @@ org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthentic
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory

View file

@ -166,6 +166,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"You will be approved if you send query string parameter 'foo' with expected value.");
addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header");
addProviderInfo(result, "identity-provider-redirector", "Identity Provider Redirector", "Redirects to default Identity Provider or Identity Provider specified with kc_idp_hint query parameter");
addProviderInfo(result, "idp-auto-link", "Automatically link brokered account", "Automatically link brokered account without any verification");
addProviderInfo(result, "idp-confirm-link", "Confirm link existing account", "Show the form where user confirms if he wants " +
"to link identity provider with existing account or rather edit user profile data retrieved from identity provider to avoid conflict");
addProviderInfo(result, "idp-create-user-if-unique", "Create User If Unique", "Detect if there is existing Keycloak account " +

View file

@ -39,6 +39,9 @@ import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.IdentityProviderModel;
import static org.keycloak.testsuite.broker.AbstractFirstBrokerLoginTest.APP_REALM_ID;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -147,6 +150,77 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
}, APP_REALM_ID);
}
/**
* Tests that user can link federated identity with existing brokered
* account without prompt (KEYCLOAK-7270).
*/
@Test
public void testAutoLinkAccountWithBroker() throws Exception {
final String originalFirstBrokerLoginFlowId = getRealm().getIdentityProviderByAlias(getProviderId()).getFirstBrokerLoginFlowId();
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
AuthenticationFlowModel newFlow = new AuthenticationFlowModel();
newFlow.setAlias("AutoLink");
newFlow.setDescription("AutoLink");
newFlow.setProviderId("basic-flow");
newFlow.setBuiltIn(false);
newFlow.setTopLevel(true);
newFlow = appRealm.addAuthenticationFlow(newFlow);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticatorFlow(false);
execution.setAuthenticator("idp-create-user-if-unique");
execution.setPriority(1);
execution.setParentFlow(newFlow.getId());
execution = appRealm.addAuthenticatorExecution(execution);
AuthenticationExecutionModel execution2 = new AuthenticationExecutionModel();
execution2.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution2.setAuthenticatorFlow(false);
execution2.setAuthenticator("idp-auto-link");
execution2.setPriority(2);
execution2.setParentFlow(newFlow.getId());
execution2 = appRealm.addAuthenticatorExecution(execution2);
IdentityProviderModel idp = appRealm.getIdentityProviderByAlias(getProviderId());
idp.setFirstBrokerLoginFlowId(newFlow.getId());
appRealm.updateIdentityProvider(idp);
}
}, APP_REALM_ID);
// login through OIDC broker
loginIDP("pedroigor");
// authenticated and redirected to app. User is linked with identity provider
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
UserModel federatedUser = getFederatedUser();
assertNotNull(federatedUser);
assertEquals("pedroigor", federatedUser.getUsername());
assertEquals("psilva@redhat.com", federatedUser.getEmail());
RealmModel realmWithBroker = getRealm();
Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker);
assertEquals(1, federatedIdentities.size());
for (FederatedIdentityModel link : federatedIdentities) {
Assert.assertEquals("pedroigor", link.getUserName());
Assert.assertTrue(link.getIdentityProvider().equals(getProviderId()));
}
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
IdentityProviderModel idp = appRealm.getIdentityProviderByAlias(getProviderId());
idp.setFirstBrokerLoginFlowId(originalFirstBrokerLoginFlowId);
appRealm.updateIdentityProvider(idp);
}
}, APP_REALM_ID);
}
// KEYCLOAK-5936
@Test