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

  1. Create a new token with a later (or no) expiry date in the user's API Tokens settings and swap it into the client
  2. For long-lived integrations, issue tokens without an expiry where policy allows, and rotate them on a schedule
  3. Add monitoring to alert before token expiry dates
  4. 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

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


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/e91a34a95fa141cf. Report an issue: GitHub.