OAuth2 es el protocolo estándar de autorización que usan Google, GitHub o Stripe para delegar acceso a recursos protegidos sin compartir contraseñas. En PHP, league/oauth2-server proporciona un servidor OAuth2 completo y mantenible.
Instalación
composer require league/oauth2-server
Generar claves RSA
El servidor firma los tokens JWT con RSA:
openssl genrsa -out private.key 2048 openssl rsa -in private.key -pubout -out public.key chmod 600 private.key
Repositorios requeridos
La librería usa interfaces de repositorio que tú implementas con tu base de datos:
ClientRepositoryInterface: valida que el cliente (app) existe y su secreto es correcto.AccessTokenRepositoryInterface: persiste y revoca access tokens.ScopeRepositoryInterface: devuelve los scopes válidos.UserRepositoryInterface: valida usuario/contraseña (solo para Authorization Code y Password Grant).RefreshTokenRepositoryInterface: persiste refresh tokens.AuthCodeRepositoryInterface: persiste los authorization codes.
Flujo Client Credentials (máquina a máquina)
<?php
use LeagueOAuth2ServerAuthorizationServer;
use LeagueOAuth2ServerGrantClientCredentialsGrant;
$server = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
'file://'. __DIR__ . '/private.key',
base64_encode(random_bytes(32)) // clave de encriptación
);
$server->enableGrantType(
new ClientCredentialsGrant(),
new DateInterval('PT1H') // tokens de 1 hora
);
// Endpoint /oauth/token (en tu router)
try {
$response = $server->respondToAccessTokenRequest($request, $response);
return $response;
} catch (LeagueOAuth2ServerExceptionOAuthServerException $e) {
return $e->generateHttpResponse($response);
}
?>
El cliente llama con:
curl -X POST https://api.ejemplo.com/oauth/token
-d "grant_type=client_credentials&client_id=mi-app&client_secret=secreto&scope=read"
Flujo Authorization Code
Para aplicaciones en nombre de un usuario. Requiere dos endpoints: autorización y token.
<?php
use LeagueOAuth2ServerGrantAuthCodeGrant;
$server->enableGrantType(
new AuthCodeGrant(
$authCodeRepository,
$refreshTokenRepository,
new DateInterval('PT10M') // authorization codes de 10 minutos
),
new DateInterval('PT1H')
);
// Endpoint GET /oauth/authorize
try {
$authRequest = $server->validateAuthorizationRequest($request);
// Aquí muestras la pantalla de consentimiento al usuario
// Una vez aprobado:
$authRequest->setAuthorizationApproved(true);
$authRequest->setUser(new UserEntity($userId));
$response = $server->completeAuthorizationRequest($authRequest, $response);
return $response;
} catch (Exception $e) {
// ...
}
?>
Validar tokens con ResourceServer
En las rutas protegidas de tu API:
<?php
use LeagueOAuth2ServerResourceServer;
$resourceServer = new ResourceServer(
$accessTokenRepository,
'file://' . __DIR__ . '/public.key'
);
try {
$request = $resourceServer->validateAuthenticatedRequest($request);
$userId = $request->getAttribute('oauth_user_id');
$clientId = $request->getAttribute('oauth_client_id');
$scopes = $request->getAttribute('oauth_scopes');
} catch (LeagueOAuth2ServerExceptionOAuthServerException $e) {
return $e->generateHttpResponse($response); // 401 Unauthorized
}
?>
Scopes como sistema de permisos
<?php
class ScopeRepository implements ScopeRepositoryInterface
{
private array $scopes = [
'read' => 'Leer datos',
'write' => 'Crear y modificar datos',
'admin' => 'Acceso completo',
'email' => 'Ver dirección de email',
];
public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntityInterface
{
if (!array_key_exists($identifier, $this->scopes)) {
return null;
}
$scope = new ScopeEntity();
$scope->setIdentifier($identifier);
return $scope;
}
public function finalizeScopes(array $scopes, string $grantType, ClientEntityInterface $client, $userId = null): array
{
// Puedes filtrar scopes según el cliente o usuario aquí
return $scopes;
}
}
?>
Errores comunes
- invalid_client: el
client_idno existe o elclient_secretno coincide. RevisaClientRepositoryInterface::validateClient(). - Clave RSA con permisos incorrectos:
private.keydebe ser legible solo por el proceso PHP (chmod 600); de lo contrario, la librería lanzará una excepción al arrancar. - Tokens expirados inmediatamente: verifica que la zona horaria del servidor sea UTC o que los
DateIntervalsean correctos.
