OAuth2 en PHP con league/oauth2-server: flujos de autorización y tokens de acceso

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_id no existe o el client_secret no coincide. Revisa ClientRepositoryInterface::validateClient().
  • Clave RSA con permisos incorrectos: private.key debe 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 DateInterval sean correctos.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP