league/oauth2-server
Standards-compliant OAuth 2.0 authorization server for PHP. Protect APIs with bearer access tokens, issue and refresh tokens, and support common grants (auth code, client credentials, device, implicit, password, refresh) with PSR-7 interoperability and RFC support.
Install the package:
composer require league/oauth2-server
Create a basic OAuth server instance (typically in a service provider):
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use League\OAuth2\Server\Grant\PasswordGrant;
use League\OAuth2\Server\ResourceServer;
// Define your entity implementations (see Implementation Patterns)
$authorizationServer = new AuthorizationServer(
$entityManager, // Your entity manager (e.g., Doctrine)
$cryptKey, // CryptKeyInterface
$serverMiddleware,
$grantTypes = [
new AuthCodeGrant($accessTokenTTL, $refreshTokenTTL),
new ClientCredentialsGrant($accessTokenTTL),
new RefreshTokenGrant($refreshTokenTTL),
new PasswordGrant($accessTokenTTL, $refreshTokenTTL),
],
$scopeManager = null,
$entityManager->getRepository(ClientEntity::class),
$entityManager->getRepository(AccessTokenEntity::class),
$entityManager->getRepository(RefreshTokenEntity::class),
$entityManager->getRepository(AuthCodeEntity::class),
$entityManager->getRepository(ScopeEntity::class)
);
First use case: Protect an API endpoint
use League\OAuth2\Server\ResourceServer;
$resourceServer = new ResourceServer(
$cryptKey,
$serverMiddleware,
$entityManager->getRepository(AccessTokenEntity::class)
);
// In your Laravel route/middleware:
$psr7Request = $request->toPsr7();
$psr7Response = new \Laminas\Diactoros\Response();
try {
$server = $resourceServer->validateAuthenticatedRequest($psr7Request);
// Proceed with authenticated logic
} catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
return response()->json($exception->generateHttpResponse($psr7Response)->getBody()->getContents(), $exception->getHttpStatusCode());
}
AuthCodeGrant for web apps, ClientCredentialsGrant for machine-to-machine).HandleOAuth2Validation middleware).Leverage Laravel’s Eloquent models to implement League’s interfaces:
use League\OAuth2\Server\Entity\ClientEntityInterface;
use League\OAuth2\Server\Entity\AccessTokenEntityInterface;
class Client extends Model implements ClientEntityInterface {
use \League\OAuth2\Server\Entity\Traits\ClientTrait;
// Add Eloquent fields: id, secret, name, etc.
}
class AccessToken extends Model implements AccessTokenEntityInterface {
use \League\OAuth2\Server\Entity\Traits\AccessTokenTrait;
// Add Eloquent fields: id, token, user_id, expires_at, etc.
}
$authorizationRequest = $authorizationServer->createAuthorizationRequest(
'https://your-app.com/callback',
'random_state_string',
['scope1', 'scope2']
);
return redirect($authorizationRequest->getAuthorizationUri());
$authCode = $authorizationServer->validateAuthorizationRequest(
$request->query->all(),
$request->get('state')
);
$tokenRequest = $authorizationServer->createTokenRequest(
$authCode->getIdentifier(),
$authCode->getClient()->getIdentifier(),
$authCode->getClient()->getSecret(),
$authCode->getRedirectUri(),
$authCode->getScopes()
);
$accessToken = $authorizationServer->issueAccessToken($tokenRequest);
$client = $entityManager->getRepository(ClientEntity::class)->find('client_id');
$tokenRequest = $authorizationServer->createTokenRequest(
null, // No auth code
$client->getIdentifier(),
$client->getSecret(),
null, // No redirect URI
['api:write'] // Scopes
);
$accessToken = $authorizationServer->issueAccessToken($tokenRequest);
Create a Laravel middleware to validate tokens:
use League\OAuth2\Server\ResourceServer;
class HandleOAuth2Validation {
protected $server;
public function __construct() {
$this->server = new ResourceServer(
$cryptKey,
$serverMiddleware,
$entityManager->getRepository(AccessTokenEntity::class)
);
}
public function handle($request, Closure $next) {
$psr7Request = $request->toPsr7();
try {
$this->server->validateAuthenticatedRequest($psr7Request);
return $next($request);
} catch (\League\OAuth2\Server\Exception\OAuthServerException $e) {
return response()->json([
'error' => $e->getErrorCode(),
'error_description' => $e->getErrorDescription()
], $e->getHttpStatusCode());
}
}
}
Define scopes in a Laravel model:
class Scope extends Model implements ScopeEntityInterface {
use \League\OAuth2\Server\Entity\Traits\ScopeTrait;
// Fields: id, identifier (e.g., 'read:profile')
}
Use a ScopeManager to validate scopes:
$scopeManager = new \League\OAuth2\Server\Scope\ScopeManager(
$entityManager->getRepository(ScopeEntity::class)
);
$authorizationServer = new AuthorizationServer(/* ... */, $scopeManager);
Handle refresh token requests:
$refreshToken = $entityManager->getRepository(RefreshTokenEntity::class)->find('refresh_token_id');
$tokenRequest = $authorizationServer->createTokenRequest(
null, // No auth code
$refreshToken->getClient()->getIdentifier(),
$refreshToken->getClient()->getSecret(),
null, // No redirect URI
$refreshToken->getScopes()
);
$accessToken = $authorizationServer->issueAccessToken($tokenRequest);
Enable PKCE for public clients:
$grant = new AuthCodeGrant($accessTokenTTL, $refreshTokenTTL);
$grant->setUsePKCE(true); // Enable PKCE
$authorizationServer = new AuthorizationServer(/* ... */, [$grant]);
Case Sensitivity in Headers
Authorization header are case-insensitive (fixed in v9.3.0), but ensure your middleware handles this correctly.Authorization: Bearer TOKEN or authorization: bearer token both work.Refresh Token Revocation
revokeRefreshTokens(false) does not disable issuing new refresh tokens (fixed in v9.0.1). Use GrantTypeInterface::revokeRefreshTokens() carefully.Scope Validation
strtolower()) if needed.invalid_scope errors.State Parameter
state parameter must be unique and unguessable (e.g., CSRF tokens). Omitting it in implicit grants can lead to security issues.Redirect URIs
UriInterface for strict validation:
$uri = new \League\Uri\Uri($redirectUri);
if (!$client->getRegisteredRedirectUris()->contains($uri)) {
throw new \League\OAuth2\Server\Exception\OAuthServerException(
'invalid_request',
'Redirect URI mismatch',
\Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
);
}
Token Expiration
expires_at time (not "after"). Use Carbon for precise timestamps:
$expiresAt = now()->addMinutes($accessTokenTTL)->timestamp;
**PKCE Downg
How can I help you explore Laravel packages today?