Skip to content

Feature/pre 3563 unified auth upc - #306

Open
adumont-payplug wants to merge 5 commits into
developfrom
feature/PRE-3563_unified_auth_upc
Open

Feature/pre 3563 unified auth upc#306
adumont-payplug wants to merge 5 commits into
developfrom
feature/PRE-3563_unified_auth_upc

Conversation

@adumont-payplug

@adumont-payplug adumont-payplug commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Description

Migrates this plugin's OAuth2 connection flow — the interactive authorization-code+PKCE login (UnifiedAuthenticationController) and the background client-credentials token refresh (PayPlugApiClientFactory) — off the legacy payplug/payplug-php Authentication class and onto payplug/unified-plugin-core's new Auth/OAuth2Client + Auth/TokenManager.

Two new adapters bridge UPC's contracts to this plugin's existing infrastructure:

  • SyliusOAuthHttpClient implements UPC's IOAuthHttpClient via Symfony's HttpClientInterface. It also catches TransportExceptionInterface (network-level failures — DNS, timeout, connection reset) rather than letting them escape uncaught, so a transient network blip is reported the same way a non-2xx response from PayPlug is: as an ApiException from OAuth2Client, caught and translated by PayPlugApiClientFactory into GatewayConfigurationException — matching the resilience the legacy Authentication::generateJWT() had (it caught all exceptions internally).
  • SyliusTokenCache implements UPC's ITokenCache via the PSR-6 CacheItemPoolInterface, sanitizing cache keys since PSR-6 rejects {}()/\@: and TokenManager's own key format contains a colon.

createClientIdAndSecret() (portal client provisioning, still called from oauthCallback() after the token exchange) is untouched — out of scope, still going through the legacy SDK.

This PR also adds a SonarCloud code coverage check to CI, mirroring the setup already in place on payplug/unified-plugin-core. Previously this plugin's sonarcloud job ran static analysis only, with no coverage data for SonarCloud to report or gate on. A new coverage job installs the Sylius test application (PHP 8.2, PCOV) and runs PHPUnit with --coverage-clover, uploading the report as a workflow artifact; sonarcloud now consumes it via sonarcloud-coverage.yml and enforces the Quality Gate. A Dockerfile + make coverage target were added so coverage can also be generated locally (PHP 8.2 + PCOV in a container), since host PHP setups commonly lack a coverage driver for the required PHP version.

Motivation:

The legacy flow had two real defects this closes, not just an internal refactor:

  1. Authentication::initiateOAuth() did a raw header('Location: ...') call, forcing setupRedirection() to scrape headers_list() for a Location: line to recover the URL. OAuth2Client::buildAuthorizationUrl() returns the URL directly instead.
  2. The legacy SDK generated an OAuth state internally but never returned it to the caller — oauthCallback() had no CSRF protection at all on the OAuth callback. This PR stores state (and code_verifier) in session from buildAuthorizationUrl()'s return value and rejects the callback outright on a mismatch, before any token exchange happens.

It also adds the first automated test coverage this connection flow has ever had — UnifiedAuthenticationControllerTest, PayPlugApiClientFactoryTest, plus tests for both new adapters, including a case covering the transport-failure handling in SyliusOAuthHttpClient. (Full happy-path coverage of oauthCallback() still stops at the createClientIdAndSecret() boundary — those are static calls on the legacy SDK and unmockable; see the docblock on UnifiedAuthenticationControllerTest.)

Separately, the coverage-check addition to CI is meant to catch coverage regressions on new code going forward — worth noting current overall src/ coverage is only ~22% (693/3,111 statements), so enforce-quality-gate: true on the sonarcloud job may start blocking PRs sooner than expected until that baseline improves.

Related issue(s): PRE-3563


Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) — missing OAuth CSRF protection; unhandled transport-level exceptions in the background token refresh path
  • ✨ New feature (non-breaking change that adds functionality) — SonarCloud coverage check in CI
    [ ] 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs)
  • 🔧 Refactor (no functional changes, code improvement only)
  • 📦 Dependency update
  • 🔒 Security fix
  • 📝 Documentation update — README badge, CLAUDE.md, PR template checklist

Checklist

Code Quality

  • Code is linted and formatted
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config)

Testing

  • Unit tests added / updated
  • New/changed code is covered by tests — SonarCloud Quality Gate (coverage on new code) passes on the sonarcloud CI job

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

Files changed

OAuth2/PKCE migration:

  • src/Action/Admin/Auth/UnifiedAuthenticationController.php — built on OAuth2Client, state/PKCE stored in session
  • src/ApiClient/PayPlugApiClientFactory.php — background token retrieval via TokenManager::getValidToken()
  • src/Auth/SyliusOAuthHttpClient.php — new IOAuthHttpClient adapter; catches TransportExceptionInterface so network failures translate into ApiException/GatewayConfigurationException instead of an uncaught exception reaching payment processing (capture/refund/IPN/status)
  • src/Auth/SyliusTokenCache.php — new ITokenCache adapter

Coverage check in CI:

  • .github/workflows/ci.yml — new coverage job; sonarcloud job switched to sonarcloud-coverage.yml@main with enforce-quality-gate: true
  • composer.json — new test-coverage script
  • phpunit.xml.dist<coverage><include> restricted to src/
  • Dockerfile + Makefilemake coverage runs PHPUnit w/ PCOV in a container, for local parity with CI
  • README.md — added SonarCloud Coverage badge
  • .github/PULL_REQUEST_TEMPLATE.md — added coverage/Quality Gate checklist item

Before merging, please double-check:

  • Given the ~22% baseline coverage noted above, confirm the SonarCloud Quality Gate thresholds for this project are configured for "coverage on new code," not overall coverage — otherwise enforce-quality-gate: true will fail this and most future PRs outright.
  • Consider splitting this into two PRs (auth migration vs. CI coverage check) — they're unrelated in scope and a security-sensitive change is easier to review in isolation.

Copilot AI review requested due to automatic review settings July 20, 2026 13:25

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 21, 2026 14:43
@adumont-payplug
adumont-payplug force-pushed the feature/PRE-3563_unified_auth_upc branch from a8be416 to 25beb1e Compare July 21, 2026 14:43
claude[bot]

This comment was marked as low quality.

This comment was marked as outdated.

@adumont-payplug
adumont-payplug changed the base branch from master to develop July 22, 2026 09:47
Copilot AI review requested due to automatic review settings July 22, 2026 10:06
@wiz-14d684d7a6

wiz-14d684d7a6 Bot commented Jul 22, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations 10 Medium 3 Low
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total 10 Medium 3 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 22, 2026 13:30
@adumont-payplug adumont-payplug self-assigned this Jul 22, 2026

This comment was marked as outdated.

@adumont-payplug

Copy link
Copy Markdown
Collaborator Author

PR #306 Review — Feature/PRE-3563 Unified Auth UPC

Overview

Migrates the plugin's OAuth2 authentication off the legacy payplug/payplug-php Authentication class onto payplug/unified-plugin-core (UPC)'s OAuth2Client/TokenManager, covering both the interactive authorization-code+PKCE admin login (UnifiedAuthenticationController) and the background client-credentials token refresh (PayPlugApiClientFactory). Two new adapter classes (SyliusOAuthHttpClient, SyliusTokenCache) bridge UPC's contracts to Symfony's HttpClientInterface/PSR-6 cache. It also bolts on a SonarCloud coverage-gate CI job, unrelated in scope to the auth work.

Real, meaningful fixes bundled in here:

  • CSRF gap closed: the legacy flow generated an OAuth state but never validated it on callback. This PR stores state/code_verifier in session and rejects a mismatch before any token exchange.
  • Redirect URL is now returned directly by buildAuthorizationUrl() instead of being scraped out of headers_list().

Code quality & style

  • Consistent declare(strict_types=1), typed properties, and PSR-6 sanitization documented with a clear "why" comment (SyliusTokenCache::sanitizeKey) — regex correctly covers all PSR-6 reserved characters ({}()/\@:), verified against the combined-characters test case.
  • SyliusOAuthHttpClient correctly distinguishes transport-level failures (TransportExceptionInterface, thrown regardless of getContent(false)) from HTTP-status failures (suppressed via the false flag) — a subtlety that's easy to get wrong, called out with an accurate comment.
  • Removed dead imports (ItemInterface, CacheInterface from UnifiedAuthenticationController) cleanly alongside the logic they supported.

Specific issues

1. Inconsistent guard against expired-session TypeErrors (oauthCallback(), [UnifiedAuthenticationController.php:86-98](src/Action/Admin/Auth/UnifiedAuthenticationController.php))

The PR explicitly adds a check for a missing/non-string $codeVerifier specifically to avoid a TypeError bubbling up and being logged as a noisy critical for what's really just an expired session (per the test docblock at line 818-823). But $clientId is read from the exact same session, one line earlier, with no equivalent guard:

/** @var string $clientId */
$clientId = $request->getSession()->get('payplug_client_id');
...
$token = $this->buildOAuth2Client($callback)->exchangeAuthorizationCode($clientId, $code, $codeVerifier);

If the session expired (or setupRedirection() was never hit), $clientId is null, and passing it into exchangeAuthorizationCode(string $clientId, ...) throws a TypeError under strict_types=1 — the exact failure mode the codeVerifier check was added to prevent. This should get the same !\is_string($clientId) || '' === $clientId check, thrown as the same BadRequestHttpException before the exchange call, for consistency and to keep the "expired session" case out of critical logs.

2. Two unrelated changes bundled in one PR

The PR description itself flags this and suggests splitting — I'd second that. The OAuth/PKCE migration is security-sensitive and benefits from an isolated, focused review; the CI coverage-gate change is unrelated in scope and carries its own risk (per the description, current src/ coverage is ~22%, and enforce-quality-gate: true could start blocking unrelated future PRs). Bundling them also means a revert of one drags the other along.

3. Minor: [.gitignore](https://claude.ai/epitaxy/.gitignore) missing trailing newline (.gitignore diff) — cosmetic only.

Test coverage

Good first-ever coverage for this flow:

  • State-mismatch, missing-state, and missing-code-verifier all get dedicated tests confirming the token exchange is never reached ($oauthHttpClient->expects(self::never())->method('post')).
  • SyliusOAuthHttpClient/SyliusTokenCache tests exercise the tricky edge cases directly (transport failure, non-2xx passthrough, key sanitization for every reserved character).
  • PayPlugApiClientFactoryTest correctly builds real OAuth2Client/TokenManager instances (both final, unmockable) and controls behavior at the actual HTTP/cache boundary instead — the right call.
  • The acknowledged gap (happy path of oauthCallback() past createClientIdAndSecret(), a static legacy SDK call) is honestly documented rather than faked with a brittle mock.

Given finding #1, a test for "missing client_id in session" (mirroring testOauthCallback_withMissingCodeVerifier_rejectsBeforeExchangingToken) should be added once that guard is added.

Security considerations

  • CSRF fix is correct and tested — state stored server-side in session, compared before any exchange occurs.
  • No new secrets/credentials introduced in code or CI config; client_secret continues to flow through the existing gateway-config storage, unchanged by this PR.
  • SyliusOAuthHttpClient correctly avoids leaking transport exceptions uncaught into a code path (PayPlugApiClientFactory, feeding capture/refund/IPN/status) that previously had no exception handling for network blips at all.

Recommendation

Solid migration with a real security fix, well-tested. Before merging: add the missing clientId guard in oauthCallback() (finding #1) for consistency with the codeVerifier handling, and consider splitting the CI coverage-gate change into its own PR as already suggested in the description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants