Authentication
The AsyncSelect component supports custom headers and internal authentication for secure API requests.
Quick Start: Enable Internal Auth Globally
Enable internal authentication globally so all AsyncSelect components automatically authenticate requests to internal endpoints:
Step 1: Generate Secret
php artisan async-select:generate-secret
This automatically adds ASYNC_SELECT_INTERNAL_SECRET to your .env file.
Step 2: Enable Global Configuration
Edit config/async-select.php:
return [
'use_internal_auth' => env('ASYNC_SELECT_USE_INTERNAL_AUTH', true),
// ... other config
];
Or set in .env:
ASYNC_SELECT_USE_INTERNAL_AUTH=true
Step 3: Apply async-auth Middleware to Routes
Important: Middleware Required for Authentication
You MUST apply the async-auth middleware to your API routes if you want authentication to work. Without the middleware, internal authentication tokens will not be verified and users will not be authenticated.
The package automatically registers the async-auth middleware globally. You can use it instead of the regular auth middleware:
Route::middleware(['async-auth'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
For authenticated endpoints, always apply the middleware:
// ✅ Correct - Middleware applied
Route::middleware(['async-auth'])->get('/api/users/search', ...);
// ❌ Wrong - No middleware, authentication won't work
Route::get('/api/users/search', ...);
The async-auth middleware:
- Works exactly like
authmiddleware when no internal header is present - Automatically handles internal authentication when
X-Internal-Userheader is present - Supports all guard specifications:
async-auth:web,async-auth:sanctum, etc. - Supports additional flags:
async-auth:web,persistfor session persistence
Step 5: Use Component
No need to pass use-internal-auth - it's enabled globally!
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
placeholder="Search users..."
/>
All AsyncSelect components will automatically use internal authentication!
See complete example with controller →
Custom Headers
Pass custom headers (e.g., for authentication) with HTTP requests to your endpoints.
Basic Usage
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:headers="[
'Authorization' => 'Bearer ' . $token,
'X-Custom-Header' => 'custom-value'
]"
/>
With Livewire Properties
class UserSelector extends Component
{
public $userId;
public $apiToken;
public function mount()
{
$this->apiToken = auth()->user()->api_token;
}
public function render()
{
return view('livewire.user-selector');
}
}
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:headers="[
'Authorization' => 'Bearer ' . $this->apiToken,
'X-API-Version' => 'v2'
]"
/>
Dynamic Headers
Headers can be updated dynamically:
public function updatedApiToken()
{
// Headers will be automatically updated on next request
$this->dispatch('$refresh');
}
Internal Authentication
For requests to endpoints on the same domain, you can use internal authentication. This automatically generates signed tokens that authenticate the current user without requiring session cookies or API tokens.
Setup
1. Generate Secret
Run the artisan command to generate a secure secret:
php artisan async-select:generate-secret
This will:
- Generate a base64-encoded secret
- Add
ASYNC_SELECT_INTERNAL_SECRETto your.envfile - Overwrite existing secret if
--forceflag is used
Manual Setup:
If you prefer to set it manually:
# Generate a secret
php -r "echo base64_encode(random_bytes(32));"
# Add to .env
ASYNC_SELECT_INTERNAL_SECRET=your-generated-secret-here
2. Enable Internal Auth
You can enable internal authentication in two ways:
Option 1: Per-Component (Recommended for testing)
Enable internal authentication on individual components:
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:use-internal-auth="true"
/>
Option 2: Global Configuration (Recommended for production)
Enable internal authentication globally in config/async-select.php:
return [
'use_internal_auth' => env('ASYNC_SELECT_USE_INTERNAL_AUTH', true),
// ... other config
];
Or via environment variable:
ASYNC_SELECT_USE_INTERNAL_AUTH=true
When enabled globally, all AsyncSelect components will automatically use internal authentication for internal endpoints. You can still override it per-component:
<!-- Uses config (e.g., enabled globally) -->
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
/>
<!-- Overrides config (disables for this component) -->
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:use-internal-auth="false"
/>
When use-internal-auth is enabled:
- The component automatically generates a signed token for the authenticated user
- The token is sent in the
X-Internal-Userheader - The
Authorizationheader is automatically removed to avoid conflicts - Only works for internal endpoints (same domain)
- Works seamlessly with all AsyncSelect components when enabled globally
How It Works
Token Generation: When
use-internal-authistrueand the endpoint is internal, the component generates a signed token containing:- User ID
- Request method, path, and host (request binding)
- Expiry time (60 seconds)
- Nonce (prevents replay attacks)
Token Transmission: The token is sent in the
X-Internal-UserheaderToken Verification: Your endpoint middleware verifies the token and authenticates the user
Middleware Setup
The package automatically registers the async-auth middleware. No manual registration required!
Required for Authentication
You MUST apply the async-auth middleware to your routes for authentication to work. The middleware verifies internal authentication tokens and authenticates users.
The async-auth middleware is registered globally and can be used exactly like the auth middleware:
// ✅ Apply middleware for authenticated routes
Route::middleware(['async-auth'])->get('/api/users/search', function () {
// User is automatically authenticated via internal auth token (if header present)
// Or via normal authentication (if no header)
$user = auth()->user();
return response()->json([
'data' => User::where('name', 'like', "%{$request->get('search')}%")
->get()
->map(fn($u) => [
'value' => $u->id,
'label' => $u->name,
])
]);
});
Without the middleware, authentication will not work:
// ❌ No middleware - authentication won't work
Route::get('/api/users/search', function () {
// auth()->user() will be null even if X-Internal-User header is present
});
Applying Middleware to Routes
Important
Always apply the async-auth middleware to routes that require authentication. Without it, the internal authentication token will not be verified and users will not be authenticated.
Single Route
use Illuminate\Support\Facades\Route;
// ✅ Apply middleware for authentication
Route::middleware(['async-auth'])->get('/api/users/search', function () {
$user = auth()->user(); // Now authenticated
return response()->json([
'data' => User::where('name', 'like', "%{$request->get('search')}%")
->get()
->map(fn($u) => [
'value' => $u->id,
'label' => $u->name,
])
]);
});
Route Groups
Route::middleware(['async-auth'])->prefix('api')->group(function () {
Route::get('/users/search', [UserController::class, 'search']);
Route::get('/users/selected', [UserController::class, 'selected']);
Route::get('/products/search', [ProductController::class, 'search']);
});
With Guard Specification
// Use with default guard (web)
Route::middleware(['async-auth'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
// Use with web guard
Route::middleware(['async-auth:web'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
// Use with Sanctum
Route::middleware(['async-auth:sanctum'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
// Use with API guard
Route::middleware(['async-auth:api'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
// Use with multiple guards (tries first, falls back to second)
Route::middleware(['async-auth:web,sanctum'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
// Use with custom guard
Route::middleware(['async-auth:admin'])->group(function () {
Route::get('/admin/users/search', [AdminController::class, 'search']);
});
With Session Persistence
// Persist login in session (requires web middleware group)
Route::middleware(['web', 'async-auth:web,persist'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
Tips
Note: The persist flag only works with session-based guards (like web). It stores the authentication in the session for subsequent requests.
Complete Example: Global Configuration
Here's a complete, step-by-step example of setting up internal authentication globally:
Step 1: Generate Secret
php artisan async-select:generate-secret
This adds ASYNC_SELECT_INTERNAL_SECRET to your .env file.
Step 2: Enable Global Configuration
Option A: Via Config File
Edit config/async-select.php:
return [
// Enable internal auth globally for all AsyncSelect components
'use_internal_auth' => env('ASYNC_SELECT_USE_INTERNAL_AUTH', true),
'internal' => [
'secret' => env('ASYNC_SELECT_INTERNAL_SECRET', ''),
// ... other internal config
],
];
Option B: Via Environment Variable
Add to your .env file:
ASYNC_SELECT_USE_INTERNAL_AUTH=true
ASYNC_SELECT_INTERNAL_SECRET=your-generated-secret-here
Step 3: Apply async-auth Middleware to Routes
The async-auth middleware is automatically registered by the package. Simply use it in your routes:
// routes/api.php or routes/web.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;
Route::middleware(['async-auth'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
Route::get('/api/users/selected', [UserController::class, 'selected']);
});
Note: The async-auth middleware works exactly like auth middleware, but also handles internal authentication automatically when the X-Internal-User header is present.
Step 5: Create Controller
// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function search(Request $request)
{
// User is automatically authenticated via internal auth
$user = auth()->user();
$userId = auth()->id();
$search = $request->get('search', '');
$users = User::query()
->when($search, function($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->limit(20)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
'email' => $user->email,
]);
return response()->json(['data' => $users]);
}
public function selected(Request $request)
{
// User is automatically authenticated
$selected = $request->get('selected', []);
$users = User::whereIn('id', (array) $selected)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
]);
return response()->json(['data' => $users]);
}
}
Step 6: Use Component
No need to pass use-internal-auth - it's enabled globally!
<!-- resources/views/livewire/user-selector.blade.php -->
<div>
<livewire:async-select
endpoint="/api/users/search"
selected-endpoint="/api/users/selected"
wire:model="userId"
placeholder="Search users..."
/>
@if($userId)
<p>Selected User ID: {{ $userId }}</p>
@endif
</div>
That's it! All AsyncSelect components will automatically use internal authentication for internal endpoints.
Example: Multiple Components
When enabled globally, all components automatically use internal auth:
<!-- All these components automatically use internal auth -->
<livewire:async-select endpoint="/api/users" wire:model="userId" />
<livewire:async-select endpoint="/api/products" wire:model="productId" />
<livewire:async-select endpoint="/api/categories" wire:model="categoryId" />
<livewire:async-select endpoint="/api/orders" wire:model="orderId" />
No need to pass :use-internal-auth="true" to each component!
Complete Example: Per-Component Configuration
If you prefer to enable internal auth only for specific components:
<!-- This component uses internal auth -->
<livewire:async-select
endpoint="/api/users/search"
selected-endpoint="/api/users/selected"
wire:model="userId"
:use-internal-auth="true"
placeholder="Search users..."
/>
<!-- This component does NOT use internal auth -->
<livewire:async-select
endpoint="/api/external/users"
wire:model="externalUserId"
placeholder="Search external users..."
/>
Using with Laravel Sanctum
Simply use async-auth:sanctum instead of auth:sanctum:
// API routes with Sanctum
Route::middleware(['async-auth:sanctum'])->prefix('api')->group(function () {
Route::get('/users/search', [UserController::class, 'search']);
Route::get('/products/search', [ProductController::class, 'search']);
});
The async-auth middleware automatically handles both internal authentication (when header present) and normal Sanctum authentication (when no header).
Example Controller:
class UserController extends Controller
{
public function search(Request $request)
{
// User is authenticated via Sanctum token OR internal auth
$user = auth()->user(); // Works with both methods
$search = $request->get('search', '');
$users = User::where('name', 'like', "%{$search}%")
->limit(20)
->get();
return response()->json(['data' => $users]);
}
}
Using with Laravel Web Auth
For web routes, use async-auth:web:
// Web routes with session-based auth
Route::middleware(['web', 'async-auth:web'])->group(function () {
Route::get('/dashboard/users/search', [UserController::class, 'search']);
});
With session persistence:
// Persist login in session (requires web middleware group)
Route::middleware(['web', 'async-auth:web,persist'])->group(function () {
Route::get('/api/users/search', [UserController::class, 'search']);
});
The persist flag ensures the user login is stored in the session, useful for web applications.
Using with API Guard
For API-only authentication:
Route::middleware(['async-auth:api'])->prefix('api')->group(function () {
Route::get('/users/search', [UserController::class, 'search']);
});
Using with Multiple Guards
Try multiple guards in order (falls back if first fails):
Route::middleware(['async-auth:web,sanctum'])->group(function () {
// Tries web guard first, then sanctum if web fails
Route::get('/api/users/search', [UserController::class, 'search']);
});
Using with Custom Guards
Works with any custom guard you've configured:
// Custom guard example
Route::middleware(['async-auth:admin'])->group(function () {
Route::get('/admin/users/search', [AdminController::class, 'search']);
});
Complete Examples
Example 1: API Routes with Sanctum
// routes/api.php
Route::middleware(['async-auth:sanctum'])->prefix('api')->group(function () {
Route::get('/users/search', function (Request $request) {
$users = User::where('name', 'like', "%{$request->get('search')}%")
->limit(20)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
]);
return response()->json(['data' => $users]);
});
});
Example 2: Web Routes with Session
// routes/web.php
Route::middleware(['web', 'async-auth:web'])->group(function () {
Route::get('/dashboard/users/search', function (Request $request) {
$users = User::where('name', 'like', "%{$request->get('search')}%")
->limit(20)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
]);
return response()->json(['data' => $users]);
});
});
Example 3: Mixed Authentication (Web + Sanctum)
// routes/api.php
Route::middleware(['async-auth:web,sanctum'])->prefix('api')->group(function () {
// Accepts both web session auth and Sanctum tokens
Route::get('/users/search', [UserController::class, 'search']);
});
Example 4: Admin Routes with Custom Guard
// routes/web.php
Route::middleware(['web', 'async-auth:admin'])->prefix('admin')->group(function () {
Route::get('/users/search', [AdminUserController::class, 'search']);
});
Security Features
The internal authentication system includes several security features:
- Request Binding: Tokens are bound to specific request attributes (method, path, host, body hash) to prevent token reuse
- Replay Protection: Each token includes a nonce that's cached to prevent replay attacks
- Short Expiry: Tokens expire after 60 seconds
- Key Rotation: Supports rotating keys without breaking existing tokens
Key Rotation
To rotate keys without breaking existing tokens:
- Generate a new secret:
php artisan async-select:generate-secret
- Update your
.env:
ASYNC_SELECT_INTERNAL_SECRET=new-secret-here
ASYNC_SELECT_INTERNAL_PREVIOUS_SECRET=old-secret-here
Tokens signed with either key will be accepted during the rotation period
After all old tokens expire, remove
ASYNC_SELECT_INTERNAL_PREVIOUS_SECRET
Configuration
All internal auth settings can be configured in config/async-select.php:
return [
/*
* Enable internal authentication globally for all AsyncSelect components.
* When enabled, all components will automatically use internal auth for
* internal endpoints. You can override this per-component if needed.
*/
'use_internal_auth' => env('ASYNC_SELECT_USE_INTERNAL_AUTH', false),
'internal' => [
'secret' => env('ASYNC_SELECT_INTERNAL_SECRET', ''),
'previous_secret' => env('ASYNC_SELECT_INTERNAL_PREVIOUS_SECRET', ''),
'nonce_ttl' => env('ASYNC_SELECT_INTERNAL_NONCE_TTL', 120),
'skew' => env('ASYNC_SELECT_INTERNAL_SKEW', 60),
],
];
Environment Variables:
# Enable internal auth globally
ASYNC_SELECT_USE_INTERNAL_AUTH=true
# Internal auth secret (required when enabled)
ASYNC_SELECT_INTERNAL_SECRET=your-generated-secret-here
# Optional: Previous secret for key rotation
ASYNC_SELECT_INTERNAL_PREVIOUS_SECRET=old-secret-here
# Optional: Nonce TTL (default: 120 seconds)
ASYNC_SELECT_INTERNAL_NONCE_TTL=120
# Optional: Time skew tolerance (default: 60 seconds)
ASYNC_SELECT_INTERNAL_SKEW=60
Global vs Per-Component Configuration
Global Configuration (Recommended)
Enable internal auth globally for all components:
// config/async-select.php
'use_internal_auth' => env('ASYNC_SELECT_USE_INTERNAL_AUTH', true),
Benefits:
- ✅ One configuration for all components
- ✅ Consistent authentication across your app
- ✅ Easy to enable/disable app-wide
- ✅ No need to pass
:use-internal-auth="true"to every component
Usage:
<!-- All components automatically use internal auth -->
<livewire:async-select endpoint="/api/users" wire:model="userId" />
<livewire:async-select endpoint="/api/products" wire:model="productId" />
<livewire:async-select endpoint="/api/categories" wire:model="categoryId" />
Per-Component Configuration
Enable internal auth only for specific components:
<!-- This component uses internal auth -->
<livewire:async-select
endpoint="/api/users"
wire:model="userId"
:use-internal-auth="true"
/>
<!-- This component does NOT use internal auth -->
<livewire:async-select
endpoint="/api/users"
wire:model="userId"
:use-internal-auth="false"
/>
Override Global Configuration
When internal auth is enabled globally, you can still disable it for specific components:
// config/async-select.php
'use_internal_auth' => true, // Enabled globally
<!-- Uses global config (internal auth enabled) -->
<livewire:async-select endpoint="/api/users" wire:model="userId" />
<!-- Overrides global config (internal auth disabled for this component) -->
<livewire:async-select
endpoint="/api/users"
wire:model="userId"
:use-internal-auth="false"
/>
Troubleshooting
Token Not Being Sent
- Ensure
use-internal-authis set totrue - Verify the endpoint is internal (same domain)
- Check that the user is authenticated
- Verify
ASYNC_SELECT_INTERNAL_SECRETis set in.env
Middleware Not Authenticating
- The
async-authmiddleware is automatically registered - no manual setup needed - Verify middleware is applied to the route:
Route::middleware(['async-auth']) - Check that the token is being sent in the
X-Internal-Userheader (if using internal auth) - Verify the secret matches between token generation and verification
- If no internal header is present,
async-authworks exactly likeauthmiddleware
Authorization Header Removed
When use-internal-auth is enabled, the Authorization header is automatically removed to avoid conflicts. If you need both, use internal auth for internal endpoints and custom headers for external endpoints.
Combining Headers and Internal Auth
You can use both custom headers and internal auth:
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:headers="[
'X-Custom-Header' => 'custom-value',
'X-API-Version' => 'v2'
]"
:use-internal-auth="true"
/>
The component will:
- Send custom headers (except
Authorizationwhich is removed) - Add the
X-Internal-Userheader with the internal auth token