PHP Logoleague/oauth2-server

The `league/oauth2-server` library is a robust and flexible PHP package designed to help developers build an OAuth 2.0 authorization server. It adheres strictly to RFC 6749 and provides a comprehensive set of tools to enable your application to act as an OAuth 2.0 provider, allowing other client applications to securely access protected resources on behalf of users.

Key Concepts and Features:

1. Authorization Server: This library implements the core functionality of an OAuth 2.0 Authorization Server. Its primary role is to issue access tokens to authorized client applications after a user grants permission.
2. Grant Types: It supports all standard OAuth 2.0 grant types, including:
* Authorization Code Grant: The most secure and recommended grant for confidential clients (web applications).
* Client Credentials Grant: For machine-to-machine authentication where a client accesses its own protected resources.
* Password Grant: For highly trusted first-party applications where the user directly provides credentials to the client.
* Refresh Token Grant: Allows clients to obtain new access tokens without re-prompting the user.
* Implicit Grant: (Less recommended now due to security concerns) For public clients like single-page applications.
3. Tokens:
* Access Tokens: Short-lived tokens that grant access to protected resources. They are usually JWTs (JSON Web Tokens) in `league/oauth2-server`, signed with a private key.
* Refresh Tokens: Long-lived tokens used to obtain new access tokens once the current access token expires, without requiring user re-authentication.
4. Scopes: Allows for fine-grained control over the permissions granted to a client. Clients can request specific scopes, and the user can approve or deny them.
5. Repositories: The library is designed with a strong emphasis on dependency injection and interfaces. You need to implement various repository interfaces (e.g., `ClientRepository`, `ScopeRepository`, `UserRepository`, `AccessTokenRepository`, `RefreshTokenRepository`) to integrate with your application's data storage (database, file system, etc.). This makes it highly adaptable to different backend systems.
6. Extensibility: Its modular design allows developers to extend or replace core components, such as token generators, request/response handlers, and grant type validators.
7. Security: It handles critical security aspects like token signing, token revocation, and secure authorization flows, reducing the burden on the developer to implement these from scratch.
8. Integration with PSR-7: It seamlessly integrates with PSR-7 HTTP message interfaces (`Psr\Http\Message\ServerRequestInterface` and `Psr\Http\Message\ResponseInterface`), making it compatible with various modern PHP frameworks (e.g., Laravel, Symfony, Slim) that use or provide adapters for PSR-7.

How it Works (Simplified Flow):

1. A client application requests authorization from your server.
2. Your server presents an authorization prompt to the user.
3. The user grants or denies permission.
4. If granted, your server issues an authorization code (for Authorization Code Grant) or directly an access token (for Implicit or Client Credentials Grant).
5. The client exchanges the authorization code for an access token (and optionally a refresh token).
6. The client uses the access token to access protected resources on your API, which your API then validates using the `league/oauth2-server`'s resource server component (though the example focuses on the authorization server).

In essence, `league/oauth2-server` provides the backbone for securely managing user consent and issuing access tokens, allowing you to build a robust and standards-compliant OAuth 2.0 provider for your services.

Example Code

<?php

require __DIR__ . '/vendor/autoload.php';

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
use League\OAuth2\Server\Grant\PasswordGrant;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

// --- 1. Define your Repositories (for demonstration purposes, in-memory) ---

// Client Repository
class MyClientRepository implements League\OAuth2\Server\Repositories\ClientRepositoryInterface
{
    private $clients = [
        'clientId123' => [
            'secret' => 'clientSecret456',
            'name' => 'My Test Client',
            'redirect_uri' => 'http://localhost/callback', // Needed for Auth Code Grant, not strictly for Client Creds/Password
            'is_confidential' => true,
        ],
    ];

    public function getClientEntity($clientIdentifier)
    {
        if (isset($this->clients[$clientIdentifier])) {
            $client = $this->clients[$clientIdentifier];
            $clientEntity = new League\OAuth2\Server\Entities\ClientEntity();
            $clientEntity->setIdentifier($clientIdentifier);
            $clientEntity->setName($client['name']);
            $clientEntity->setRedirectUri($client['redirect_uri']);
            $clientEntity->isConfidential($client['is_confidential']);
            return $clientEntity;
        }
        return null;
    }

    public function validateClient($clientIdentifier, $clientSecret, $grantType)
    {
        if (isset($this->clients[$clientIdentifier]) && $this->clients[$clientIdentifier]['secret'] === $clientSecret) {
            return true;
        }
        return false;
    }
}

// Access Token Repository
class MyAccessTokenRepository implements League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface
{
    // In a real application, you'd store tokens in a database
    private $tokens = [];

    public function getNewToken(League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
    {
        $accessToken = new League\OAuth2\Server\Entities\AccessTokenEntity();
        $accessToken->setIdentifier(bin2hex(random_bytes(40))); // Generate unique ID
        $accessToken->setClient($clientEntity);
        foreach ($scopes as $scope) {
            $accessToken->addScope($scope);
        }
        $accessToken->setUserIdentifier($userIdentifier);
        return $accessToken;
    }

    public function persistNewAccessToken(League\OAuth2\Server\Entities\AccessTokenEntityInterface $accessTokenEntity)
    {
        $this->tokens[$accessTokenEntity->getIdentifier()] = $accessTokenEntity;
        // Logic to save to database here
    }

    public function revokeAccessToken($tokenId)
    {
        if (isset($this->tokens[$tokenId])) {
            unset($this->tokens[$tokenId]);
        }
        // Logic to mark as revoked in database
    }

    public function isAccessTokenRevoked($tokenId)
    {
        return !isset($this->tokens[$tokenId]);
    }
}

// Scope Repository
class MyScopeRepository implements League\OAuth2\Server\Repositories\ScopeRepositoryInterface
{
    public function getScopeEntityByIdentifier($scopeIdentifier)
    {
        if ($scopeIdentifier === 'basic') {
            $scope = new League\OAuth2\Server\Entities\ScopeEntity();
            $scope->setIdentifier('basic');
            return $scope;
        }
        return null;
    }

    public function finalizeScopes(
        array $scopes,
        $grantType,
        League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity,
        $userIdentifier = null
    ) {
        // Here you can add logic to filter or modify scopes based on client, user, or grant type
        return $scopes;
    }
}

// User Repository (required for Password Grant)
class MyUserRepository implements League\OAuth2\Server\Repositories\UserRepositoryInterface
{
    private $users = [
        'johndoe' => [
            'password' => 'secretpass', // In real life, use password_hash() and password_verify()
            'id' => '1',
        ],
    ];

    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity
    ) {
        if (isset($this->users[$username]) && $this->users[$username]['password'] === $password) {
            $userEntity = new League\OAuth2\Server\Entities\UserEntity();
            $userEntity->setIdentifier($this->users[$username]['id']);
            return $userEntity;
        }
        return null;
    }
}

// Refresh Token Repository (for a complete server, even if not used in this specific token request)
class MyRefreshTokenRepository implements League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface
{
    private $tokens = [];

    public function getNewRefreshToken()
    {
        return new League\OAuth2\Server\Entities\RefreshTokenEntity();
    }

    public function persistNewRefreshToken(League\OAuth2\Server\Entities\RefreshTokenEntityInterface $refreshTokenEntity)
    {
        $this->tokens[$refreshTokenEntity->getIdentifier()] = $refreshTokenEntity;
    }

    public function revokeRefreshToken($tokenId)
    {
        if (isset($this->tokens[$tokenId])) {
            unset($this->tokens[$tokenId]);
        }
    }

    public function isRefreshTokenRevoked($tokenId)
    {
        return !isset($this->tokens[$tokenId]);
    }
}

// --- 2. Setup the Authorization Server ---

// Paths to the RSA private and public keys.
// You can generate these using `openssl genrsa -out private.key 2048` and
// `openssl rsa -in private.key -pubout -out public.key`
// For this example, let's assume they are present or simulate them.
$privateKeyPath = __DIR__ . '/private.key';
$publicKeyPath = __DIR__ . '/public.key';

// Simulate key files if they don't exist for demo purposes
if (!file_exists($privateKeyPath)) {
    // WARNING: In a real application, generate these securely and store them properly.
    // This is purely for demonstration.
    exec('openssl genrsa -out ' . escapeshellarg($privateKeyPath) . ' 2048');
    exec('openssl rsa -in ' . escapeshellarg($privateKeyPath) . ' -pubout -out ' . escapeshellarg($publicKeyPath));
}

// Init repositories
$clientRepository = new MyClientRepository();
$scopeRepository = new MyScopeRepository();
$accessTokenRepository = new MyAccessTokenRepository();
$userRepository = new MyUserRepository(); // Only needed for Password Grant
$refreshTokenRepository = new MyRefreshTokenRepository(); // Needed for Refresh Token Grant

$server = new AuthorizationServer(
    $clientRepository,
    $accessTokenRepository,
    $scopeRepository,
    $privateKeyPath, // Path to private key
    $publicKeyPath // Path to public key
);

// Add the grant types you want to support
$passwordGrant = new PasswordGrant(
    $userRepository,
    $refreshTokenRepository // Required for Password Grant to issue refresh tokens
);
$passwordGrant->setAccessTokenTTL(new DateInterval('PT1H')); // Access tokens will expire in 1 hour
$server->addGrantType($passwordGrant);

$clientCredentialsGrant = new ClientCredentialsGrant();
$clientCredentialsGrant->setAccessTokenTTL(new DateInterval('PT1H'));
$server->addGrantType($clientCredentialsGrant);

// --- 3. Handle a token request ---

// Create a PSR-7 compliant request and response
$psr17Factory = new Psr17Factory();
$requestCreator = new ServerRequestCreator(
    $psr17Factory, // ServerRequestFactory
    $psr17Factory, // UriFactory
    $psr17Factory, // UploadedFileFactory
    $psr17Factory  // StreamFactory
);
$request = $requestCreator->fromGlobals();
$response = $psr17Factory->createResponse();

// Example: Simulate a POST request for a token (e.g., Client Credentials Grant)
// For demonstration, let's manually set POST data for Client Credentials
// In a real scenario, this would come from the actual HTTP request.
// To test Client Credentials:
//      grant_type=client_credentials&client_id=clientId123&client_secret=clientSecret456&scope=basic
// To test Password Grant:
//      grant_type=password&client_id=clientId123&client_secret=clientSecret456&username=johndoe&password=secretpass&scope=basic

// Let's create a simulated POST request for Client Credentials Grant
$simulatedBody = [
    'grant_type' => 'client_credentials',
    'client_id' => 'clientId123',
    'client_secret' => 'clientSecret456',
    'scope' => 'basic'
];
$request = $request->withParsedBody($simulatedBody);
$request = $request->withMethod('POST'); // Crucial for token requests

// OR for Password Grant
/*
$simulatedBody = [
    'grant_type' => 'password',
    'client_id' => 'clientId123',
    'client_secret' => 'clientSecret456',
    'username' => 'johndoe',
    'password' => 'secretpass',
    'scope' => 'basic'
];
$request = $request->withParsedBody($simulatedBody);
$request = $request->withMethod('POST');
*/

try {
    // Try to respond to the access token request
    $response = $server->respondToAccessTokenRequest($request, $response);

    // Output the response
    header('Content-Type: ' . $response->getHeaderLine('Content-Type'));
    http_response_code($response->getStatusCode());
    echo (string) $response->getBody();

} catch (OAuthServerException $exception) {
    // All OAuth 2.0 errors are handled by this exception
    $response = $exception->generateHttpResponse($response);
    header('Content-Type: ' . $response->getHeaderLine('Content-Type'));
    http_response_code($response->getStatusCode());
    echo (string) $response->getBody();

} catch (Exception $exception) {
    // Other errors (e.g., internal server error)
    $response = $response->withStatus(500)->withHeader('Content-Type', 'application/json');
    $response->getBody()->write(json_encode([
        'error' => 'internal_server_error',
        'message' => $exception->getMessage(),
    ]));
    header('Content-Type: ' . $response->getHeaderLine('Content-Type'));
    http_response_code($response->getStatusCode());
    echo (string) $response->getBody();
}

// --- IMPORTANT NOTE ---
// This example uses in-memory repositories and generates keys for demonstration.
// In a production environment:
// 1. You would use a database to persist clients, access tokens, refresh tokens, and users.
// 2. Private and public keys should be securely generated and stored, never committed to VCS.
// 3. Your PSR-7 request/response objects would come from your web server's actual input.
// 4. Implement a proper Resource Server for validating access tokens when protecting API endpoints.