After 2.7 → 2.8 upgrade: OIDC UI uses the browser language (Accept-Language) and ignores jmix.core.available-locales → raw message key

jmix-bug-report-locale-oidc.md (5.6 KB)

▎ Worked fine for months on 2.7.6; broke immediately after upgrading to 2.8.2. OIDC-authenticated sessions now resolve i18n messages in the browser’s locale — for languages
▎ that aren’t (fully) translated this shows raw message keys instead of text. Critically, setting jmix.core.available-locales = de_DE (German only) does not prevent it —
▎ the UI still uses en for an English browser, i.e. a locale that isn’t even in available-locales. Easy to miss in production unless you test multiple browser languages.
▎ Not mentioned in the 2.8 release notes / breaking changes.

▎ Mechanism (decompiled 2.7.6 vs 2.8.2): RequestLocaleProvider is byte-identical. New in 2.8: UiClientDetailsSource / ClientDetailsSourceSupport / BaseClientDetailsSource
▎ and OidcVaadinSecurityFilterChainCustomizer, which sets the OAuth2 login filter’s AuthenticationDetailsSource to UiClientDetailsSource → the OIDC login now derives the
▎ ClientDetails locale from the request. UI message resolution uses it (MessagesImpl.getMessage → CurrentAuthentication.getLocale()).

▎ Defect localized by our workaround: our fix is a @Primary RequestLocaleProvider. The fact that overriding this class fixes it proves the OIDC locale flows through
▎ RequestLocaleProvider.getLocale(). The only difference is its return value for an unsupported locale: stock returns null (for en when only de_DE is available); our
▎ override returns a non-null de_DE. So when getLocale() returns null, the OIDC path does not fall back to an available locale (getDefaultLocale() is bypassed) — the
▎ browser locale is used instead.

▎ Please confirm whether this is intended, and either fix the fallback (never select a locale that isn’t in available-locales) or document the 2.7 → 2.8 behaviour change.
▎ Full details, reproduction and workaround in the attachment.

Hi,

This change happend in 2.8.0 within fix of security scopes (Login to UI via OIDC doesn't provide info about security scope · Issue #4842 · jmix-framework/jmix · GitHub). The idea was to replace the default org.springframework.security.web.authentication.WebAuthenticationDetails (which contains almost nothing) to our custom ClientDetails (which is used with our default non-OIDC login).

The case when it applies locale which is not in available-locales is definitely not intended.

But for now I can’t reproduce this case.

You mentioned that ClientDetailsSourceSupport.getLocale(request) should have fallback in case of returning null from requestLocaleProvider.getLocale(request) and return default locale.
And that exactly what happens:

ClientDetailsSourceSupport

public Locale getLocale(HttpServletRequest request) {
    Locale locale = requestLocaleProvider.getLocale(request);
    return locale == null ? getDefaultLocale() : locale;
}

protected Locale getDefaultLocale() {
    List<Locale> locales = coreProperties.getAvailableLocales();
    return locales.get(0);
}

My setup:

  • jmix.core.available-locales=de_DE
  • messages_de_DE.properties with values
  • Empty messages_en.properties
  • Edge browser with english only preferable language

Login flow:

  1. RequestLocaleProvider checks the ACCEPT_LANGUAGE header - not empty
  2. Request locale - en
  3. Available locales - only de_DE. en is not in available locales/languages → RequestLocaleProvider returns null as locale
  4. ClientDetailsSourceSupport goes for default locale which is the first of available → de_DE

As a possible extra change we can consider introducing some feature-toggle which prevents from setting locale to ClientDetails (or set default one directly) - this will simulate the old behavior.

But now I’m really curios how did you manage to apply the non-available locale.

Regards,
Ivan

Follow-up: reproduced — RequestLocaleProvider returns the request locale (de) instead of the matched available locale (de_DE), so a non-available locale ends up in ClientDetails

Jmix 2.8.2 · Vaadin 24.10.6 · Spring Boot 3.5.14 · OIDC/Keycloak login (jmix-oidc)

Thanks, Ivan — you were right that with available-locales = de_DE an English browser falls back to de_DE (I confirmed it in an isolated unit test). The reason you couldn’t reproduce is that our trigger was never en — it is de vs de_DE. I can now reproduce the “non-available locale applied” case you said is not intended.

Root cause

io.jmix.security.util.RequestLocaleProvider.getLocale() (unchanged 2.7↔2.8) decides like this (decompiled):

Locale req = request.getLocale();                 // e.g. de  (language only, no region)
List<Locale> available = coreProperties.getAvailableLocales();   // [de_DE]
if (available.contains(req)                                       // de_DE.equals(de) -> false
    || available.stream().anyMatch(l -> l.getLanguage().equals(req.getLanguage())))  // "de".equals("de") -> TRUE
    result = req;                                 // <-- returns req (= "de"), NOT the matched de_DE
else { log.warn(... "not supported ... ignored"); result = null; }

In the language-match branch it returns the request locale (de), not the matched available locale (de_DE). So ClientDetails.locale = de, which is not in available-locales = [de_DE]. Since only messages_de_DE.properties exists (no messages_de), message resolution does not find the bundle → raw message keys.

This is the same “a locale not in available-locales is applied” you confirmed is unintended — it just arrives via the language-match returns request-locale path, not via en.

Why only one browser, and why only after 2.8

  • The browser’s Accept-Language ordering differs:
    • Chrome: de-DE,de;q=0.9,...request.getLocale() = de_DE → exact match → de_DE → fine.
    • Edge: de,de-DE;q=0.9,... (user had “Deutsch” listed above “Deutsch (Deutschland)”) → request.getLocale() = de → language-match → returns de → raw keys.
  • 2.7 → 2.8 behavior change: In 2.7 the OIDC UI session did not derive its locale from the request (no ClientDetails/UiClientDetailsSource wiring), so the UI locale was the application default de_DE regardless of Accept-Language. In 2.8 (the WebAuthenticationDetailsClientDetails change, #4842) the request locale flows into ClientDetails, so Edge’s de now takes effect. The browser always sent de first — 2.7 simply ignored it.
  • Visible symptom: the page shows German briefly (initial render at default de_DE), then flips to raw keys the moment the authenticated UI applies CurrentAuthentication.getLocale() = ClientDetails.locale = de.

Reproduction

  1. Jmix 2.8.x app, OIDC/Keycloak login, jmix.core.available-locales = de_DE (German only), full messages_de_DE.properties, no messages_de.properties.
  2. Browser whose Accept-Language starts with de (language only), e.g. Edge with “Deutsch” above “Deutsch (Deutschland)”, header de,de-DE;q=0.9,.... (Your English test takes the null → fallback path, which is why it looked fine.)
  3. Log in via OIDC.
  4. Expected: German (de_DE is the only available locale). Actual: UI renders German for a moment, then flips to raw keys — ClientDetails.locale = de, which is not an available locale.

Isolated unit test (pure Mockito against stock RequestLocaleProvider) confirms: available=[de_DE], request.getLocale()=Locale.GERMANgetLocale() returns de (≠ de_DE, not contained in available-locales).

Suggested fix

In the language-match branch, return the matched available locale, not the request locale:

return available.stream()
        .filter(l -> l.getLanguage().equals(req.getLanguage()))
        .findFirst()
        .orElse(null);   // -> de_DE for a "de" request when available = [de_DE]

That way a language-only request (de) resolves to the available region locale (de_DE) and the region-specific message bundle is found — and a locale outside available-locales is never applied.

Workaround in use

@Primary RequestLocaleProvider returning Locale.GERMANY (de_DE) for our German-only product — forces the region and prevents the flip.

Regards,

Maik

Now it’s reproduced.

Created an issue - Login via OIDC can miss project-level localization messages (port to 2.8) · Issue #5390 · jmix-framework/jmix · GitHub
Will be fixed in the next release.

Thanks.

Regards,
Ivan