KEYCLOAK-7270 - Support for automatically linking brokered identities
This commit is contained in:
parent
62c1ffcb52
commit
c4a651bcac
5 changed files with 243 additions and 0 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthentic
|
||||||
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
|
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
||||||
|
org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
|
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
|
||||||
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
|
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
||||||
|
|
|
@ -166,6 +166,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
||||||
"You will be approved if you send query string parameter 'foo' with expected value.");
|
"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, "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, "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 " +
|
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");
|
"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 " +
|
addProviderInfo(result, "idp-create-user-if-unique", "Create User If Unique", "Detect if there is existing Keycloak account " +
|
||||||
|
|
|
@ -39,6 +39,9 @@ import java.util.Set;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -147,6 +150,77 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
|
||||||
}, APP_REALM_ID);
|
}, 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
|
// KEYCLOAK-5936
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue