<?php
declare(strict_types=1);
namespace GNexus\GAuth\Client;
use GNexus\GAuth\Config\GAuthConfig;
use GNexus\GAuth\Contract\ClockInterface;
use GNexus\GAuth\Contract\PkceStoreInterface;
use GNexus\GAuth\Contract\RuntimeUserProviderInterface;
use GNexus\GAuth\Contract\StateStoreInterface;
use GNexus\GAuth\Contract\TokenEndpointInterface;
use GNexus\GAuth\Contract\WebhookParserInterface;
use GNexus\GAuth\Contract\WebhookVerifierInterface;
use GNexus\GAuth\DTO\AuthenticatedUser;
use GNexus\GAuth\DTO\AuthorizationRequest;
use GNexus\GAuth\DTO\TokenSet;
use GNexus\GAuth\DTO\VerifiedWebhook;
use GNexus\GAuth\DTO\WebhookEvent;
use GNexus\GAuth\Exception\PkceException;
use GNexus\GAuth\Exception\StateValidationException;
use GNexus\GAuth\OAuth\AuthorizationUrlBuilder;
use GNexus\GAuth\OAuth\PkceGenerator;
use GNexus\GAuth\Support\SystemClock;
final class GAuthClient
{
private readonly AuthorizationUrlBuilder $authorizationUrlBuilder;
private readonly ClockInterface $clock;
public function __construct(
private readonly GAuthConfig $config,
private readonly TokenEndpointInterface $tokenEndpoint,
private readonly RuntimeUserProviderInterface $runtimeUserProvider,
private readonly WebhookVerifierInterface $webhookVerifier,
private readonly WebhookParserInterface $webhookParser,
private readonly StateStoreInterface $stateStore,
private readonly PkceStoreInterface $pkceStore,
?ClockInterface $clock = null,
?AuthorizationUrlBuilder $authorizationUrlBuilder = null,
) {
$this->clock = $clock ?? new SystemClock();
$this->authorizationUrlBuilder = $authorizationUrlBuilder ?? new AuthorizationUrlBuilder($config);
}
public function buildAuthorizationRequest(?string $returnTo = null, array $scopes = []): AuthorizationRequest
{
$state = PkceGenerator::generateState();
$verifier = PkceGenerator::generateVerifier();
$challenge = PkceGenerator::generateChallenge($verifier);
$expiresAt = $this->clock->now()->modify(sprintf('+%d seconds', $this->config->stateTtlSeconds()));
$this->stateStore->put($state, $expiresAt, [
'return_to' => $returnTo,
'scopes' => array_values($scopes),
]);
$this->pkceStore->put($state, $verifier, $expiresAt);
$url = $this->authorizationUrlBuilder->build(
state: $state,
pkceChallenge: $challenge,
returnTo: $returnTo,
scopes: $scopes,
);
return new AuthorizationRequest(
authorizationUrl: $url,
state: $state,
pkceVerifier: $verifier,
pkceChallenge: $challenge,
scopes: array_values($scopes),
returnTo: $returnTo,
);
}
public function exchangeAuthorizationCode(string $code, string $state): TokenSet
{
if (! $this->stateStore->has($state)) {
throw new StateValidationException('Unknown or expired authorization state.');
}
$verifier = $this->pkceStore->get($state);
if ($verifier === null || $verifier === '') {
throw new PkceException('Missing PKCE verifier for authorization callback.');
}
$tokenSet = $this->tokenEndpoint->exchangeAuthorizationCode($code, $verifier);
$this->stateStore->forget($state);
$this->pkceStore->forget($state);
return $tokenSet;
}
public function refreshToken(string $refreshToken): TokenSet
{
return $this->tokenEndpoint->refreshToken($refreshToken);
}
public function revokeToken(string $token, ?string $tokenTypeHint = null): void
{
$this->tokenEndpoint->revokeToken($token, $tokenTypeHint);
}
public function fetchUser(string $accessToken): AuthenticatedUser
{
return $this->runtimeUserProvider->fetchUser($accessToken);
}
public function verifyWebhook(string $rawBody, array $headers, string $secret): VerifiedWebhook
{
return $this->webhookVerifier->verify($rawBody, $headers, $secret);
}
public function parseWebhook(string $rawBody): WebhookEvent
{
return $this->webhookParser->parse($rawBody);
}
public function verifyAndParseWebhook(string $rawBody, array $headers, string $secret): WebhookEvent
{
$this->verifyWebhook($rawBody, $headers, $secret);
return $this->parseWebhook($rawBody);
}
}