{"record":{"id":"799bb631466c7191","repo":"roundcube/roundcubemail","slug":"failed-to-validate-jwt-expired-message","errorCode":null,"errorMessage":"Failed to validate JWT: expired message","messagePattern":"Failed to validate JWT: expired message","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"program/include/rcmail_oauth.php","lineNumber":462,"sourceCode":"        }\n\n        // FIXME depends on body type: ID, Logout, Bearer, Refresh,\n        if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {\n            throw new \\RuntimeException('Failed to validate JWT: invalid azp value');\n        } elseif (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) {\n            throw new \\RuntimeException('Failed to validate JWT: invalid aud value');\n        } elseif (!isset($body['azp']) && !isset($body['aud'])) {\n            throw new \\RuntimeException('Failed to validate JWT: missing aud/azp value');\n        }\n\n        // if defined in parameters, check that issuer match\n        if (isset($this->options['issuer']) && $body['iss'] !== $this->options['issuer']) {\n            throw new \\RuntimeException('Failed to validate JWT: issuer mismatch');\n        }\n\n        // check that token is not an outdated message\n        if (isset($body['exp']) && (time() > $body['exp'])) {\n            throw new \\RuntimeException('Failed to validate JWT: expired message');\n        }\n\n        $body['header'] = $header;\n\n        $this->log_debug('jwt: %s', json_encode($body));\n\n        return $body;\n    }\n\n    /**\n     * Compose a fully qualified redirect URI for auth requests\n     *\n     * @return string\n     */\n    public function get_redirect_uri()\n    {\n        $url = $this->rcmail->url([]);\n","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/include/rcmail_oauth.php#L444-L480","documentation":"jwt_decode() rejects tokens whose 'exp' claim is in the past. This is a standard JWT freshness check ensuring expired tokens (ID, logout, bearer, refresh) are never trusted.","triggerScenarios":"parse_tokens() -> jwt_decode() where isset($body['exp']) && time() > $body['exp'] — server clock ahead of the IDP's clock, long-idle session replaying an old token, or back-channel logout arriving after token expiry.","commonSituations":"Clock skew between web server and IDP; stored tokens reused after long inactivity; refresh flow failure leaving an expired access token in place; testing with a deliberately expired token.","solutions":["Trigger a fresh login/token refresh so a new unexpired token is obtained.","Synchronize server time via NTP to eliminate clock skew with the IDP.","Verify the token refresh/rotation logic in the OAuth client keeps tokens current.","Decode the token and compare 'exp' with current time to confirm the diagnosis."],"exampleFix":"// before: replaying stored token\n$token = $_SESSION['oauth_token']; // exp in the past\n// after: refresh or re-authenticate when expired\nif ($expiry <= time()) { $token = $oauth->refresh_token($refresh_token); }","handlingStrategy":"validation","validationCode":"$claims = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true);\nif (isset($claims['exp']) && time() >= $claims['exp']) { /* token expired: refresh before use */ }","typeGuard":"function tokenIsFresh(array $claims, int $leeway = 30): bool { return !isset($claims['exp']) || time() < ($claims['exp'] - $leeway); }","tryCatchPattern":"try { $payload = $oauth->jwt_decode($token); } catch (\\RuntimeException $e) { if (str_contains($e->getMessage(), 'expired')) { $oauth->request_access_token(); } }","preventionTips":["Keep server clocks NTP-synchronized with the IDP.","Refresh access tokens proactively before expiry.","Clear expired tokens from session storage instead of replaying them.","Log token expiry times to catch refresh-flow regressions early."],"tags":["oauth","jwt","expired","oidc"],"backgroundTag":"jwt-token-expired","analyzedSha":"4b54c2acfb54d5ee3d1c281ca7f143bed0dea804","analyzedAt":"2026-09-14T13:23:19.231Z","contentChangedAt":"2026-09-14T13:23:19.231Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}