8000 Custom OIDC identity providers parse email_verified from validated tokens when not pre-populated in context (26.6) by msdaly200 · Pull Request #55113 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
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 @@ -55,7 +55,8 @@ Although each type of identity provider has its configuration options, all share
|Trust Email
|When *ON*, {project_name} trusts email addresses from the identity provider. If the realm requires email validation, users that log in from this identity provider do not need to perform the email verification process.
If the target identity provider supports email verification and advertises this information when returning the user profile information, the email of the federated user will be (un)marked as verified.
For instance, an OpenID Connect Provider returning a `email_verified` claim in their ID Tokens.
For an OpenID Connect provider, this applies to the `email_verified` claim accompanying the email address in its ID token or UserInfo response.
If the claim is absent, the *Trust Email* setting provides the default verification status.
Note that this setting will set the email as verified when the user is federated for the first time and on subsequent logins
through the broker if the sync mode is set to `FORCE`.

Expand Down
25 changes: 25 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_6_9.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,28 @@ Updating an existing client scope does not revalidate its stored mappers.

Existing deployments continue to start, because mappers that are read from the database at startup are not validated.
If you provision realms from an exported JSON file, import the file once after upgrading to find out whether it contains a mapper that is now rejected.

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

Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
It also lists significant changes to internal APIs.

=== OIDC identity provider now respects `email_verified=false` from the ID token when Trust Email is enabled
// https://github.com/keycloak/keycloak/issues/54245

When brokering through an OpenID Connect identity provider with *Trust Email* enabled, {project_name} now correctly honors an `email_verified=false` claim in the ID token, even when the UserInfo endpoint does not include an `email_verified` field.

Previously, if a provider such as GitLab supplied `email_verified=false` in the ID token but omitted the claim from UserInfo, {project_name} ignored the token value and fell back to the `trustEmail` setting, marking the email as verified despite the IdP explicitly stating otherwise.

After upgrading, the following behavior applies when *Trust Email* is enabled:

* If UserInfo includes `email_verified`, that value is used and the ID token value is ignored.
* If UserInfo provides an email but omits `email_verified`, the ID token's `email_verified` value is used, provided the two emails match.
* If UserInfo returns a different email than the ID token, the ID token's `email_verified` claim is discarded because it does not apply to the email that was actually selected. {project_name} falls back to the default trust-email behavior for that email.
* If a custom provider stores the validated ID token under `VALIDATED_ID_TOKEN` in the brokered identity context but does not explicitly set an `EMAIL_VERIFIED` context entry, {project_name} reads `email_verified` directly from the token as a fallback, again only when the token and context emails match.

The change affects existing users under FORCE synchronization mode. When UserInfo provides the user's email but omits `email_verified`, {project_name} now applies the ID token's `email_verified` value, whether `true` or `false`, on every login, even if the user's email address has not changed since the last login.
For example, if an administrator marked the user's email as not verified, a later login whose ID token carries `email_verified=true` sets the flag back to `true`. Conversely, a token carrying `email_verified=false` sets it to `false` even if the administrator marked the email as verified.
Previously, the flag was left unchanged for existing users with an unchanged address in this situation.
If this behavior is not desired, set *Trust Email* to *OFF* for that identity provider. {project_name} then leaves the flag unchanged for existing users whose email address has not changed, and sets it to `false` for new users until the email is verified locally.
Original file line number Diff line number Diff line change
Expand Up @@ -627,11 +627,12 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
familyName = getJsonProperty(userInfo, IDToken.FAMILY_NAME);
preferredUsername = getUsernameFromUserInfo(userInfo);
String userInfoEmail = getJsonProperty(userInfo, "email");
Boolean userInfoEmailVerified = Boolean.parseBoolean(getJsonProperty(userInfo, IDToken.EMAIL_VERIFIED));
String userInfoEmailVerifiedClaim = getJsonProperty(userInfo, IDToken.EMAIL_VERIFIED);
Boolean userInfoEmailVerified = userInfoEmailVerifiedClaim == null ? null : Boolean.valueOf(userInfoEmailVerifiedClaim);

if (userInfoEmail != null) {
emailVerified = resolveEmailVerifiedFromUserInfo(userInfoEmail, userInfoEmailVerified, email, emailVerified);
email = userInfoEmail;
emailVerified = userInfoEmailVerified;
}

AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
Expand Down Expand Up @@ -682,6 +683,24 @@ protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenRespo
return identity;
}

protected static Boolean resolveEmailVerifiedFromUserInfo(String userInfoEmail, Boolean userInfoEmailVerified,
String idTokenEmail, Boolean idTokenEmailVerified) {
// use UserInfo claim if present
if (userInfoEmailVerified != null) {
return userInfoEmailVerified;
}
// discard the ID token claim if addresses differ (to avoid applying it to the wrong address)
if (!isSameEmail(userInfoEmail, idTokenEmail)) {
return null;
}
return idTokenEmailVerified;
}

// a missing address on either side is not considered a mismatch
private static boolean isSameEmail(String email1, String email2) {
return email1 == null || email2 == null || email1.equalsIgnoreCase(email2);
}

protected String getusernameClaimNameForIdToken() {
return IDToken.PREFERRED_USERNAME;
}
Expand Down Expand Up @@ -1186,10 +1205,22 @@ public boolean reloadKeys() {
protected void setEmailVerified(UserModel user, BrokeredIdentityContext context) {
OIDCIdentityProviderConfig config = getConfig();
Map<String, Object> contextData = context.getContextData();

// the value stored for the selected email takes precedence
Boolean emailVerified = (Boolean) contextData.get(IDToken.EMAIL_VERIFIED);

// Otherwise use the validated ID token claim, but only if the token has an email and it matches the selected one
if (emailVerified == null && contextData.get(VALIDATED_ID_TOKEN) instanceof JsonWebToken validatedIdToken) {
// a non-string email claim is ignored instead of failing the login
String tokenEmail = validatedIdToken.getOtherClaims().get(IDToken.EMAIL) instanceof String s ? s : null;
if (tokenEmail != null && isSameEmail(context.getEmail(), tokenEmail)) {
// no explicit entry, fallback to the claim in the validated token (providers that do not use the default extractIdentity)
emailVerified = getEmailVerifiedClaim(validatedIdToken);
}
}

// fallback to the default behavior if trust is disabled or there is no email_verified claim
if (Booleans.isFalse(config.isTrustEmail()) || emailVerified == null) {
// fallback to the default behavior if trust is disabled or there is no email_verified claim
super.setEmailVerified(user, context);
return;
}
Expand Down
Loading
0