From c7a3b42b27873a53f5193d730dcdeb9be6c7cff8 Mon Sep 17 00:00:00 2001 From: Mohammed Elbagoury Date: Tue, 5 May 2026 01:41:11 +0300 Subject: [PATCH 1/2] Resolve realm display name localization placeholders in TOTP issuer name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the realm display name before it is used as the issuer name when configuring TOTP credentials. This allows values that use localization placeholder syntax (e.g., ${realm.displayName}) to be resolved using the LOGIN theme messages and the user’s resolved locale, so authenticator applications show the localized display name instead of the raw placeholder. Fixes #48680 Signed-off-by: Mohammed Elbagoury --- .../login/freemarker/model/TotpBean.java | 2 +- .../java/org/keycloak/utils/TotpUtils.java | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java index ea73e3f7515a..79a867e53140 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java @@ -72,7 +72,7 @@ public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBu this.totpSecret = secret; } this.totpSecretEncoded = TotpUtils.encode(totpSecret); - this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); + this.totpSecretQrCode = TotpUtils.qrCode(session, totpSecret, realm, user); OTPPolicy otpPolicy = realm.getOTPPolicy(); this.supportedApplications = session.getAllProviders(OTPApplicationProvider.class).stream() diff --git a/services/src/main/java/org/keycloak/utils/TotpUtils.java b/services/src/main/java/org/keycloak/utils/TotpUtils.java index cd4f0cc36751..bb64602464fd 100644 --- a/services/src/main/java/org/keycloak/utils/TotpUtils.java +++ b/services/src/main/java/org/keycloak/utils/TotpUtils.java @@ -17,15 +17,26 @@ package org.keycloak.utils; +import java.io.IOException; +import java.util.Locale; +import java.util.Properties; + +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.Base32; +import org.keycloak.theme.TemplatingUtil; +import org.keycloak.theme.Theme; + +import org.jboss.logging.Logger; /** * @author Stian Thorgersen */ public class TotpUtils { + private static final Logger logger = Logger.getLogger(TotpUtils.class); + public static String encode(String totpSecret) { String encoded = Base32.encode(totpSecret.getBytes()); StringBuilder sb = new StringBuilder(); @@ -51,4 +62,37 @@ public static String qrCode(String totpSecret, RealmModel realm, UserModel user) } } + public static String qrCode(KeycloakSession session, String totpSecret, RealmModel realm, UserModel user) { + if (session == null) { + return qrCode(totpSecret, realm, user); + } + try { + String issuerName = getIssuerName(session, realm, user); + String keyUri = realm.getOTPPolicy().getKeyURI(issuerName, user.getUsername(), totpSecret); + + int width = 246; + int height = 246; + + return QRCodeUtils.encodeAsQRString(keyUri, width, height); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String getIssuerName(KeycloakSession session, RealmModel realm, UserModel user) { + String displayName = realm.getDisplayName(); + if (StringUtil.isNullOrEmpty(displayName)) { + return realm.getName(); + } + + try { + Locale locale = session.getContext().resolveLocale(user); + Properties messages = session.theme().getTheme(Theme.Type.LOGIN).getEnhancedMessages(realm, locale); + return TemplatingUtil.resolveVariables(displayName, messages); + } catch (IOException e) { + logger.warn("Failed to load messages to resolve realm display name", e); + return displayName; + } + } + } From ea8030853ac80b19199a30a78baada774e87f06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Thu, 7 May 2026 15:38:34 +0200 Subject: [PATCH 2/2] Cleanup the TotpUtils + add deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Bartoš --- .../java/org/keycloak/utils/TotpUtils.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/services/src/main/java/org/keycloak/utils/TotpUtils.java b/services/src/main/java/org/keycloak/utils/TotpUtils.java index bb64602464fd..b1a935295f11 100644 --- a/services/src/main/java/org/keycloak/utils/TotpUtils.java +++ b/services/src/main/java/org/keycloak/utils/TotpUtils.java @@ -49,26 +49,28 @@ public static String encode(String totpSecret) { return sb.toString(); } + /** + * Generates a QR code using the realm name as issuer. Use when no session is available. + * + * @deprecated Use {@link #qrCode(KeycloakSession, String, RealmModel, UserModel)} instead to get locale-aware issuer names. + */ + @Deprecated(since = "26.7.0") public static String qrCode(String totpSecret, RealmModel realm, UserModel user) { - try { - String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret); - - int width = 246; - int height = 246; - - return QRCodeUtils.encodeAsQRString(keyUri, width, height); - } catch (Exception e) { - throw new RuntimeException(e); - } + return qrCode(null, totpSecret, realm, user); } + /** + * Generates a QR code using a locale-aware realm display name as issuer. Preferred when a session is available. + */ public static String qrCode(KeycloakSession session, String totpSecret, RealmModel realm, UserModel user) { - if (session == null) { - return qrCode(totpSecret, realm, user); - } try { - String issuerName = getIssuerName(session, realm, user); - String keyUri = realm.getOTPPolicy().getKeyURI(issuerName, user.getUsername(), totpSecret); + String keyUri; + if (session != null) { + String issuerName = getIssuerName(session, realm, user); + keyUri = realm.getOTPPolicy().getKeyURI(issuerName, user.getUsername(), totpSecret); + } else { + keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret); + } int width = 246; int height = 246; @@ -79,7 +81,7 @@ public static String qrCode(KeycloakSession session, String totpSecret, RealmMod } } - public static String getIssuerName(KeycloakSession session, RealmModel realm, UserModel user) { + private static String getIssuerName(KeycloakSession session, RealmModel realm, UserModel user) { String displayName = realm.getDisplayName(); if (StringUtil.isNullOrEmpty(displayName)) { return realm.getName();