Installation
composer require bshaffer/oauth2-server-php
Add to composer.json:
"autoload": {
"psr-4": {
"App\\": "app/",
"League\\OAuth2\\Server\\": "vendor/league/oauth2-server/"
}
}
Run composer dump-autoload.
Basic Server Initialization
In a Laravel route (e.g., routes/api.php):
use League\OAuth2\Server\ResourceServer;
use League\OAuth2\Server\AuthorizationServer;
$server = new ResourceServer(
new \League\OAuth2\Server\Repository\DefaultAccessTokenRepository(),
new \League\OAuth2\Server\Repository\DefaultClientRepository(),
new \League\OAuth2\Server\Repository\DefaultScopeRepository(),
new \League\OAuth2\Server\Repository\DefaultUserRepository(),
'HS256', // Algorithm
'your-secret-key' // Private key or shared secret
);
First Use Case: Protecting an API Endpoint
Route::get('/protected', function () use ($server) {
try {
$request = \League\OAuth2\Server\RequestTypes\BearerTokenRequest::createFromGlobals();
$server->validateAuthenticatedRequest($request);
return response()->json(['message' => 'Access granted!']);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $e) {
return response()->json(['error' => (string)$e], $e->getHttpStatusCode());
}
});
src/League/OAuth2/Server/ (Core classes)tests/ (Example implementations and edge cases)$server = new AuthorizationServer(
new \League\OAuth2\Server\Repository\DefaultClientRepository(),
new \League\OAuth2\Server\Repository\DefaultAccessTokenRepository(),
new \League\OAuth2\Server\Repository\DefaultRefreshTokenRepository(),
new \League\OAuth2\Server\Repository\DefaultScopeRepository(),
'HS256',
'your-secret-key'
);
Route::post('/oauth/token', function () use ($server) {
$request = \League\OAuth2\Server\RequestTypes\TokenRequest::createFromGlobals();
$response = new \League\OAuth2\Server\ResponseTypes\BearerTokenResponse();
return $server->respondToAccessTokenRequest($request, $response);
});
Password Grant (Client credentials + user credentials):
$grant = new \League\OAuth2\Server\Grant\PasswordGrant(
new \League\OAuth2\Server\Repository\DefaultUserRepository(),
new \League\OAuth2\Server\Repository\DefaultClientRepository(),
new \League\OAuth2\Server\Repository\DefaultAccessTokenRepository(),
new \League\OAuth2\Server\Repository\DefaultRefreshTokenRepository()
);
$server->enableGrantType($grant, new \League\OAuth2\Server\RequestTypes\PasswordRequest());
Refresh Token Grant:
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant(
new \League\OAuth2\Server\Repository\DefaultRefreshTokenRepository(),
new \League\OAuth2\Server\Repository\DefaultAccessTokenRepository()
);
$server->enableGrantType($grant, new \League\OAuth2\Server\RequestTypes\RefreshTokenRequest());
Route::get('/api/data', function () use ($server) {
$request = \League\OAuth2\Server\RequestTypes\BearerTokenRequest::createFromGlobals();
$server->validateAuthenticatedRequest($request);
// Get the authenticated user (if using a custom user repo)
$user = $server->getResourceOwnerRepository()->getResourceOwner($request->getServer()->get('PHP_AUTH_USER'));
return response()->json(['data' => $user->toArray()]);
});
Override DefaultAccessTokenRepository to add custom claims:
class CustomAccessTokenRepository extends \League\OAuth2\Server\Repository\DefaultAccessTokenRepository {
public function getNewToken($accessTokenId, $clientId, $userIdentifier, $scope, $accessToken, $expiresAt, $refreshToken = null, array $customClaims = []) {
$customClaims['custom_field'] = 'value';
return parent::getNewToken($accessTokenId, $clientId, $userIdentifier, $scope, $accessToken, $expiresAt, $refreshToken, $customClaims);
}
}
use League\OAuth2\Server\ResourceServer;
class Authenticate extends Middleware {
protected $server;
public function __construct(ResourceServer $server) {
$this->server = $server;
}
public function handle($request, Closure $next) {
$request = \League\OAuth2\Server\RequestTypes\BearerTokenRequest::createFromGlobals();
$this->server->validateAuthenticatedRequest($request);
return $next($request);
}
}
Register in app/Http/Kernel.php:
protected $routeMiddleware = [
'auth.oauth' => \App\Http\Middleware\Authenticate::class,
];
Extend DefaultUserRepository to integrate with Laravel’s Auth:
class LaravelUserRepository extends \League\OAuth2\Server\Repository\DefaultUserRepository {
public function getResourceOwner($username) {
$user = \App\Models\User::where('username', $username)->first();
return $user ? new \League\OAuth2\Server\Entity\UserEntity($user->id, $username) : null;
}
}
Token Validation Failures
invalid_token or insufficient_scope errors.client_id and client_secret are correct.exp claim).scope claim in token vs. requested scope).try {
$server->validateAuthenticatedRequest($request);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $e) {
return response()->json(['error' => $e->getMessage()], $e->getHttpStatusCode());
}
Repository Mismatches
RepositoryNotFoundException or EntityNotFoundException.Client, AccessToken, User, etc.) are properly implemented.getClient() or getAccessToken() returns null for valid inputs.// Example: Custom Client Repository
class CustomClientRepository extends \League\OAuth2\Server\Repository\DefaultClientRepository {
public function getClientEntity($clientIdentifier) {
$client = \App\Models\Client::where('id', $clientIdentifier)->first();
return $client ? new \League\OAuth2\Server\Entity\ClientEntity(
$client->id,
$client->secret,
$client->redirectUri
) : null;
}
}
Cryptographic Issues
invalid_signature or invalid_token with JWT.HS256, RS256, etc.) matches in both server and client.secret or private key is correct and accessible.// For RS256 (asymmetric), use:
$server = new ResourceServer(
// ...,
'RS256',
file_get_contents('path/to/private.key')
);
State Management in Authorization Code Flow
state parameter in authorization requests.$state = bin2hex(random_bytes(32));
session(['oauth_state' => $state]);
return redirect()->to("https://oauth-provider.com/authorize?response_type=code&client_id={$clientId}&redirect_uri={$redirectUri}&state={$state}");
How can I help you explore Laravel packages today?