8000 Prevent users to unlink their home identity provider when they are a managed member by pedroigor · Pull Request #30106 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Di 8000 ff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.keycloak.organization.utils.Organizations.resolveBroker;

import java.util.List;

import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
Expand Down Expand Up @@ -86,14 +87,14 @@ public void action(AuthenticationFlowContext context) {
return;
}

IdentityProviderModel broker = resolveBroker(session, user);
List<IdentityProviderModel> broker = resolveBroker(session, user);

if (broker == null) {
if (broker.isEmpty()) {
// not a managed member, continue with the regular flow
context.attempted();
} else {
} else if (broker.size() == 1) {
// user is a managed member and associated with a broker, redirect automatically
redirect(context, broker.getAlias(), user.getEmail());
redirect(context, broker.get(0).getAlias(), user.getEmail());
}

return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,18 @@ public static boolean canManageOrganizationGroup(KeycloakSession session, GroupM
return true;
}

public static IdentityProviderModel resolveBroker(KeycloakSession session, UserModel user) {
public static List<IdentityProviderModel> resolveBroker(KeycloakSession session, UserModel user) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
RealmModel realm = session.getContext().getRealm();
OrganizationModel organization = provider.getByMember(user);

if (organization == null || !organization.isEnabled()) {
return null;
return List.of();
}

if (provider.isManagedMember(organization, user)) {
// user is a managed member, try to resolve the origin broker and redirect automatically
List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList();
List<IdentityProviderModel> brokers = session.users().getFederatedIdentitiesStream(realm, user)
return session.users().getFederatedIdentitiesStream(realm, user)
.map(f -> {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());

Expand All @@ -95,11 +94,9 @@ public static IdentityProviderModel resolveBroker(KeycloakSession session, UserM
return null;
}).filter(Objects::nonNull)
.toList();

return brokers.size() == 1 ? brokers.get(0) : null;
}

return null;
return List.of();
}

public static Consumer<GroupModel> removeGroup(KeycloakSession session, RealmModel realm) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
import static org.keycloak.organization.utils.Organizations.resolveBroker;
import static org.keycloak.validate.BuiltinValidators.emailValidator;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.keycloak.Config.Scope;
import org.keycloak.broker.provider.BrokeredIdentityContext;
Expand Down Expand Up @@ -92,7 +96,7 @@ private void validateEmailDomain(String email, String inputHint, ValidationConte
AttributeContext attributeContext = upContext.getAttributeContext();
UserModel user = attributeContext.getUser();
String emailDomain = email.substring(email.indexOf('@') + 1);
List<String> expectedDomains = organization.getDomains().map(OrganizationDomainModel::getName).toList();
Set<String> expectedDomains = organization.getDomains().map(OrganizationDomainModel::getName).collect(Collectors.toSet());

if (expectedDomains.isEmpty()) {
// no domain to check
Expand All @@ -116,24 +120,33 @@ private void validateEmailDomain(String email, String inputHint, ValidationConte
context.addError(new ValidationError(ID, inputHint, "Email domain does not match any domain from the organization"));
}

private static List<String> resolveExpectedDomainsForManagedUser(ValidationContext context, UserModel user) {
IdentityProviderModel broker = resolveBroker(context.getSession(), user);
private static Set<String> resolveExpectedDomainsForManagedUser(ValidationContext context, UserModel user) {
List<IdentityProviderModel> brokers = resolveBroker(context.getSession(), user);

if (broker == null) {
return List.of();
if (brokers.isEmpty()) {
return Set.of();
}

Set<String> domains = new HashSet<>();

for (IdentityProviderModel broker : brokers) {
String domain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);

if (domain != null) {
domains.add(domain);
}
}

String domain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
return ofNullable(domain).map(List::of).orElse(List.of());
return Collections.unmodifiableSet(domains);
}

private static List<String> resolveExpectedDomainsWhenReviewingFederatedUserProfile(OrganizationModel organization, AttributeContext attributeContext) {
private static Set<String> resolveExpectedDomainsWhenReviewingFederatedUserProfile(OrganizationModel organization, AttributeContext attributeContext) {
// validating in the context of the brokering flow
KeycloakSession session = attributeContext.getSession();
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) session.getAttribute(BrokeredIdentityContext.class.getName());

if (brokerContext == null) {
return List.of();
return Set.of();
}

String alias = brokerContext.getIdpConfig().getAlias();
Expand All @@ -144,12 +157,12 @@ private static List<String> resolveExpectedDomainsWhenReviewingFederatedUserProf

if (broker == null) {
// the broker the user is authenticating is not linked to the organization
return List.of();
return Set.of();
}

// expect the email domain to match the domain set to the broker or none if not set
String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
return ofNullable(brokerDomain).map(List::of).orElse(List.of());
return ofNullable(brokerDomain).map(Set::of).orElse(Set.of());
}

private OrganizationModel resolveOrganization(ValidationContext context, KeycloakSession session) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ public class Messages {

public static final String FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER = "federatedIdentityRemovingLastProviderMessage";

public static final String FEDERATED_IDENTITY_BOUND_ORGANIZATION = "federatedIdentityBoundOrganization";

public static final String IDENTITY_PROVIDER_REDIRECT_ERROR = "identityProviderRedirectErrorMessage";

public static final String IDENTITY_PROVIDER_REMOVED = "identityProviderRemovedMessage";
Expand Down
55 changes: 33 additions & 22 deletions services/src/main/java/org/keycloak/services/resources/account/LinkedAccountsResource.java
10BC8
< 10BC8 td class="blob-code blob-code-context js-file-line"> String errorMessage = checkCommonPreconditions(providerAlias);
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.http.HttpRequest;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.util.Base64Url;
Expand All @@ -52,6 +54,7 @@
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.account.AccountLinkUriRepresentation;
import org.keycloak.representations.account.LinkedAccountRepresentation;
Expand All @@ -72,18 +75,18 @@
*/
public class LinkedAccountsResource {
private static final Logger logger = Logger.getLogger(LinkedAccountsResource.class);

private final KeycloakSession session;
private final HttpRequest request;
private final EventBuilder event;
private final UserModel user;
private final RealmModel realm;
private final Auth auth;

public LinkedAccountsResource(KeycloakSession session,
HttpRequest request,
public LinkedAccountsResource(KeycloakSession session,
HttpRequest request,
Auth auth,
EventBuilder event,
EventBuilder event,
UserModel user) {
this.session = session;
this.request = request;
Expand All @@ -92,7 +95,7 @@ public LinkedAccountsResource(KeycloakSession session,
this.user = user;
realm = session.getContext().getRealm();
}

@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
Expand All @@ -101,7 +104,7 @@ public Response linkedAccounts() {
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(linkedAccounts));
}

private Set<String> findSocialIds() {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(SocialIdentityProvider.class)
.map(ProviderFactory::getId)
Expand Down Expand Up @@ -141,27 +144,27 @@ private FederatedIdentityModel getIdentity(Stream<FederatedIdentityModel> identi
return identities.filter(model -> Objects.equals(model.getIdentityProvider(), providerAlias))
.findFirst().orElse(null);
}

@GET
@Path("/{providerAlias}")
@Produces(MediaType.APPLICATION_JSON)
@Deprecated
public Response buildLinkedAccountURI(@PathParam("providerAlias") String providerAlias,
@QueryParam("redirectUri") String redirectUri) {
auth.require(AccountRoles.MANAGE_ACCOUNT);

if (redirectUri == null) {
ErrorResponse.error(Messages.INVALID_REDIRECT_URI, Response.Status.BAD_REQUEST);
}

if (errorMessage != null) {
throw ErrorResponse.error(errorMessage, Response.Status.BAD_REQUEST);
}
if (auth.getSession() == null) {
throw ErrorResponse.error(Messages.SESSION_NOT_ACTIVE, Response.Status.BAD_REQUEST);
}

try {
String nonce = UUID.randomUUID().toString();
MessageDigest md = MessageDigest.getInstance("SHA-256");
Expand All @@ -177,35 +180,43 @@ public Response buildLinkedAccountURI(@PathParam("providerAlias") String provide
.queryParam("client_id", ACCOUNT_CONSOLE_CLIENT_ID)
.queryParam("redirect_uri", redirectUri)
.build();

AccountLinkUriRepresentation rep = new AccountLinkUriRepresentation();
rep.setAccountLinkUri(linkUri);
rep.setHash(hash);
rep.setNonce(nonce);

return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.ok(rep));
} catch (Exception spe) {
spe.printStackTrace();
throw ErrorResponse.error(Messages.FAILED_TO_PROCESS_RESPONSE, Response.Status.INTERNAL_SERVER_ERROR);
}
}

@DELETE
@Path("/{providerAlias}")
@Produces(MediaType.APPLICATION_JSON)
public Response removeLinkedAccount(@PathParam("providerAlias") String providerAlias) {
auth.require(AccountRoles.MANAGE_ACCOUNT);

String errorMessage = checkCommonPreconditions(providerAlias);
if (errorMessage != null) {
throw ErrorResponse.error(errorMessage, Response.Status.BAD_REQUEST);
}

FederatedIdentityModel link = session.users().getFederatedIdentity(realm, user, providerAlias);
if (link == null) {
throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_NOT_ACTIVE), Response.Status.BAD_REQUEST);
}

if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
if (Organizations.resolveBroker(session, user).stream()
.map(IdentityProviderModel::getAlias)
.anyMatch(providerAlias::equals)) {
throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_BOUND_ORGANIZATION), Response.Status.BAD_REQUEST);
}
}

// Removing last social provider is not possible if you don't have other possibility to authenticate
if (!(session.users().getFederatedIdentitiesStream(realm, user).count() > 1 || user.getFederationLink() != null || isPasswordSet())) {
throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER), Response.Status.BAD_REQUEST);
Expand All @@ -223,22 +234,22 @@ public Response removeLinkedAccount(@PathParam("providerAlias") String providerA

return Cors.builder().auth().allowedOrigins(auth.getToken()).add(Response.noContent());
}

private String checkCommonPreconditions(String providerAlias) {
auth.require(AccountRoles.MANAGE_ACCOUNT);

if (Validation.isEmpty(providerAlias)) {
return Messages.MISSING_IDENTITY_PROVIDER;
}

if (!isValidProvider(providerAlias)) {
return Messages.IDENTITY_PROVIDER_NOT_FOUND;
}

if (!user.isEnabled()) {
return Messages.ACCOUNT_DISABLED;
}

return null;
}

Expand All @@ -249,11 +260,11 @@ private String translateErrorMessage(String errorCode) {
return errorCode;
}
}

private boolean isPasswordSet() {
return user.credentialManager().isConfiguredFor(PasswordCredentialModel.TYPE);
}

private boolean isValidProvider(String providerAlias) {
return realm.getIdentityProvidersStream().anyMatch(model -> Objects.equals(model.getAlias(), providerAlias));
}
Expand Down
Loading
0