BookStackApp/BookStack · error · ApiAuthException
errors.api_user_token_expired
Error message
errors.api_user_token_expired
What it means
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.
Source
Thrown at app/Api/ApiTokenGuard.php:132
/**
* Validate the given secret against the given token and ensure the token
* currently has access to the instance API.
*
* @throws ApiAuthException
*/
protected function validateToken(?ApiToken $token, string $secret): void
{
if ($token === null) {
throw new ApiAuthException(trans('errors.api_user_token_not_found'));
}
if (!Hash::check($secret, $token->secret)) {
throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
}
$now = Carbon::now();
if ($token->expires_at <= $now) {
throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
}
if (!$token->user->can(Permission::AccessApi)) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
}
/**
* {@inheritdoc}
*/
public function validate(array $credentials = []): bool
{
if (empty($credentials['id']) || empty($credentials['secret'])) {
return false;
}
$token = ApiToken::query()
->where('token_id', '=', $credentials['id'])View on GitHub (pinned to 18f8469a1c)
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
Example fix
// before $token = '12:secret'; // expires_at in the past // after // In BookStack UI: create new token with Expires At = blank (never) or future date $token = '15:newSecret';
Defensive patterns
Strategy: try-catch
Validate before calling
// Track expiry locally if your token record exposes it
if (isset($tokenMeta['expires_at']) && strtotime($tokenMeta['expires_at']) <= time()) {
throw new RuntimeException('BookStack API token expired; issue a new one before calling the API');
} Type guard
function isTokenUsable(?array $meta): bool {
return $meta !== null && (empty($meta['expires_at']) || strtotime($meta['expires_at']) > time());
} Try / catch
try {
$res = $client->get($url, ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 403) {
// expired (or permission denied): trigger token-rotation workflow
}
throw $e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unauthorized
- errors.email_confirmation_awaiting
- errors.api_no_authorization_found
- errors.api_bad_authorization_format
- errors.api_user_token_not_found
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/e91a34a95fa141cf.
Report an issue: GitHub.