Newer
Older
smart-home-server / server / SHServ / Integrations / GAuth / AuthService.php
@Eugene Sukhodolskiy Eugene Sukhodolskiy 22 hours ago 5 KB Phase 0: gnexus-auth integration infrastructure
<?php

declare(strict_types=1);

namespace SHServ\Integrations\GAuth;

use GNexus\GAuth\Client\GAuthClient;
use GNexus\GAuth\Config\GAuthConfig;
use GNexus\GAuth\DTO\AuthenticatedUser;
use GNexus\GAuth\DTO\TokenSet;
use GNexus\GAuth\OAuth\HttpTokenEndpoint;
use GNexus\GAuth\Runtime\HttpRuntimeUserProvider;
use GNexus\GAuth\Webhook\HmacWebhookVerifier;
use GNexus\GAuth\Webhook\JsonWebhookParser;
use SHServ\Integrations\GAuth\Http\CurlHttpClient;
use SHServ\Integrations\GAuth\Http\Psr7Factory;
use SHServ\Integrations\GAuth\Store\DbTokenStore;
use SHServ\Integrations\GAuth\Store\SessionPkceStore;
use SHServ\Integrations\GAuth\Store\SessionStateStore;

final class AuthService
{
    private GAuthClient $client;
    private GAuthConfig $config;

    public function __construct()
    {
        $cfg = FCONF['gauth'];
        $this->config = new GAuthConfig(
            baseUrl: $cfg['base_url'],
            clientId: $cfg['client_id'],
            clientSecret: $cfg['client_secret'],
            redirectUri: $cfg['redirect_uri'],
            userAgent: 'shserv/' . (FCONF['version'] ?? '0.4'),
        );

        $httpClient = new CurlHttpClient();
        $factory = new Psr7Factory();

        $this->client = new GAuthClient(
            config: $this->config,
            tokenEndpoint: new HttpTokenEndpoint($this->config, $httpClient, $factory, $factory),
            runtimeUserProvider: new HttpRuntimeUserProvider($this->config, $httpClient, $factory),
            webhookVerifier: new HmacWebhookVerifier($this->config),
            webhookParser: new JsonWebhookParser(),
            stateStore: new SessionStateStore(),
            pkceStore: new SessionPkceStore(),
        );
    }

    public function getClient(): GAuthClient
    {
        return $this->client;
    }

    public function getConfig(): GAuthConfig
    {
        return $this->config;
    }

    /**
     * Build authorization URL and redirect user to gnexus-auth.
     */
    public function buildLoginUrl(?string $returnTo = null): string
    {
        $authRequest = $this->client->buildAuthorizationRequest(
            returnTo: $returnTo,
            scopes: ['openid', 'email', 'profile', 'roles', 'permissions'],
        );

        return $authRequest->authorizationUrl;
    }

    /**
     * Exchange authorization code for tokens and fetch user info.
     */
    public function handleCallback(string $code, string $state): AuthenticatedUser
    {
        $tokenSet = $this->client->exchangeAuthorizationCode($code, $state);
        $user = $this->client->fetchUser($tokenSet->accessToken);

        // Persist tokens
        $sessionToken = bin2hex(random_bytes(32));
        $_SESSION['shserv_auth_token'] = $sessionToken;
        $_SESSION['shserv_access_token'] = $tokenSet->accessToken;
        $_SESSION['shserv_user_id'] = $user->userId;

        $dbStore = new DbTokenStore(app()->thin_builder);
        $dbStore->put($sessionToken, $tokenSet);

        return $user;
    }

    /**
     * Logout: revoke token + clear local session.
     */
    public function logout(): void
    {
        $sessionToken = $_SESSION['shserv_auth_token'] ?? null;
        if ($sessionToken) {
            $dbStore = new DbTokenStore(app()->thin_builder);
            $tokenSet = $dbStore->get($sessionToken);
            if ($tokenSet && $tokenSet->refreshToken) {
                try {
                    $this->client->revokeToken($tokenSet->refreshToken, 'refresh_token');
                } catch (\Throwable $e) {
                    // ignore revoke failures during logout
                }
            }
            $dbStore->forget($sessionToken);
        }

        unset(
            $_SESSION['shserv_auth_token'],
            $_SESSION['shserv_access_token'],
            $_SESSION['shserv_user_id'],
            $_SESSION['gauth_state'],
            $_SESSION['gauth_pkce']
        );
    }

    /**
     * Refresh access token using stored refresh token.
     */
    public function refreshAccessToken(string $sessionToken): ?TokenSet
    {
        $dbStore = new DbTokenStore(app()->thin_builder);
        $tokenSet = $dbStore->get($sessionToken);

        if (!$tokenSet || !$tokenSet->refreshToken) {
            return null;
        }

        try {
            $newTokenSet = $this->client->refreshToken($tokenSet->refreshToken);
            $dbStore->put($sessionToken, $newTokenSet);
            $_SESSION['shserv_access_token'] = $newTokenSet->accessToken;
            return $newTokenSet;
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Get current authenticated user from access token in session.
     */
    public function getCurrentUser(): ?AuthenticatedUser
    {
        $accessToken = $_SESSION['shserv_access_token'] ?? null;
        if (!$accessToken) {
            return null;
        }

        try {
            return $this->client->fetchUser($accessToken);
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Verify webhook signature and parse payload.
     */
    public function verifyWebhook(string $rawBody, array $headers): void
    {
        $secret = FCONF['gauth']['webhook_secret'] ?? '';
        if ($secret === '') {
            throw new \RuntimeException('Webhook secret not configured.');
        }
        $this->client->verifyWebhook($rawBody, $headers, $secret);
    }

    public function parseWebhook(string $rawBody): \GNexus\GAuth\DTO\WebhookEvent
    {
        return $this->client->parseWebhook($rawBody);
    }
}