{"record":{"id":"70f047fb04e77ac7","repo":"Leantime/leantime","slug":"sprintf-this-language-translationkey","errorCode":null,"errorMessage":"sprintf($this->language->__($translationKey), ...$values)","messagePattern":"sprintf\\(\\$this->language->__\\(\\$translationKey\\), \\.\\.\\.\\$values\\)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"app/Domain/Oidc/Services/Oidc.php","lineNumber":643,"sourceCode":"    {\n        $storedState = (string) session('oidc.state');\n        session()->forget('oidc.state');\n\n        return $storedState !== '' && hash_equals($storedState, $state);\n    }\n\n    private function decodeBase64Url(string $value): string\n    {\n        return base64_decode(strtr($value, '-_', '+/'));\n    }\n\n    /**\n     * @throws HttpResponseException\n     */\n    private function displayError(string $translationKey, string ...$values): void\n    {\n\n        throw new \\RuntimeException(sprintf($this->language->__($translationKey), ...$values));\n    }\n}\n","sourceCodeStart":625,"sourceCodeEnd":646,"githubUrl":"https://github.com/Leantime/leantime/blob/9a9f49f1008f4782b30f6723c54228f4f992e636/app/Domain/Oidc/Services/Oidc.php#L625-L646","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"; before (custom de-DE.ini override - sprintf() gets 0 placeholders but 2 values)\noidc.error.providerMismatch=\"Der Provider stimmt nicht mit der lokalen Einstellung überein\"\n\n; after - exactly two %s, matching displayError('oidc.error.providerMismatch', $iss, $providerUrl)\noidc.error.providerMismatch=\"Der zurückgegebene Provider %s stimmt nicht mit der lokalen Einstellung %s überein\"","handlingStrategy":"validation","validationCode":"// before translating/interpolating, ensure the translation keeps one %s per value\n$translated = $this->language->__($translationKey);\nif (substr_count($translated, '%') !== count($values)) {\n    Log::warning(\"OIDC translation placeholder mismatch for {$translationKey}\");\n    $translated = implode(' ', array_merge([$translationKey], $values)); // safe, specifier-free fallback\n}\nthrow new \\RuntimeException(sprintf($translated, ...$values));","typeGuard":"/** True when a translation string's specifier count matches the values about to be passed. */\nfunction translationPlaceholdersMatch(string $translated, int $valueCount): bool\n{\n    return substr_count($translated, '%s') === $valueCount\n        && substr_count($translated, '%') === $valueCount; // no stray % or exotic specifiers\n}","tryCatchPattern":"try {\n    $oidc->login();\n} catch (\\RuntimeException $e) {\n    // intended OIDC validation failures arrive here, already localized\n    return redirect('/login')->withErrors($e->getMessage());\n} catch (\\ValueError $e) {\n    // sprintf() itself failed: a language INI override has mismatched %s placeholders\n    Log::error('OIDC translation defect: '.$e->getMessage());\n    return redirect('/login')->withErrors('Login provider misconfigured. Contact the admin.');\n}","preventionTips":["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."],"tags":["oidc","i18n","sprintf","configuration"],"backgroundTag":"oidc-issuer-mismatch","analyzedSha":"9a9f49f1008f4782b30f6723c54228f4f992e636","analyzedAt":"2026-08-21T02:37:38.966Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}