SDKs oficiales · PietroCal API v1
PietroCal API v1 · PHP

OAuth 2.1 para PHP

Helper para Authorization Code con PKCE S256, intercambio de código, refresh rotatorio y revocación. Revisá los requisitos, copiá el archivo completo o descargalo para incorporarlo a tu proyecto.

PietroCalOAuth.phpphp/PietroCalOAuth.php · 148 líneas
Descargar
<?php

declare(strict_types=1);

namespace PietroCal\Sdk;

use JsonException;
use RuntimeException;


/*
 * 10Q00.4.1: una solicitud HTTP directa debe mostrar el visor seguro en lugar
 * de ejecutar este archivo. Los usos por CLI o mediante require/include no se
 * modifican.
 */
if (
    PHP_SAPI !== 'cli'
    && isset($_SERVER['SCRIPT_FILENAME'])
    && realpath((string)$_SERVER['SCRIPT_FILENAME']) === __FILE__
) {
    header('Cache-Control: no-store');
    header('Location: /api/sdk/view.php?file=php%2FPietroCalOAuth.php', true, 302);
    exit;
}

final class PietroCalOAuth
{
    public function __construct(
        private readonly string $clientId,
        private readonly string $redirectUri,
        private readonly ?string $clientSecret = null,
        private readonly string $baseUrl = 'https://pietrocal.com'
    ) {
        if (!str_starts_with($clientId, 'pc_oauth_')) {
            throw new RuntimeException('El Client ID debe comenzar con pc_oauth_.');
        }
    }

    public function createPkce(): array
    {
        $verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
        $challenge = rtrim(strtr(
            base64_encode(hash('sha256', $verifier, true)),
            '+/',
            '-_'
        ), '=');

        return [
            'codeVerifier' => $verifier,
            'codeChallenge' => $challenge,
        ];
    }

    public function authorizationUrl(array $scopes, string $state, string $codeChallenge): string
    {
        return rtrim($this->baseUrl, '/') . '/oauth/authorize.php?' . http_build_query([
            'response_type' => 'code',
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUri,
            'scope' => implode(' ', array_values(array_unique($scopes))),
            'state' => $state,
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256',
        ], '', '&', PHP_QUERY_RFC3986);
    }

    public function exchangeCode(string $code, string $codeVerifier): array
    {
        return $this->postForm('/oauth/token.php', [
            'grant_type' => 'authorization_code',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'code' => $code,
            'redirect_uri' => $this->redirectUri,
            'code_verifier' => $codeVerifier,
        ]);
    }

    public function refresh(string $refreshToken): array
    {
        return $this->postForm('/oauth/token.php', [
            'grant_type' => 'refresh_token',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'refresh_token' => $refreshToken,
        ]);
    }

    public function revoke(string $token, ?string $tokenTypeHint = null): void
    {
        $this->postForm('/oauth/revoke.php', [
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'token' => $token,
            'token_type_hint' => $tokenTypeHint,
        ], true);
    }

    private function postForm(string $path, array $fields, bool $allowEmpty = false): array
    {
        $fields = array_filter($fields, static fn ($value): bool => $value !== null && $value !== '');
        $curl = curl_init(rtrim($this->baseUrl, '/') . $path);
        if ($curl === false) {
            throw new RuntimeException('No se pudo inicializar cURL.');
        }

        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Accept: application/json',
                'Content-Type: application/x-www-form-urlencoded',
            ],
            CURLOPT_POSTFIELDS => http_build_query($fields, '', '&', PHP_QUERY_RFC3986),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
        ]);

        $raw = curl_exec($curl);
        if ($raw === false) {
            $message = curl_error($curl);
            curl_close($curl);
            throw new RuntimeException('Error de red: ' . $message);
        }

        $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
        curl_close($curl);

        if ($status < 200 || $status >= 300) {
            $data = json_decode($raw, true);
            throw new RuntimeException(
                (string) ($data['error_description'] ?? 'La operación OAuth falló.')
            );
        }

        if ($allowEmpty && trim($raw) === '') {
            return [];
        }

        try {
            return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new RuntimeException('PietroCal devolvió JSON inválido.', 0, $exception);
        }
    }
}