8000 [CVE-2026-97175] Block auto-linking when IdP email is unverified by msdaly200 · Pull Request #54900 · 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
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,13 @@ The previous Base login templates are no longer provided.
Custom login themes with `parent=base` now inherit the new markup.
Review and update their template overrides and CSS to match the new structure.

=== Auto-linking requires a trusted email (CVE-2026-97175)

Identity provider auto-linking now blocks the link when the IdP email is not verified and *Trust Email* is not enabled on the identity provider.
This prevents an attacker who controls a brokered account with an unverified email from being automatically linked to a victim's existing consumer account.

If you use auto-link flows with a SAML or custom identity provider that does not set the `email_verified` claim, enable *Trust Email* on that identity provider to preserve the previous behavior.

// ------------------------ Notable changes ------------------------ //
== Notable changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.IDToken;
import org.keycloak.sessions.AuthenticationSessionModel;

/**
Expand Down Expand Up @@ -134,6 +135,11 @@ public void setEmail(String email) {
this.email = email;
}

public boolean isEmailVerified() {
Object val = contextData.get(IDToken.EMAIL_VERIFIED);
return val instanceof Boolean && (Boolean) val;
}

public String getBrokerSessionId() {
return brokerSessionId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
package org.keycloak.authentication.authenticators.broker;

import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.Booleans;

import org.jboss.logging.Logger;

import static org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator.getExistingUser;

/**
* @author <a href="mailto:Ryan.Slominski@gmail.com">Ryan Slominski</a>
Expand All @@ -38,6 +41,47 @@ public class IdpAutoLinkAuthenticator extends AbstractIdpAuthenticator {

@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {

// Check if email was changed on Review Profile Page
String emailChanged = context.getAuthenticationSession().getAuthNote(AbstractIdentityProvider.UPDATE_PROFILE_EMAIL_CHANGED);
boolean emailWasEdited = "true".equals(emailChanged);
Comment on lines +46 to +47

// Check if username was changed on Review Profile Page
String usernameChanged = context.getAuthenticationSession().getAuthNote(AbstractIdentityProvider.UPDATE_PROFILE_USERNAME_CHANGED);
boolean usernameWasEdited = "true".equals(usernameChanged);

// Don't auto-link when email or username edited on Review Profile.
if (emailWasEdited || usernameWasEdited) {
logger.debugf("Auto-link blocked: email or username was edited on Review Profile Page");
context.attempted();
return;
}

String existingUserInfoJson = context.getAuthenticationSession().getAuthNote(EXISTING_USER_INFO);
if (existingUserInfoJson != null) {
ExistingUserInfo existingUserInfo = ExistingUserInfo.deserialize(existingUserInfoJson);

if (UserModel.EMAIL.equals(existingUserInfo.getDuplicateAttributeName())) {
Comment thread
msdaly200 marked this conversation as resolved.
Resolved
IdentityProviderModel idpConfig = brokerContext.getIdpConfig();

// Email must match the broker email
if (brokerContext.getEmail() == null || !brokerContext.getEmail().equalsIgnoreCase(existingUserInfo.getDuplicateAttributeValue())) {
logger.debugf("Auto-link blocked: broker email '%s' does not match existing user email '%s'",
brokerContext.getEmail(), existingUserInfo.getDuplicateAttributeValue());
context.attempted();
return;
}

// Email must be verified or trusted
if (!Booleans.isTrue(idpConfig.isTrustEmail()) && !brokerContext.isEmailVerified()) {
Comment thread
msdaly200 marked this conversation as resolved.
Resolved
logger.debugf("Auto-link blocked: email '%s' is not verified by IdP '%s'",
brokerContext.getEmail(), idpConfig.getAlias());
context.attempted();
Comment thread
msdaly200 marked this conversation as resolved.
Resolved
return;
}
}
}

KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
Expand Down
Loading
Loading
0