{"record":{"id":"e91a34a95fa141cf","repo":"BookStackApp/BookStack","slug":"errors-api-user-token-expired","errorCode":null,"errorMessage":"errors.api_user_token_expired","messagePattern":"errors\\.api_user_token_expired","errorType":"exception","errorClass":"ApiAuthException","httpStatus":403,"severity":"error","filePath":"app/Api/ApiTokenGuard.php","lineNumber":132,"sourceCode":"    /**\n     * Validate the given secret against the given token and ensure the token\n     * currently has access to the instance API.\n     *\n     * @throws ApiAuthException\n     */\n    protected function validateToken(?ApiToken $token, string $secret): void\n    {\n        if ($token === null) {\n            throw new ApiAuthException(trans('errors.api_user_token_not_found'));\n        }\n\n        if (!Hash::check($secret, $token->secret)) {\n            throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));\n        }\n\n        $now = Carbon::now();\n        if ($token->expires_at <= $now) {\n            throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);\n        }\n\n        if (!$token->user->can(Permission::AccessApi)) {\n            throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function validate(array $credentials = []): bool\n    {\n        if (empty($credentials['id']) || empty($credentials['secret'])) {\n            return false;\n        }\n\n        $token = ApiToken::query()\n            ->where('token_id', '=', $credentials['id'])","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Api/ApiTokenGuard.php#L114-L150","documentation":"ApiTokenGuard::validateToken throws ApiAuthException('errors.api_user_token_expired') with status 403 when $token->expires_at <= Carbon::now(). BookStack API tokens can carry an expiry date; once passed, the token is refused even though the id and secret are correct. This is an intentional credential-lifetime control.","triggerScenarios":"Using a token whose expiry date set at creation has passed; long-running integrations that never refresh tokens; tokens created with short expiries for testing and later reused in production.","commonSituations":"Scheduled jobs failing after months of inactivity; test tokens with a 1-week expiry promoted into production configs; organizations enforcing expiry policies on all API tokens.","solutions":["Create a new token with a later (or no) expiry date in the user's API Tokens settings and swap it into the client","For long-lived integrations, issue tokens without an expiry where policy allows, and rotate them on a schedule","Add monitoring to alert before token expiry dates","Store the expiry alongside the credential in your secrets manager and fail early with a clear message"],"exampleFix":"// before\n$token = '12:secret'; // expires_at in the past\n// after\n// In BookStack UI: create new token with Expires At = blank (never) or future date\n$token = '15:newSecret';","handlingStrategy":"try-catch","validationCode":"// Track expiry locally if your token record exposes it\nif (isset($tokenMeta['expires_at']) && strtotime($tokenMeta['expires_at']) <= time()) {\n    throw new RuntimeException('BookStack API token expired; issue a new one before calling the API');\n}","typeGuard":"function isTokenUsable(?array $meta): bool {\n    return $meta !== null && (empty($meta['expires_at']) || strtotime($meta['expires_at']) > time());\n}","tryCatchPattern":"try {\n    $res = $client->get($url, ['headers' => ['Authorization' => \"Token {$id}:{$secret}\"]]);\n} catch (ClientException $e) {\n    if ($e->getResponse()->getStatusCode() === 403) {\n        // expired (or permission denied): trigger token-rotation workflow\n    }\n    throw $e;\n}","preventionTips":["Create integration tokens without an expiry unless policy forbids it","Set calendar/monitoring alerts ahead of any token expiry date","Rotate tokens on a schedule rather than waiting for 403s","Keep a runbook for re-issuing tokens for each integration"],"tags":["api","authentication","token-expiry"],"backgroundTag":"api-token-expired","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}