BookStackApp/BookStack · error · ApiAuthException

Unauthorized

Error message

Unauthorized

What it means

ApiTokenGuard::authenticate() throws ApiAuthException('Unauthorized') when no earlier exception was recorded and the request's API token could not be resolved to an authorised user. It is the generic fallback for failed API token authentication (missing/unknown token, failed secret check) when a more specific exception was not already stored in lastAuthException.

Source

Thrown at app/Api/ApiTokenGuard.php:71

    /**
     * Determine if the current user is authenticated. If not, throw an exception.
     *
     * @throws ApiAuthException
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable
     */
    public function authenticate()
    {
        if (!is_null($user = $this->user())) {
            return $user;
        }

        if ($this->lastAuthException) {
            throw $this->lastAuthException;
        }

        throw new ApiAuthException('Unauthorized');
    }

    /**
     * Check the API token in the request and fetch a valid authorised user.
     *
     * @throws ApiAuthException
     */
    protected function getAuthorisedUserFromRequest(): Authenticatable
    {
        $authToken = trim($this->request->headers->get('Authorization', ''));
        $this->validateTokenHeaderValue($authToken);

        [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
        $token = ApiToken::query()
            ->where('token_id', '=', $id)
            ->with(['user'])->first();

        $this->validateToken($token, $secret);

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Verify the Authorization header uses the format 'Token id:secret' with values from the user's API token settings.
  2. Regenerate the token in the user profile API settings and update the client credentials.
  3. Confirm the request targets the same BookStack instance the token was created on.
  4. Catch ApiAuthException client-side and check the 401 response before retrying.

Example fix

// before
$response = $http->get($url, ['headers' => ['Authorization' => $apiKey]]);
// after
$authHeader = 'Token ' . $tokenId . ':' . $tokenSecret;
$response = $http->get($url, ['headers' => ['Authorization' => $authHeader]]);
if ($response->status() === 401) {
    // regenerate token and refresh credentials
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (empty($tokenId) || empty($tokenSecret)) {
    throw new InvalidArgumentException('API token id and secret are required.');
}

Try / catch

try {
    $response = $client->get($apiUrl);
} catch (ApiAuthException | ClientException $e) {
    if ($e->getCode() === 401) {
        // regenerate token / refresh credentials before retrying
    }
    throw $e;
}

Prevention

When it happens

Trigger: Any API request whose Authorization header token does not match a valid token row (unknown token id, wrong secret, nonexistent/deleted token/user), reaching the final throw in authenticate().

Common situations: Sending an API request without creating a token in the user's API settings; copying a token from the wrong environment/instance; token revoked or its owner deleted; typos in the token string; forgetting the 'Token ' prefix or secret part (though those usually raise the more specific errors 78/79).

Understand the failure class

Related errors


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