BookStackApp/BookStack · error · ApiAuthException
errors.api_incorrect_token_secret
Error message
errors.api_incorrect_token_secret
What it means
ApiTokenGuard::validateToken throws ApiAuthException('errors.api_incorrect_token_secret') when Hash::check fails: the supplied secret does not match the hashed secret stored for the found ApiToken. The token id was valid, but the credential half of the pair is wrong.
Solutions
- Regenerate or view the token in BookStack (secrets are only shown at creation, so regenerate if lost) and update the client
- Trim whitespace/newlines from the secret in environment variables or config
- Confirm the header order is 'Token <id>:<secret>', not '<secret>:<id>'
- Update all deployed clients/cron jobs after any token rotation
Example fix
// before
$secret = trim(env('BOOKSTACK_TOKEN_SECRET')); // actually mismatched old secret
// after
// Regenerate token in UI, then:
$secret = 'NewSecretFromTokenCreationScreen';
$headers = ['Authorization' => 'Token ' . $id . ':' . $secret]; Defensive patterns
Strategy: validation
Validate before calling
// Guard against common secret corruption before sending
$secret = trim($secret);
if (strlen($secret) < 16 || preg_match('/\s/', $secret)) {
throw new RuntimeException('BookStack token secret looks truncated or contains whitespace');
} Type guard
function looksLikeTokenSecret(?string $s): bool {
return is_string($s) && strlen($s) >= 16 && !preg_match('/\s/', $s);
} Try / catch
try {
$res = $client->get($url, ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 401) {
// secret mismatch: rotate the token via UI/admin and reload credentials
}
throw $e;
} Prevention
- Regenerate the token in the UI after any suspected leak and update all clients
- Avoid editing secrets by hand; load them from a secrets manager
- Watch for token rotations by other admins and rotate your client at the same time
- Never swap the id and secret halves in the header
When it happens
Trigger: Sending a stale or regenerated secret with a still-valid token id; truncating the secret (copy/paste cut); swapping the id and secret halves in the header; whitespace or newline appended to the secret in an env var.
Common situations: The token was regenerated in the UI (invalidating the old secret) but the client kept the old secret; secrets stored in .env files with trailing spaces or unescaped characters; multiple tokens and the wrong secret paired with an id.
Related errors
- errors.api_bad_authorization_format
- errors.api_cookie_auth_only_get
- errors.api_no_authorization_found
- errors.api_user_token_expired
- errors.api_user_token_not_found
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/bf949ffa888beac5.
Report an issue: GitHub.
Appendix: source
Thrown at app/Api/ApiTokenGuard.php:127
if (!str_contains($authToken, ':') || !str_starts_with($authToken, 'Token ')) {
throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
}
}
/**
* 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'])) {View on GitHub (pinned to 18f8469a1c)