League OAuth2 Client
Authentication Library
League OAuth2 Client
Overview
League OAuth2 Client is a lightweight and extensible OAuth 2.0 client library for PHP applications that enables easy integration with major OAuth2 providers.
Details
League OAuth2 Client is an OAuth 2.0 client library for PHP developed by The PHP League. It provides a complete implementation of the OAuth 2.0 specification compliant with RFC 6749, supporting over 100 OAuth2 providers including Google, Facebook, GitHub, Slack, and Twitter. The core GenericProvider class implements standard OAuth2 flows and supports Authorization Code Grant, Client Credentials Grant, Refresh Token Grant, and Resource Owner Password Credentials Grant flows. It also supports PKCE (Proof Key for Code Exchange) to enhance security for mobile applications and SPAs (Single Page Applications). Provider-specific implementations are provided as independent packages, allowing for lightweight design where only necessary features need to be installed. Creating custom providers is also easy by extending the AbstractProvider class, enabling integration with proprietary OAuth2 services. The library follows PSR standards and integrates well with modern PHP frameworks and applications.
Pros and Cons
Pros
- Standards Compliant: Full RFC 6749 compliance with security standard implementations
- Rich Provider Support: Support for 100+ official and community providers
- Lightweight Design: Install only necessary providers
- Extensibility: Easy implementation of custom providers
- PSR Adapter: PSR-7 HTTP message interface support
- PKCE Support: Enhanced security for mobile and SPA applications
- Composer Management: Easy dependency management
Cons
- PHP Only: Cannot be used with languages other than PHP
- No Async Support: Only synchronous processing, no asynchronous request support
- Learning Curve: Requires understanding of OAuth2 specifications
- Provider Dependencies: Need to handle provider-specific specifications
- Error Handling: Complex provider-specific error handling
Key Links
- League OAuth2 Client Official Page
- League OAuth2 Client Official Documentation
- OAuth2 Provider List
- The PHP League
- PHP-FIG PSR-7
- RFC 6749 - OAuth 2.0 Specification
Code Examples
Hello World (Basic Google OAuth Authentication)
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\Google;
// Google OAuth2 provider configuration
$provider = new Google([
'clientId' => 'your-google-client-id',
'clientSecret' => 'your-google-client-secret',
'redirectUri' => 'https://your-app.com/callback',
]);
session_start();
if (!isset($_GET['code'])) {
// Generate authorization URL and redirect
$authUrl = $provider->getAuthorizationUrl([
'scope' => ['openid', 'profile', 'email']
]);
$_SESSION['oauth2state'] = $provider->getState();
header('Location: ' . $authUrl);
exit;
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
// CSRF attack protection
unset($_SESSION['oauth2state']);
exit('Invalid state');
} else {
try {
// Get access token
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
// Get user information
$user = $provider->getResourceOwner($token);
echo 'Hello ' . $user->getName() . '!';
echo '<br>Email: ' . $user->getEmail();
} catch (\Exception $e) {
exit('OAuth authentication failed: ' . $e->getMessage());
}
}
?>
Custom OAuth2 Implementation using GenericProvider
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\GenericProvider;
// Custom OAuth2 provider configuration
$provider = new GenericProvider([
'clientId' => 'your-client-id',
'clientSecret' => 'your-client-secret',
'redirectUri' => 'https://your-app.com/oauth/callback',
'urlAuthorize' => 'https://api.example.com/oauth/authorize',
'urlAccessToken' => 'https://api.example.com/oauth/token',
'urlResourceOwnerDetails' => 'https://api.example.com/user',
'scopes' => 'read write',
]);
class CustomOAuthHandler
{
private $provider;
public function __construct(GenericProvider $provider)
{
$this->provider = $provider;
}
public function initiateLogin(): string
{
session_start();
$authUrl = $this->provider->getAuthorizationUrl([
'scope' => ['read', 'write', 'admin']
]);
$_SESSION['oauth2state'] = $this->provider->getState();
return $authUrl;
}
public function handleCallback(): array
{
session_start();
if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
throw new \Exception('CSRF validation failed');
}
if (!isset($_GET['code'])) {
throw new \Exception('Authorization code not received');
}
try {
$token = $this->provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
$resourceOwner = $this->provider->getResourceOwner($token);
return [
'access_token' => $token->getToken(),
'refresh_token' => $token->getRefreshToken(),
'expires_at' => $token->getExpires(),
'user_data' => $resourceOwner->toArray()
];
} catch (\Exception $e) {
throw new \Exception('Token exchange failed: ' . $e->getMessage());
}
}
public function refreshToken(string $refreshToken): array
{
try {
$newToken = $this->provider->getAccessToken('refresh_token', [
'refresh_token' => $refreshToken
]);
return [
'access_token' => $newToken->getToken(),
'refresh_token' => $newToken->getRefreshToken(),
'expires_at' => $newToken->getExpires()
];
} catch (\Exception $e) {
throw new \Exception('Token refresh failed: ' . $e->getMessage());
}
}
}
// Usage example
$oauthHandler = new CustomOAuthHandler($provider);
if (!isset($_GET['code'])) {
$authUrl = $oauthHandler->initiateLogin();
header('Location: ' . $authUrl);
exit;
} else {
try {
$result = $oauthHandler->handleCallback();
echo 'Authentication successful!';
echo '<pre>' . print_r($result, true) . '</pre>';
} catch (\Exception $e) {
echo 'Error: ' . $e->getMessage();
}
}
?>
PKCE-Enabled OAuth2 Implementation
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\GenericProvider;
class PKCEOAuthClient
{
private $provider;
public function __construct()
{
$this->provider = new GenericProvider([
'clientId' => 'your-public-client-id',
'clientSecret' => '', // Not required for PKCE
'redirectUri' => 'https://your-app.com/oauth/callback',
'urlAuthorize' => 'https://api.example.com/oauth/authorize',
'urlAccessToken' => 'https://api.example.com/oauth/token',
'urlResourceOwnerDetails' => 'https://api.example.com/user',
'pkceMethod' => GenericProvider::PKCE_METHOD_S256,
]);
}
public function getAuthorizationUrl(): string
{
session_start();
$authUrl = $this->provider->getAuthorizationUrl([
'scope' => ['read', 'write']
]);
// Save PKCE code and state
$_SESSION['oauth2state'] = $this->provider->getState();
$_SESSION['oauth2pkceCode'] = $this->provider->getPkceCode();
return $authUrl;
}
public function exchangeCodeForToken(string $code): array
{
session_start();
// PKCE validation
if (empty($_SESSION['oauth2pkceCode'])) {
throw new \Exception('PKCE code not found in session');
}
// Restore PKCE code
$this->provider->setPkceCode($_SESSION['oauth2pkceCode']);
try {
$token = $this->provider->getAccessToken('authorization_code', [
'code' => $code
]);
// Clear PKCE information from session
unset($_SESSION['oauth2pkceCode']);
unset($_SESSION['oauth2state']);
return [
'access_token' => $token->getToken(),
'token_type' => 'Bearer',
'expires_in' => $token->getExpires() - time(),
'refresh_token' => $token->getRefreshToken(),
'scope' => implode(' ', $token->getValues()['scope'] ?? [])
];
} catch (\Exception $e) {
throw new \Exception('PKCE token exchange failed: ' . $e->getMessage());
}
}
}
// Usage example for SPA (Single Page Application)
$pkceClient = new PKCEOAuthClient();
if (!isset($_GET['code'])) {
$authUrl = $pkceClient->getAuthorizationUrl();
// For JavaScript processing
echo json_encode([
'auth_url' => $authUrl,
'message' => 'Redirect to this URL for authentication'
]);
} else {
try {
$tokens = $pkceClient->exchangeCodeForToken($_GET['code']);
echo json_encode([
'success' => true,
'tokens' => $tokens
]);
} catch (\Exception $e) {
http_response_code(400);
echo json_encode([
'error' => $e->getMessage()
]);
}
}
?>
Multi-Provider Integration System
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\Google;
use League\OAuth2\Client\Provider\Facebook;
use League\OAuth2\Client\Provider\Github;
class MultiProviderOAuthManager
{
private $providers = [];
private $config;
public function __construct(array $config)
{
$this->config = $config;
$this->initializeProviders();
}
private function initializeProviders(): void
{
// Google Provider
if (isset($this->config['google'])) {
$this->providers['google'] = new Google([
'clientId' => $this->config['google']['client_id'],
'clientSecret' => $this->config['google']['client_secret'],
'redirectUri' => $this->config['google']['redirect_uri'],
]);
}
// Facebook Provider
if (isset($this->config['facebook'])) {
$this->providers['facebook'] = new Facebook([
'clientId' => $this->config['facebook']['app_id'],
'clientSecret' => $this->config['facebook']['app_secret'],
'redirectUri' => $this->config['facebook']['redirect_uri'],
'graphApiVersion' => 'v19.0',
]);
}
// GitHub Provider
if (isset($this->config['github'])) {
$this->providers['github'] = new Github([
'clientId' => $this->config['github']['client_id'],
'clientSecret' => $this->config['github']['client_secret'],
'redirectUri' => $this->config['github']['redirect_uri'],
]);
}
}
public function getProvider(string $name)
{
if (!isset($this->providers[$name])) {
throw new \InvalidArgumentException("Provider '{$name}' not configured");
}
return $this->providers[$name];
}
public function getAuthorizationUrl(string $providerName, array $options = []): string
{
$provider = $this->getProvider($providerName);
session_start();
$authUrl = $provider->getAuthorizationUrl($options);
$_SESSION['oauth2state'] = $provider->getState();
$_SESSION['oauth2provider'] = $providerName;
return $authUrl;
}
public function handleCallback(): array
{
session_start();
if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
throw new \Exception('CSRF validation failed');
}
if (empty($_SESSION['oauth2provider'])) {
throw new \Exception('Provider information missing from session');
}
$providerName = $_SESSION['oauth2provider'];
$provider = $this->getProvider($providerName);
try {
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
$resourceOwner = $provider->getResourceOwner($token);
// Convert provider-specific information to unified format
$userData = $this->normalizeUserData($providerName, $resourceOwner);
return [
'provider' => $providerName,
'access_token' => $token->getToken(),
'refresh_token' => $token->getRefreshToken(),
'expires_at' => $token->getExpires(),
'user' => $userData
];
} catch (\Exception $e) {
throw new \Exception("OAuth callback failed for {$providerName}: " . $e->getMessage());
} finally {
unset($_SESSION['oauth2state']);
unset($_SESSION['oauth2provider']);
}
}
private function normalizeUserData(string $provider, $resourceOwner): array
{
$userData = $resourceOwner->toArray();
switch ($provider) {
case 'google':
return [
'id' => $userData['sub'] ?? $userData['id'],
'name' => $userData['name'],
'email' => $userData['email'],
'avatar' => $userData['picture'],
'verified' => $userData['email_verified'] ?? false
];
case 'facebook':
return [
'id' => $userData['id'],
'name' => $userData['name'],
'email' => $userData['email'] ?? null,
'avatar' => $userData['picture']['data']['url'] ?? null,
'verified' => true // Assume Facebook is verified
];
case 'github':
return [
'id' => $userData['id'],
'name' => $userData['name'] ?? $userData['login'],
'email' => $userData['email'],
'avatar' => $userData['avatar_url'],
'verified' => !empty($userData['email'])
];
default:
return $userData;
}
}
public function getAvailableProviders(): array
{
return array_keys($this->providers);
}
}
// Configuration example
$config = [
'google' => [
'client_id' => 'your-google-client-id',
'client_secret' => 'your-google-client-secret',
'redirect_uri' => 'https://your-app.com/oauth/callback'
],
'facebook' => [
'app_id' => 'your-facebook-app-id',
'app_secret' => 'your-facebook-app-secret',
'redirect_uri' => 'https://your-app.com/oauth/callback'
],
'github' => [
'client_id' => 'your-github-client-id',
'client_secret' => 'your-github-client-secret',
'redirect_uri' => 'https://your-app.com/oauth/callback'
]
];
$oauthManager = new MultiProviderOAuthManager($config);
// Login selection screen
if (!isset($_GET['provider']) && !isset($_GET['code'])) {
echo '<h2>Login with:</h2>';
foreach ($oauthManager->getAvailableProviders() as $provider) {
echo "<a href='?provider={$provider}'>Login with " . ucfirst($provider) . "</a><br>";
}
exit;
}
// Start authentication after provider selection
if (isset($_GET['provider']) && !isset($_GET['code'])) {
$provider = $_GET['provider'];
$options = [];
// Provider-specific options
if ($provider === 'google') {
$options['scope'] = ['openid', 'profile', 'email'];
} elseif ($provider === 'facebook') {
$options['scope'] = ['email', 'public_profile'];
} elseif ($provider === 'github') {
$options['scope'] = ['user:email'];
}
$authUrl = $oauthManager->getAuthorizationUrl($provider, $options);
header('Location: ' . $authUrl);
exit;
}
// Callback processing
if (isset($_GET['code'])) {
try {
$result = $oauthManager->handleCallback();
echo '<h2>Authentication Successful!</h2>';
echo '<pre>' . print_r($result, true) . '</pre>';
} catch (\Exception $e) {
echo '<h2>Authentication Failed</h2>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '<a href="/">Try again</a>';
}
}
?>
Client Credentials and Resource Protection
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\GenericProvider;
class ApiResourceManager
{
private $provider;
private $accessToken;
public function __construct(array $config)
{
$this->provider = new GenericProvider([
'clientId' => $config['client_id'],
'clientSecret' => $config['client_secret'],
'redirectUri' => '', // Not needed for Client Credentials
'urlAuthorize' => '', // Not needed for Client Credentials
'urlAccessToken' => $config['token_url'],
'urlResourceOwnerDetails' => '', // Not needed for Client Credentials
]);
}
public function authenticateClient(): void
{
try {
$this->accessToken = $this->provider->getAccessToken('client_credentials', [
'scope' => 'api:read api:write'
]);
} catch (\Exception $e) {
throw new \Exception('Client authentication failed: ' . $e->getMessage());
}
}
public function makeApiRequest(string $method, string $url, array $data = []): array
{
if (!$this->accessToken) {
$this->authenticateClient();
}
// Check token expiration
if ($this->accessToken->hasExpired()) {
$this->authenticateClient();
}
$request = $this->provider->getAuthenticatedRequest(
$method,
$url,
$this->accessToken,
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json'
],
'body' => json_encode($data)
]
);
try {
$response = $this->provider->getParsedResponse($request);
return $response;
} catch (\Exception $e) {
throw new \Exception('API request failed: ' . $e->getMessage());
}
}
public function getUsers(): array
{
return $this->makeApiRequest('GET', 'https://api.example.com/users');
}
public function createUser(array $userData): array
{
return $this->makeApiRequest('POST', 'https://api.example.com/users', $userData);
}
public function updateUser(int $userId, array $userData): array
{
return $this->makeApiRequest('PUT', "https://api.example.com/users/{$userId}", $userData);
}
public function deleteUser(int $userId): array
{
return $this->makeApiRequest('DELETE', "https://api.example.com/users/{$userId}");
}
}
// Usage example
$config = [
'client_id' => 'your-service-client-id',
'client_secret' => 'your-service-client-secret',
'token_url' => 'https://api.example.com/oauth/token'
];
$apiManager = new ApiResourceManager($config);
try {
// Get user list
$users = $apiManager->getUsers();
echo "Users: " . json_encode($users, JSON_PRETTY_PRINT) . "\n";
// Create new user
$newUser = [
'name' => 'John Doe',
'email' => '[email protected]',
'role' => 'user'
];
$createdUser = $apiManager->createUser($newUser);
echo "Created User: " . json_encode($createdUser, JSON_PRETTY_PRINT) . "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Error Handling and Rate Limiting Support
<?php
require_once 'vendor/autoload.php';
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
class RobustOAuthClient
{
private $provider;
private $maxRetries;
private $retryDelay;
public function __construct(array $config, int $maxRetries = 3, int $retryDelay = 1000)
{
$this->provider = new GenericProvider($config);
$this->maxRetries = $maxRetries;
$this->retryDelay = $retryDelay; // milliseconds
}
public function getAccessTokenWithRetry(string $grant, array $options = []): ?object
{
$lastException = null;
for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
try {
return $this->provider->getAccessToken($grant, $options);
} catch (IdentityProviderException $e) {
$lastException = $e;
$response = $e->getResponseBody();
// Retry on rate limiting
if ($this->isRateLimited($e)) {
$retryAfter = $this->getRetryAfter($e);
$delay = $retryAfter ? $retryAfter * 1000 : $this->retryDelay * $attempt;
error_log("Rate limited, retrying after {$delay}ms (attempt {$attempt}/{$this->maxRetries})");
usleep($delay * 1000); // microseconds
continue;
}
// Retry on temporary errors
if ($this->isTemporaryError($e)) {
$delay = $this->retryDelay * $attempt;
error_log("Temporary error, retrying after {$delay}ms (attempt {$attempt}/{$this->maxRetries})");
usleep($delay * 1000);
continue;
}
// Fail immediately on permanent errors
throw $e;
} catch (\Exception $e) {
$lastException = $e;
error_log("Unexpected error (attempt {$attempt}/{$this->maxRetries}): " . $e->getMessage());
if ($attempt < $this->maxRetries) {
$delay = $this->retryDelay * $attempt;
usleep($delay * 1000);
continue;
}
throw $e;
}
}
throw $lastException;
}
private function isRateLimited(IdentityProviderException $e): bool
{
$statusCode = $e->getResponseStatus();
$response = $e->getResponseBody();
// Check by HTTP status code
if ($statusCode === 429) {
return true;
}
// Check by response body
if (is_array($response)) {
$error = $response['error'] ?? '';
return in_array($error, ['rate_limit_exceeded', 'too_many_requests']);
}
return false;
}
private function isTemporaryError(IdentityProviderException $e): bool
{
$statusCode = $e->getResponseStatus();
// Treat 5xx series as temporary errors
return $statusCode >= 500 && $statusCode < 600;
}
private function getRetryAfter(IdentityProviderException $e): ?int
{
$headers = $e->getResponseHeaders();
// Get from Retry-After header
if (isset($headers['Retry-After'])) {
return (int) $headers['Retry-After'];
}
// Calculate from X-Rate-Limit-Reset headers
if (isset($headers['X-Rate-Limit-Reset'])) {
$resetTime = (int) $headers['X-Rate-Limit-Reset'];
return max(0, $resetTime - time());
}
return null;
}
public function safeApiCall(callable $apiCall, array $context = []): array
{
try {
return $apiCall();
} catch (IdentityProviderException $e) {
$error = [
'type' => 'identity_provider_error',
'status_code' => $e->getResponseStatus(),
'message' => $e->getMessage(),
'response' => $e->getResponseBody(),
'context' => $context
];
error_log('OAuth API Error: ' . json_encode($error));
return [
'success' => false,
'error' => $error
];
} catch (\Exception $e) {
$error = [
'type' => 'general_error',
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'context' => $context
];
error_log('General API Error: ' . json_encode($error));
return [
'success' => false,
'error' => $error
];
}
}
}
// Usage example
$config = [
'clientId' => 'your-client-id',
'clientSecret' => 'your-client-secret',
'redirectUri' => 'https://your-app.com/callback',
'urlAuthorize' => 'https://api.example.com/oauth/authorize',
'urlAccessToken' => 'https://api.example.com/oauth/token',
'urlResourceOwnerDetails' => 'https://api.example.com/user',
];
$robustClient = new RobustOAuthClient($config, 3, 2000);
// Token acquisition with error handling
$result = $robustClient->safeApiCall(function() use ($robustClient) {
$token = $robustClient->getAccessTokenWithRetry('client_credentials');
return [
'success' => true,
'token' => $token->getToken(),
'expires_at' => $token->getExpires()
];
}, ['operation' => 'get_access_token']);
if ($result['success']) {
echo "Token obtained successfully: " . $result['token'] . "\n";
} else {
echo "Failed to obtain token: " . json_encode($result['error'], JSON_PRETTY_PRINT) . "\n";
}
?>