PHPackages                             licorice19/laravel-api-key - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. licorice19/laravel-api-key

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

licorice19/laravel-api-key
==========================

Laravel package for API key authentication

0.1.0(1mo ago)02↓100%MITPHPPHP ^8.1CI passing

Since Mar 19Pushed 1mo agoCompare

[ Source](https://github.com/licorice19/laravel-api-key)[ Packagist](https://packagist.org/packages/licorice19/laravel-api-key)[ RSS](/packages/licorice19-laravel-api-key/feed)WikiDiscussions master Synced 1mo ago

READMEChangelogDependencies (6)Versions (3)Used By (0)

[![Pest Tests](https://github.com/licorice19/laravel-api-key/actions/workflows/tests.yml/badge.svg)](https://github.com/licorice19/laravel-api-key/actions/workflows/tests.yml)

API Key Authentication for Laravel
==================================

[](#api-key-authentication-for-laravel)

Simple and reliable API key authentication for internal tools and B2B integrations — no user binding, no overhead.

Installation
------------

[](#installation)

```
composer require licorice19/api-key

php artisan vendor:publish --tag=api-key-config
php artisan vendor:publish --tag=api-key-migrations
php artisan migrate
```

Quick Start
-----------

[](#quick-start)

Create your first API key:

```
# Save the key — the plain key won't be shown again
php artisan api-key:create "My App"
```

Protect your routes:

```
Route::middleware('api-key')->group(function () {
    Route::get('/data', DataController::class);
});
```

Clients can pass the key in either format:

```
X-API-Key: your-api-key
Authorization: Bearer your-api-key

```

Usage Examples
--------------

[](#usage-examples)

### Creating an API Key

[](#creating-an-api-key)

**Via Artisan CLI:**

```
# Without expiration
php artisan api-key:create "Partner Integration"

# With expiration date
php artisan api-key:create "Temporary Access" --expires="2026-12-31 23:59:59"

# With rate limit (100 requests per 60 seconds)
php artisan api-key:create "Limited Partner" --rate-limit=100 --rate-period=60
```

**Programmatically:**

```
use Licorice19\ApiKey\Models\ApiKey;
use Licorice19\ApiKey\Services\ApiKeyService;

// Via model
$result = ApiKey::createKey('Partner Integration', new \DateTime('2026-12-31'));
$key = $result['key']; // Store this — it won't be shown again!

// Via service with rate limit
$service = app(ApiKeyService::class);
$result = $service->createKey('Limited Partner', null, 100, 60); // 100 req / 60 sec
```

### Protecting Routes

[](#protecting-routes)

```
// Protect a group of routes
Route::middleware('api-key')->group(function () {
    Route::get('/data', [DataController::class, 'index']);
    Route::post('/webhook', [WebhookController::class, 'handle']);
});

// Protect a single route
Route::get('/internal/stats', [StatsController::class, 'index'])
    ->middleware('api-key');
```

### Accessing the API Key in a Controller

[](#accessing-the-api-key-in-a-controller)

```
public function index(Request $request)
{
    $apiKey = $request->attributes->get('api_key');

    return response()->json([
        'key_name' => $apiKey->name,
        'last_used' => $apiKey->last_used_at,
    ]);
}
```

### Testing with cURL

[](#testing-with-curl)

```
# Using X-API-Key header
curl -H "X-API-Key: your-api-key-here" \
     https://your-app.com/api/data

# Using Authorization Bearer
curl -H "Authorization: Bearer your-api-key-here" \
     https://your-app.com/api/data

# POST request
curl -X POST \
     -H "X-API-Key: your-api-key-here" \
     -H "Content-Type: application/json" \
     -d '{"event": "test"}' \
     https://your-app.com/api/webhook

# Missing key → 401
# {"error": "API key is required"}

# Invalid key → 401
# {"error": "Invalid API key"}
```

Key Management
--------------

[](#key-management)

```
php artisan api-key:create        # Create a new key
php artisan api-key:list          # List all keys
php artisan api-key:revoke {id}   # Deactivate a key
php artisan api-key:activate {id} # Reactivate a key
php artisan api-key:delete {id}   # Delete a key
```

Or manage keys programmatically:

```
$service = app(ApiKeyService::class);

$result = $service->createKey('Partner Integration', new \DateTime('2026-12-31'));
$keyId = $result['model']->id;

$service->revokeById($keyId);
$service->activateById($keyId);
$service->deleteById($keyId);
```

Rate Limiting
-------------

[](#rate-limiting)

Each API key can have its own rate limit. When exceeded, the API returns `429 Too Many Requests`.

### Configure Rate Limit per Key

[](#configure-rate-limit-per-key)

```
# 100 requests per 60 seconds
php artisan api-key:create "Limited Partner" --rate-limit=100 --rate-period=60

# 1000 requests per hour
php artisan api-key:create "Hourly Limited" --rate-limit=1000 --rate-period=3600
```

### Response Headers

[](#response-headers)

When rate limiting is enabled, responses include:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95

```

### Rate Limit Exceeded Response

[](#rate-limit-exceeded-response)

```
{
    "error": "Rate limit exceeded"
}
```

Rate limit information is available via response headers only:

- `X-RateLimit-Limit` — maximum requests
- `X-RateLimit-Remaining` — remaining requests
- `X-RateLimit-Reset` — reset time in seconds

### Disable Rate Limit for a Key

[](#disable-rate-limit-for-a-key)

Simply omit `--rate-limit` option or set `rate_limit` to `null`:

```
$result = $service->createKey('Unlimited Key'); // No rate limit
```

Tags
----

[](#tags)

Tags allow you to categorize API keys and restrict access to specific routes based on key category.

### Create Key with Tag

[](#create-key-with-tag)

**Via Artisan CLI:**

```
# Create key with custom tag
php artisan api-key:create "Admin Service" --tag=admin

# Create key with tag and rate limit
php artisan api-key:create "API Partner" --tag=api --rate-limit=1000
```

**Programmatically:**

```
use Licorice19\ApiKey\Models\ApiKey;

// Create key with tag
$result = ApiKey::createKey('Admin Service', null, null, null, 'admin');
$key = $result['key'];

// Via service
$service = app(ApiKeyService::class);
$result = $service->createKey('API Partner', null, 100, 60, 'api');
```

### Protect Routes by Tag

[](#protect-routes-by-tag)

Use the `api-key.tag` middleware to restrict access to specific key categories:

```
// Only keys with "admin" tag can access these routes
Route::middleware(['api-key', 'api-key.tag:admin'])->group(function () {
    Route::get('/admin/stats', [AdminController::class, 'stats']);
    Route::post('/admin/config', [AdminController::class, 'config']);
});

// Keys with "api" OR "internal" tag can access
Route::middleware(['api-key', 'api-key.tag:api,internal'])->group(function () {
    Route::get('/data', [DataController::class, 'index']);
});

// Multiple tags as separate arguments (same as comma-separated)
Route::middleware(['api-key', 'api-key.tag:admin,api'])->group(function () {
    // Accessible by admin OR api keys
});
```

### Tag Middleware Responses

[](#tag-middleware-responses)

ScenarioStatusResponseKey tag matches200Normal responseKey tag doesn't match403`{"error": "Access denied"}`No API key in request401`{"error": "Unauthorized"}`### Filter Keys by Tag

[](#filter-keys-by-tag)

```
# List only keys with specific tag
php artisan api-key:list --tag=admin

# List keys with default tag
php artisan api-key:list --tag=default
```

### Check Key Tag Programmatically

[](#check-key-tag-programmatically)

```
$apiKey = $request->attributes->get('api_key');

// Check single tag
if ($apiKey->hasTag('admin')) {
    // Key has admin tag
}

// Check multiple tags (returns true if ANY matches)
if ($apiKey->hasTag(['admin', 'api'])) {
    // Key has admin OR api tag
}
```

### Default Tag

[](#default-tag)

When creating a key without specifying a tag, the default tag is used:

```
# Creates key with "default" tag
php artisan api-key:create "My App"
```

Configure the default tag in `config/api-key.php`:

```
return [
    'default_tag' => 'default',  // Default tag for new keys
];
```

Configuration
-------------

[](#configuration)

`config/api-key.php`:

```
return [
    'header_name' => 'X-API-Key',       // Request header name
    'cache_ttl'   => 300,               // Cache duration in seconds
    'rate_limit'  => null,              // Default rate limit (null = unlimited)
    'rate_limit_period' => 60,          // Default rate limit period in seconds
    'default_tag' => 'default',         // Default tag for new keys
    'last_used_probability' => 100,     // Probability of updating last_used_at (0-100)
];
```

Programmatic API Reference
--------------------------

[](#programmatic-api-reference)

### ApiKeyService Methods

[](#apikeyservice-methods)

```
use Licorice19\ApiKey\Services\ApiKeyService;

$service = app(ApiKeyService::class);
```

#### Key Creation

[](#key-creation)

```
// Create key without expiration
$result = $service->createKey('Partner Integration');
$key = $result['key'];       // Plain key (save it — won't be shown again!)
$model = $result['model'];    // ApiKey model instance

// Create key with expiration
$result = $service->createKey('Temporary Access', new \DateTime('2026-12-31'));

// Create key with rate limit
$result = $service->createKey('Limited', null, 100, 60); // 100 req / 60 sec
```

#### Key Validation &amp; Retrieval

[](#key-validation--retrieval)

```
// Validate a key
$isValid = $service->validateKey($plainKey);  // Returns bool

// Get key model by plain key
$model = $service->getKeyModel($plainKey);     // Returns ApiKey|null

// Find key by ID
$model = $service->findById($id);              // Returns ApiKey|null

// Get all active keys
$activeKeys = $service->getActiveKeys();       // Returns Collection

// Get all keys
$allKeys = $service->getAllKeys();             // Returns Collection
```

#### Key Management by ID

[](#key-management-by-id)

```
// Revoke (deactivate) by ID
$success = $service->revokeById($id);          // Returns bool

// Activate by ID
$success = $service->activateById($id);        // Returns bool

// Delete by ID
$success = $service->deleteById($id);          // Returns bool
```

#### Request Handling

[](#request-handling)

```
// Extract key from request
$plainKey = $service->extractKeyFromRequest($request);  // Returns string|null

// Update last used timestamp
$service->touchLastUsed($plainKey);            // Returns void
```

### ApiKey Model Methods

[](#apikey-model-methods)

```
use Licorice19\ApiKey\Models\ApiKey;

// Create key
$result = ApiKey::createKey('Partner Integration', new \DateTime('2026-12-31'));

// Create key with rate limit
$result = ApiKey::createKey('Limited', null, 100, 60);

// Find by hash with caching (internal use — hash is SHA-256 of plain key)
$keyHash = hash('sha256', $plainKey);
$apiKey = ApiKey::findByHashCached($keyHash, $cacheTtl = 300);

// Get active keys
$activeKeys = ApiKey::getActive();

// Instance methods
$apiKey->isValid();           // Check if key is valid (active + not expired)
$apiKey->activate();          // Activate key
$apiKey->revoke();            // Revoke (deactivate) key
$apiKey->touchLastUsed();     // Update last_used_at

// Rate limiting
$apiKey->hasRateLimit();      // Check if rate limit is enabled
$apiKey->getRateLimit();      // Get rate limit value
$apiKey->getRateLimitPeriod();// Get rate limit period
$apiKey->checkRateLimit();    // Check and increment counter
$apiKey->getRateLimitUsage(); // Get current usage
$apiKey->resetRateLimit();    // Reset counter

// Tags
$apiKey->hasTag('admin');     // Check if key has specific tag
$apiKey->hasTag(['admin', 'api']); // Check if key has any of the tags
$apiKey->tag;                 // Get key's tag value
```

---

Frequently Asked Questions
--------------------------

[](#frequently-asked-questions)

### Does cache invalidate immediately on key revocation?

[](#does-cache-invalidate-immediately-on-key-revocation)

Yes. Cache is cleared immediately via `Cache::forget()` when a key is revoked, activated, or deleted. No stale cache entries.

### Which Laravel versions are supported?

[](#which-laravel-versions-are-supported)

Laravel 10.x, 11.x, 12.x, and 13.x are fully supported. PHP 8.1+ is required.

### Is this compatible with Laravel Sanctum?

[](#is-this-compatible-with-laravel-sanctum)

Yes. These packages serve different purposes:

- **Sanctum**: User authentication (personal access tokens)
- **api-key**: Service-to-service authentication (no user binding)

You can use both in the same application for different use cases.

### What happens when a key expires?

[](#what-happens-when-a-key-expires)

Expired keys return `401 Unauthorized` with `{"error": "Unauthorized"}`. The key remains in the database but is considered invalid.

### How should I rotate keys without downtime?

[](#how-should-i-rotate-keys-without-downtime)

1. Create a new key: `$service->createKey('New Key')`
2. Distribute the new key to your client
3. Revoke the old key: `$service->revokeById($oldKeyId)`
4. No requests will fail — clients switch seamlessly

### Does updating `last_used_at` create database load?

[](#does-updating-last_used_at-create-database-load)

By default, `touchLastUsed()` writes to the database on every authenticated request. For high-traffic APIs, use probabilistic updates:

```
// config/api-key.php
'last_used_probability' => 5,  // Update on ~5% of requests
```

Configuration options:

- `100` — Update on every request (default, exact tracking)
- `5` — Update on ~5% of requests (recommended for high-traffic APIs)
- `0` — Disable automatic updates entirely

With `last_used_probability => 5`, you get approximate tracking with 20x less database writes. The `last_used_at` timestamp will be within a few minutes of actual usage — sufficient for auditing and debugging.

### Can I restrict keys by IP or domain?

[](#can-i-restrict-keys-by-ip-or-domain)

Not built-in.

### Is rate limiting included?

[](#is-rate-limiting-included)

**Yes!** Each key can have its own rate limit. Use `--rate-limit` option when creating a key.

You can also combine with Laravel's throttle middleware for additional protection:

```
Route::middleware(['api-key', 'throttle:60,1'])->group(function () {
    // Your routes
});
```

### Does it work with Octane?

[](#does-it-work-with-octane)

Yes. The middleware is stateless and doesn't rely on persistent state between requests.

### Where should clients store API keys?

[](#where-should-clients-store-api-keys)

- **Never** commit keys to version control (`.gitignore` your `.env` files)
- **Development**: Use `.env` files (`API_KEY=your-key-here`)
- **Production**: Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
- **CI/CD**: Use built-in secrets (GitHub Actions secrets, GitLab CI variables)

---

Security Considerations
-----------------------

[](#security-considerations)

- **Hashing**: Keys are hashed with SHA-256 before storage
- **Plain keys**: Never stored, only shown once at creation
- **No encryption**: Use HTTPS for key transmission
- **No IP/domain restrictions**: Implement in your middleware if needed
- **Rate limiting**: Per-key rate limiting is built-in

---

Performance
-----------

[](#performance)

OperationCache BehaviorValidate keyCached for `cache_ttl` secondsCreate keyNo cache (key returned once — caching pointless)Revoke/DeleteCache cleared immediatelyRate limitCached for `rate_limit_period` secondsFor high-traffic APIs:

- Set `cache_ttl` to 300-3600 seconds
- Consider batching `last_used_at` updates

---

Features
--------

[](#features)

- Keys are stored as hashes — never in plain text
- Built-in caching for high-throughput APIs
- Optional expiration via `expires_at`
- Per-key rate limiting — each key can have its own limits
- Supports both `X-API-Key` and `Bearer` headers
- Full Artisan CLI for key lifecycle management
- No user binding — designed for service-to-service and B2B use cases

Use Cases
---------

[](#use-cases)

Perfect for:

- **B2B integrations** — issue keys to partners and external services ```
    php artisan api-key:create "Acme Corp" --rate-limit=1000
    ```
- **Internal APIs** — authenticate microservices and background workers ```
    Route::middleware('api-key')->post('/internal/jobs', JobController::class);
    ```
- **Webhooks &amp; automation** — secure inbound endpoints without user sessions ```
    Route::middleware('api-key')->post('/webhooks/stripe', WebhookController::class);
    ```

License
-------

[](#license)

MIT

###  Health Score

34

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

52d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a67a03f982f7a83df3e484e653a1b26d5fbdaf023e4f95b77a5ea327fbc024b6?d=identicon)[licorice19](/maintainers/licorice19)

---

Top Contributors

[![licorice19](https://avatars.githubusercontent.com/u/59661049?v=4)](https://github.com/licorice19 "licorice19 (9 commits)")

---

Tags

apilaravelauthAuthenticationapi-key

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/licorice19-laravel-api-key/health.svg)

```
[![Health](https://phpackages.com/badges/licorice19-laravel-api-key/health.svg)](https://phpackages.com/packages/licorice19-laravel-api-key)
```

###  Alternatives

[tymon/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

11.5k49.1M344](/packages/tymon-jwt-auth)[php-open-source-saver/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

8359.8M52](/packages/php-open-source-saver-jwt-auth)[auth0/login

Auth0 Laravel SDK. Straight-forward and tested methods for implementing authentication, and accessing Auth0's Management API endpoints.

2745.0M3](/packages/auth0-login)[hasinhayder/tyro

Tyro - The ultimate Authentication, Authorization, and Role &amp; Privilege Management solution for Laravel 12 &amp; 13

6712.1k2](/packages/hasinhayder-tyro)[benbjurstrom/cognito-jwt-guard

A laravel auth guard for JSON Web Tokens issued by Amazon AWS Cognito

1113.1k](/packages/benbjurstrom-cognito-jwt-guard)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
