Keys and abilities

The Content API authenticates requests with Bearer keys minted from the panel. Each key carries a set of abilities that scope which discovered resources it may touch. Keys are hashed at rest; the plaintext is shown once.

Keys

Keys are created on the API settings page in the panel. ApiKey::mint() generates a random token, prefixes it with agk_, and stores only a SHA-256 hash plus a 12-character display prefix:

$plain = 'agk_'.bin2hex(random_bytes(24));
$key = ApiKey::create([
    'prefix'    => substr($plain, 0, 12),
    'key_hash'  => hash('sha256', $plain),
    'abilities' => $abilities ?? ContentApi::abilities(),
]);

The plaintext is returned to the caller exactly once and never stored. Lookups on each request re-hash the incoming Bearer token and match it against key_hash (ApiKey::findByPlain()). Store the plaintext in your secret manager immediately after minting.

Abilities

Every discovered resource exposes four abilities, shaped {prefix}:create|read|update|delete. The prefix is the basename of the resource slug (Str::of($class::getSlug())->basename()), so a BlogPostResource with slug blog-posts yields blog-posts:create, blog-posts:read, blog-posts:update, blog-posts:delete.

ApiKey::allows($ability) returns true if the ability is in the key's abilities array. A key with null abilities bypasses the ability layer — useful for a master key, but scope keys down in production.

Discovery

Abilities are not hand-written. ContentApi::discover($panel) walks $panel->getResources(), skips anything in the opt-out list, and builds the ability set automatically. Add a new Filament resource and its four abilities appear on the next key mint.

Opt-out covers the panel's own infrastructure resources: UserResource, RoleResource, MediaResource, plus any model that extends Authenticatable. ContentApi::except() lets you exclude more, and ContentApi::resource() registers a model that has no Filament resource under a chosen prefix.

Authentication flow

The content.api middleware stack resolves the request to a user before the ability check runs:

  1. Bearer agk_ key — hash lookup, then markUsed(), then authenticate as the key's owner.
  2. Env API_KEY — single shared secret, impersonates the user configured at content-api.key_user. Compared with hash_equals.
  3. Passport token — falls through to Laravel's api guard. Valid, but unscoped at the key layer; use a hashed agk_ key when you need per-resource abilities.

After auth, EnsureContentApiAbility calls ContentApi::ensureAbility($ability), which abort_unlesses 403 when the key lacks the required ability. Authorization then defers to the same Eloquent policies the panel uses.

Ability reference

Ability Allows
posts:create POST /api/v1/posts
posts:read GET /api/v1/posts, GET /api/v1/posts/{id}
posts:update PATCH /api/v1/posts/{id}
posts:delete DELETE /api/v1/posts/{id}

Mint and request flow

sequenceDiagram
    actor Editor
    participant Panel as API settings
    participant DB
    participant API as Content API
    participant Policy

    Editor->>Panel: Mint key (select abilities)
    Panel->>DB: store key_hash + abilities
    Panel-->>Editor: show plaintext once

    Editor->>API: Bearer agk_…
    API->>DB: hash compare (findByPlain)
    API->>API: markUsed() + authenticate as owner
    API->>API: EnsureContentApiAbility(prefix:action)
    API->>Policy: authorize (Eloquent policy)
    Policy-->>API: allowed
    API-->>Editor: 2xx + JSON

See also

Built by Qcentic