BookStackApp/BookStack · error · ApiAuthException

errors.api_user_no_api_permission

Error message

errors.api_user_no_api_permission

What it means

ApiTokenGuard::validateToken throws ApiAuthException('errors.api_user_no_api_permission') with status 403 when $token->user->can(Permission::AccessApi) is false. The token itself is valid (exists, secret matches, not expired), but the owning user account lacks the 'Access the API' system permission, so all its API tokens are refused.

Source

Thrown at app/Api/ApiTokenGuard.php:136

     * @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'])
            ->with(['user'])->first();

        if ($token === null) {
            return false;

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. In BookStack admin, grant the user (or its role) the 'Access the API' permission
  2. If the user account is intentionally restricted, issue a token under a dedicated integration user with API access and minimal role permissions
  3. Audit role permission changes when API 403s start appearing
  4. Keep a dedicated service account for integrations so human permission changes don't break them

Example fix

// before
// user 'report-bot' role has no 'Access the API' permission -> all its tokens 403
// after
// Admin UI: Edit role 'bots' -> check 'Access the API' -> save; retry the request
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the service user has API access before deploying the integration
// Check in admin: the user's role has 'Access the API' permission enabled
// Optionally verify with a cheap authenticated call at startup:
$probe = $client->get('/api/books', ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
if ($probe->getStatusCode() !== 200) { throw new RuntimeException('API user lacks AccessApi permission'); }

Type guard

function apiProbeSucceeds(array $headers): bool {
    $code = (int) (probeBookstack('/api/books', $headers) ?? 500);
    return $code === 200;
}

Try / catch

try {
    $res = $client->get($url, ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() === 403) {
        // ask an admin to re-enable 'Access the API' for the token's user/role
    }
    throw $e;
}

Prevention

When it happens

Trigger: An admin revoked or never granted the 'Access the API' permission from the user's role/profile; the token owner's role was changed to one without API access; API access was disabled for a whole role during a security lockdown.

Common situations: Role refactors that silently drop API permission; offboarding processes that strip permissions while integrations still run under that user; new tokens created for a read-only role that lacks AccessApi.

Related errors


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