Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Keys Laravel Package

savannabits/api-keys

A Laravel package for generating, managing, and authenticating API keys, with configurable models and middleware integration for protecting routes and tracking key-based access.

View on GitHub
Deep Wiki
Context7

Getting Started

v0.0.1 is the first public tag, so treat this as an early-adopter package: read the source before wiring it into anything user-facing. The typical bootstrap for a Laravel API-key package like this is short:

  1. Install

    composer require savannabits/api-keys
    

    Pin it exactly ("savannabits/api-keys": "0.0.1") — at 0.0.x every release can break.

  2. Publish and migrate

    php artisan vendor:publish --provider="Savannabits\ApiKeys\ApiKeysServiceProvider"
    php artisan migrate
    

    Look first at what lands in config/api-keys.php (table name, key length/prefix, hashing driver, header name, model class) and the generated migration. Those two files tell you more about the package's design than the README does.

  3. Attach the trait to whatever authenticatable model should own keys — usually App\Models\User:

    use Savannabits\ApiKeys\Concerns\HasApiKeys;
    
    class User extends Authenticatable
    {
        use HasApiKeys;
    }
    
  4. Issue your first key (the plaintext is returned once, at creation):

    $plain = $user->createApiKey('cli-test'); // name/label arg — check the signature
    // store $plain somewhere safe for the test, it will never be retrievable again
    
  5. Protect a route — either via the shipped middleware or a guard, depending on what the service provider registers:

    Route::middleware('auth.api-key')->get('/user', fn () => request()->user());
    

First real use case: a machine-to-machine endpoint (webhook receiver, mobile client, CI integration) where you want per-client revocable credentials without standing up Passport/Sanctum OAuth.

Implementation Patterns

One key per device, not per user. Give each consumer its own key row with a human-readable name ("mobile-ios", "zapier", "ci-runner"). Revocation then becomes a surgical operation instead of locking a user out everywhere.

Issue in a management controller, return plaintext once.

public function store(Request $request)
{
    $request->validate(['name' => ['required', 'string', 'max:64']]);

    $key = $request->user()->createApiKey($request->name);

    // Only chance to show it: flash to session, display, then gone.
    return back()->with('plain_text_key', $key->plainText ?? $key);
}

Never echo it in a JSON response that a browser or proxy might log; prefer a one-time flash view.

Middleware/guard at the route-group level. Group by consumer type so you can layer throttling and logging per group:

Route::prefix('api/v1')
    ->middleware(['auth.api-key', 'throttle:60,1'])
    ->group(base_path('routes/api-v1.php'));

If the package registers a guard driver instead, add it to config/auth.php under guards.api and use auth:api — mixing the middleware and guard styles in one app is a common source of "authenticated but $request->user() is null".

Abilities/scopes, if present. If the key model exposes an abilities array, check it in a thin custom middleware rather than sprinkling if statements through controllers. Keep the check in one place so adding a scope later is a one-line change.

Revocation and rotation as a first-class flow. Build a POST /api-keys/{key}/rotate that creates the new key, returns the plaintext, and only then deletes the old row — with a short grace window if your consumers can't hot-swap instantly:

DB::transaction(function () use ($user, $key) {
    $new = $user->createApiKey($key->name.' (rotated)');
    $key->update(['expires_at' => now()->addHours(24)]); // grace, don't delete yet
});

Housekeeping. If the package ships a prune/expire command, schedule it in routes/console.php or the kernel: expired keys that still authenticate are a silent liability.

Testing. Reset the guard between tests and hit routes with the header your config expects:

$plain = $user->createApiKey('test')->plainText ?? /* ... */;

$this->withHeader('Authorization', "Bearer {$plain}")
     ->getJson('/api/v1/user')
     ->assertOk();

Add one negative test per guard behaviour — missing header, malformed header, revoked key, expired key — because those are exactly the paths that regress between 0.0.x tags.

Sanctum/Passport coexistence. If the app already uses Sanctum for first-party SPAs, keep API-key auth on a separate guard and route group. Do not let both guards fall through on the same route: whichever resolves first wins and you get nondeterministic 401s.

Gotchas and Tips

  • Plaintext is shown once. Design your UI around it: a modal that can't be re-opened, plus a "regenerate" affordance. Users will lose the key, and there is no recovery by design — that's correct, don't add one.
  • Never $guarded = [] on the key model. Mass-assigning hashed_key, user_id, or expires_at from a request is a privilege-escalation bug. Check the shipped model's fillable list before you trust it.
  • Plaintext vs. hash comparison. If the package uses hash('sha256', ...) you get constant-time lookup by indexed hash; if it uses Hash::make() (bcrypt) it must iterate every candidate key — fine for 10 keys, painful for 10,000. Check the migration: an index on the lookup column is the tell.
  • php artisan config:cache will happily serve stale api-keys.php values after you edit the header name or table. Clear the cache in deploys, and remember production env vars aren't read until then.
  • Copying the published migration after it has already run leaves you with a duplicate create_api_keys_table. Guard it with Schema::hasTable() in local environments, or just don't publish twice.
  • last_used_at writes are a hidden write-per-request. On a busy endpoint that's a row update on every call and will show up as lock contention. Consider throttling the touch (e.g. only if older than 60s) or pushing it to a queued job.
  • Prefix your keys (sk_live_...) so secret scanners and git log grep can catch leaks. If the package doesn't do this at 0.0.1, wrap key generation in your own service rather than forking.
  • Timezone/expiry comparisons. expires_at stored in UTC but compared against now() in app timezone is the classic off-by-hours bug that makes a key work locally and 401 in production. Verify the cast and the comparison column.
  • request()->user() resolution. If auth succeeds but user() is null, the guard isn't setting the resolved user on the request. Middleware that authenticates must call auth()->setUser($user) (or Auth::guard(...)->setUser()) — worth confirming with a two-line test before assuming it's your code.
  • Extension points to look for: a bindable ApiKey model in config, a KeyGenerator/Hasher contract, and an event fired on key creation/revocation (ApiKeyCreated, ApiKeyRevoked). Those three make the package usable without forking; if they're missing at 0.0.1, wrap issuance in your own ApiKeyService so you have a seam to swap later.
  • Semver reality check. 0.0.1 means no BC promise and possibly no tests in CI. Read the tests/ directory and the CHANGELOG before adopting; if it's empty, budget an afternoon to write your own integration tests around the four guard paths above before shipping.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager