Leantime/leantime · error · RuntimeException
sprintf($this->language->__($translationKey), ...$values)
Error message
sprintf($this->language->__($translationKey), ...$values)
What it means
displayError() (Oidc.php:640-644) is the single exit point for every translated OIDC login failure: it resolves the key via $this->language->__($translationKey) and throws RuntimeException(sprintf(...)) with the interpolated message. Known call sites: oidc.error.providerMismatch (issuer 'iss' does not equal the configured provider URL), oidc.error.unsupportedAlgorythm (only RS256 is mapped), oidc.error.unsupportedKeyFormat (JWKS key has neither x5c nor n/e). A second failure mode exists: sprintf() itself throws a PHP ValueError when the translation string contains more %-specifiers (or invalid ones) than values passed, which happens with hand-edited or partial language INI overrides of oidc.error.* keys.
Source
Thrown at app/Domain/Oidc/Services/Oidc.php:643
{
$storedState = (string) session('oidc.state');
session()->forget('oidc.state');
return $storedState !== '' && hash_equals($storedState, $state);
}
private function decodeBase64Url(string $value): string
{
return base64_decode(strtr($value, '-_', '+/'));
}
/**
* @throws HttpResponseException
*/
private function displayError(string $translationKey, string ...$values): void
{
throw new \RuntimeException(sprintf($this->language->__($translationKey), ...$values));
}
}
View on GitHub (pinned to 9a9f49f100)
Solutions
- Compare the token's iss claim byte-for-byte with the configured provider URL and align LEAN_OIDC_PROVIDER_URL (trailing slash is already tolerated).
- For unsupportedAlgorythm: switch the IdP client/realm signing algorithm to RS256 - getAlgorythm() only maps RS256 to OPENSSL_ALGO_SHA256.
- For unsupportedKeyFormat: point jwks_uri at an endpoint exposing x5c or n/e entries (for Google use https://www.googleapis.com/oauth2/v1/certs) or pin a certificate via env.
- For sprintf ValueError: fix the language INI override so the placeholder count matches the call (providerMismatch and unsupportedAlgorythm take exactly one/two %s).
- Check the raw translated string: php artisan tinker >>> app()->make(Language::class)->__('oidc.error.providerMismatch') to see exactly what sprintf receives.
Example fix
; before (custom de-DE.ini override - sprintf() gets 0 placeholders but 2 values)
oidc.error.providerMismatch="Der Provider stimmt nicht mit der lokalen Einstellung überein"
; after - exactly two %s, matching displayError('oidc.error.providerMismatch', $iss, $providerUrl)
oidc.error.providerMismatch="Der zurückgegebene Provider %s stimmt nicht mit der lokalen Einstellung %s überein" Defensive patterns
Strategy: validation
Validate before calling
// before translating/interpolating, ensure the translation keeps one %s per value
$translated = $this->language->__($translationKey);
if (substr_count($translated, '%') !== count($values)) {
Log::warning("OIDC translation placeholder mismatch for {$translationKey}");
$translated = implode(' ', array_merge([$translationKey], $values)); // safe, specifier-free fallback
}
throw new \RuntimeException(sprintf($translated, ...$values)); Type guard
/** True when a translation string's specifier count matches the values about to be passed. */
function translationPlaceholdersMatch(string $translated, int $valueCount): bool
{
return substr_count($translated, '%s') === $valueCount
&& substr_count($translated, '%') === $valueCount; // no stray % or exotic specifiers
} Try / catch
try {
$oidc->login();
} catch (\RuntimeException $e) {
// intended OIDC validation failures arrive here, already localized
return redirect('/login')->withErrors($e->getMessage());
} catch (\ValueError $e) {
// sprintf() itself failed: a language INI override has mismatched %s placeholders
Log::error('OIDC translation defect: '.$e->getMessage());
return redirect('/login')->withErrors('Login provider misconfigured. Contact the admin.');
} Prevention
- When overriding oidc.error.* keys in a language INI, keep exactly the placeholder count of the original string.
- Re-test OIDC login after installing custom language packs.
- Keep LEAN_OIDC_PROVIDER_URL byte-identical to the issuer claim (mod trailing slash) and prefer https.
- Confirm the IdP signs with RS256 before rolling out.
When it happens
Trigger: Any displayError() call during /oidc/callback: (1) the id_token's iss claim differs from LEAN_OIDC_PROVIDER_URL (only trailing slashes are trimmed); (2) token alg is ES256/HS256 instead of RS256; (3) JWKS keys carry neither x5c nor n/e; (4) sprintf ValueError when a custom language INI defines e.g. oidc.error.providerMismatch without exactly two %s placeholders.
Common situations: Trailing path or http/https mismatch between the configured provider URL and the issuer claim (e.g. 'https://sso.example.com/realms/x' vs 'https://sso.example.com/realms/x/'); provider signing with ES256 (Keycloak default for newer realms is RS256 but clients sometimes switch); untranslated or custom language packs whose oidc.error.* strings lost their %s placeholders; provider responding at a slightly different issuer after an upgrade.
Related errors
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/70f047fb04e77ac7.
Report an issue: GitHub.