Skip to content

fix: enforce Sanctum API token abilities on Api controllers - #219

Merged
morcen merged 2 commits into
mainfrom
fix/issue-53-enforce-api-token-abilities
Sep 9, 2026
Merged

fix: enforce Sanctum API token abilities on Api controllers#219
morcen merged 2 commits into
mainfrom
fix/issue-53-enforce-api-token-abilities

Conversation

@morcen

@morcen morcen commented Sep 8, 2026

Copy link
Copy Markdown
Owner

What was broken

Jetstream's API-token feature (Features::api()) was recently enabled (#218), so users can now create personal access tokens and restrict them to specific abilities (create, read, update, delete) via the API Tokens UI. However, auth:sanctum only verifies that a token is valid — it never checks what the token is scoped to do. None of EndpointController, EventController, DeliveryController, or WebhookController (under app/Http/Controllers/Api) called tokenCan() or used any ability: middleware.

This meant a token a user deliberately restricted to "read" (e.g. handed to a third-party integration) silently retained full create/update/delete access anyway — it could create/update/delete endpoints and events, trigger arbitrary webhook deliveries, and read all delivery data, regardless of what abilities were selected in the UI.

What changed

  • Added a small authorizeAbility() helper on the base App\Http\Controllers\Controller that aborts with a 403 unless $request->user()->tokenCan($ability).
  • Called it at the top of every action across the four API controllers, mapping each action to the ability that already exists in the API Tokens UI:
    • index/show/statsread
    • storecreate
    • update/regenerateSecretupdate
    • destroydelete
    • WebhookController::triggercreate (creates new deliveries)
    • WebhookController::retryDeliveryupdate (mutates an existing delivery)

Session-authenticated requests (the Inertia dashboard, and tests using actingAs()) are unaffected: Sanctum wraps session-authenticated users in a TransientToken, whose can() always returns true, so this only restricts requests actually made with a real, ability-scoped personal access token.

  • Added tests/Feature/ApiTokenAbilityEnforcementTest.php covering that a read-only token is rejected on every mutating endpoint/event/delivery/webhook action, that a token with the right ability succeeds, and that session-authenticated requests are unaffected.
  • Updated ApiEventNameUniquenessScopeTest and DeliveryRetryRateLimitTest to use Sanctum::actingAs() for the specific cases where a test authenticates as a second user against a Sanctum-guarded route within the same test method. auth:sanctum's guard caches its resolved (and TransientToken-wrapped) user for the rest of the test process once it first authenticates, so a later plain actingAs() call for a different user object bypassed that wrapping — previously invisible because nothing checked abilities, now surfaced by this fix. This is Sanctum's own documented pattern for this exact test scenario.

Testing

  • vendor/bin/pint --dirty — clean
  • php artisan test — full suite passes (240 passed, 4 pre-existing skips, 0 failures)

Fixes #53

morcen and others added 2 commits September 8, 2026 19:56
auth:sanctum only verified that a token was valid, not what it was
scoped to do. A personal access token restricted to "read" in the
API Tokens UI still had full create/update/delete access, because no
controller ever called tokenCan()/checked the token's abilities.

Add an authorizeAbility() helper on the base Controller and call it
at the top of every action in EndpointController, EventController,
DeliveryController, and WebhookController, mapping each action to the
create/read/update/delete abilities already offered by the API Tokens
UI. Session-authenticated requests (the dashboard, and tests using
actingAs()) are unaffected, since Sanctum wraps them in a
TransientToken that allows every ability.

Also switch ApiEventNameUniquenessScopeTest and
DeliveryRetryRateLimitTest to Sanctum::actingAs() where a test
authenticates as a second user against a Sanctum-guarded route within
the same method: auth:sanctum's guard caches its resolved user for
the rest of the test process once it authenticates, so a later plain
actingAs() call for a different user bypasses Sanctum's own
token-wrapping and was incidentally relying on the missing ability
check to go unnoticed.

Fixes #53
@morcen
morcen merged commit b1901cc into main Sep 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sanctum API token abilities are never enforced anywhere, despite UI and tests implying they are a security boundary

2 participants