benjaminfavre/oauth2-http-client
Lightweight OAuth 2 decorator for Symfony HttpClient. Supports standard grant types, fetches and caches access tokens, injects them into requests, and can retry on token expiry. Minimal dependencies (Symfony Contracts + PHP JSON) and highly customizable auth steps.
Installation
composer require benjaminfavre/oauth2-http-client
Add to config/app.php under providers:
BenjaminFavre\OAuth2HttpClient\OAuth2HttpClientServiceProvider::class,
First Use Case: Basic OAuth2 Request
use BenjaminFavre\OAuth2HttpClient\OAuth2HttpClient;
$client = new OAuth2HttpClient(
new \Symfony\Contracts\HttpClient\HttpClientInterface(),
'your-client-id',
'your-client-secret',
'https://oauth-provider.com/token'
);
$response = $client->request('GET', 'https://api.example.com/endpoint');
Key Files to Review
config/oauth2.php (auto-generated after installation)src/OAuth2HttpClient.php (core class)src/OAuth2HttpClientServiceProvider.php (service binding)Token Management
// Cache tokens for 59 minutes (default)
$client = new OAuth2HttpClient(/* ... */, 3540); // 59m in seconds
// Force token refresh
$client->request('GET', '...', ['force_refresh' => true]);
Scopes and Headers
// Request with custom scopes
$response = $client->request('GET', '...', [
'scopes' => ['read:user', 'write:data']
]);
// Add custom headers (merged with Authorization)
$response = $client->request('GET', '...', [
'headers' => ['X-Custom-Header' => 'value']
]);
Integration with Laravel HTTP Client
use Illuminate\Support\Facades\Http;
$client = new OAuth2HttpClient(
Http::getDecorator(),
'client-id',
'client-secret',
'https://provider.com/token'
);
$response = $client->request('GET', 'https://api.example.com/data');
Custom Token Endpoint Logic
$client = new OAuth2HttpClient(/* ... */, null, null, null, function () {
return [
'access_token' => 'pre-generated-token',
'expires_in' => 3600,
'token_type' => 'Bearer'
];
});
AppServiceProvider:
$this->app->singleton(OAuth2HttpClient::class, function ($app) {
return new OAuth2HttpClient(
$app->make(\Symfony\Contracts\HttpClient\HttpClientInterface::class),
config('oauth2.client_id'),
config('oauth2.client_secret'),
config('oauth2.token_endpoint')
);
});
Http::macro() for reusable endpoints:
Http::macro('oauthGet', function ($url, $scopes = []) use ($client) {
return $client->request('GET', $url, ['scopes' => $scopes]);
});
Token Expiry Handling
$client->setTokenRefreshListener(function ($token) {
\Log::debug('Token refreshed', ['token' => $token]);
});
force_refresh or implement a mutex.Configuration Overrides
config/oauth2.php file is auto-generated but may not reflect all options. Override via:
$client = new OAuth2HttpClient(/* ... */, null, null, null, null, [
'grant_type' => 'client_credentials',
'custom_param' => 'value'
]);
Symfony HTTP Client Quirks
// ❌ Wrong: Double decorator
$client = new OAuth2HttpClient(Http::getDecorator()->getDecorator(), ...);
// ✅ Correct: Single decorator
$client = new OAuth2HttpClient(Http::getDecorator(), ...);
PKCE for Public Clients
league/oauth2-client alongside this package or implement a custom token endpoint handler.$client = new OAuth2HttpClient(
Http::withOptions(['debug' => true]),
...
);
$client->setTokenStorage(new \BenjaminFavre\OAuth2HttpClient\Storage\RedisTokenStorage());
Custom Token Storage
Implement \BenjaminFavre\OAuth2HttpClient\Storage\TokenStorageInterface:
class DatabaseTokenStorage implements TokenStorageInterface {
public function get(): ?array { /* ... */ }
public function save(array $token): void { /* ... */ }
public function delete(): void { /* ... */ }
}
Token Refresh Logic
Override the refreshToken() method in a subclass:
class CustomOAuth2Client extends OAuth2HttpClient {
protected function refreshToken(): array {
// Custom refresh logic (e.g., retry with exponential backoff)
return parent::refreshToken();
}
}
Event Listeners Hook into token events:
$client->onTokenRefresh(function ($token) {
// Log or broadcast token refresh
});
$client->onTokenExpiry(function () {
// Handle expiry (e.g., notify user)
});
How can I help you explore Laravel packages today?