Skip to content

fix: recover addon store oauth client id#1601

Merged
superdav42 merged 1 commit into
mainfrom
fix-addon-store-client-id
Jul 1, 2026
Merged

fix: recover addon store oauth client id#1601
superdav42 merged 1 commit into
mainfrom
fix-addon-store-client-id

Conversation

@superdav42

@superdav42 superdav42 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a legacy OAuth credential decryption fallback so source-package installs can still populate the addon-store client_id.
  • Harden malformed credential payload handling to return an empty string predictably.
  • Add regression coverage that asserts the addon-store OAuth URL includes a non-empty client_id and supports both current and legacy decryption keys.

Investigation

  • Confirmed the v2.13.1 release ZIP decrypts packaged credentials successfully.
  • Confirmed source/tag packages decrypt to an empty client ID without the fallback, which matches the reported invalid_client / No client id supplied authorize response.

Verification

  • vendor/bin/phpcs inc/class-addon-repository.php tests/WP_Ultimo/Addon_Repository_Test.php
  • vendor/bin/phpunit --filter Addon_Repository_Test
  • git diff --check

aidevops.sh v3.31.17 plugin for OpenCode v1.17.13 with gpt-5.5 spent 19m and 183,175 tokens on this with the user in an interactive session.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of stored OAuth credentials so older saved data can still be read successfully.
    • Made credential decryption more resilient, reducing the chance of errors when data is incomplete or invalid.
    • OAuth-related values are now handled more reliably during retrieval.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds legacy OAuth credential decryption support to Addon_Repository by introducing a legacy credential key constant and reworking decrypt_value() to iterate through multiple candidate keys with stricter validation. Corresponding test helpers and new tests validate both current and legacy key decryption paths.

Changes

Multi-key decryption support

Layer / File(s) Summary
Legacy key constant and candidate key list
inc/class-addon-repository.php
Adds a LEGACY_CREDENTIAL_KEY constant and a get_decryption_keys() helper that builds, deduplicates, and filters candidate decryption keys derived from the current file hash and the legacy key.
Reworked decrypt_value() with multi-key iteration
inc/class-addon-repository.php
Rewrites decrypt_value() to strictly base64-decode input, validate AES-CBC IV length, fail closed with an empty string on invalid input, and try each candidate key until decryption succeeds.
Test helpers and decryption test coverage
tests/WP_Ultimo/Addon_Repository_Test.php
Adds helpers to parse OAuth query args and encrypt payloads matching the repository's scheme, updates the client_id assertion to use the parser, adds tests for current-key and legacy-key decryption (skipping when OpenSSL is unavailable), and reformats several URL literal assignments.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant AddonRepository as Addon_Repository
    participant KeyBuilder as get_decryption_keys
    participant OpenSSL

    Caller->>AddonRepository: decrypt_value(encoded_payload)
    AddonRepository->>AddonRepository: base64_decode (strict)
    AddonRepository->>AddonRepository: validate IV length
    AddonRepository->>KeyBuilder: get_decryption_keys()
    KeyBuilder-->>AddonRepository: [current_key, legacy_key]
    loop for each candidate key
        AddonRepository->>OpenSSL: openssl_decrypt(ciphertext, key, iv)
        OpenSSL-->>AddonRepository: plaintext or failure
    end
    AddonRepository-->>Caller: first successful plaintext or ''
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: recovering addon-store OAuth client_id via legacy credential decryption fallback.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-addon-store-client-id

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42 superdav42 merged commit 59fbd36 into main Jul 1, 2026
10 of 11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Summary

  • Add a legacy OAuth credential decryption fallback so source-package installs can still populate the addon-store client_id.
  • Harden malformed credential payload handling to return an empty string predictably.
  • Add regression coverage that asserts the addon-store OAuth URL includes a non-empty client_id and supports both current and legacy decryption keys.

Investigation

  • Confirmed the v2.13.1 release ZIP decrypts packaged credentials successfully.
  • Confirmed source/tag packages decrypt to an empty client ID without the fallback, which matches the reported invalid_client / No client id supplied authorize response.

Verification

  • vendor/bin/phpcs inc/class-addon-repository.php tests/WP_Ultimo/Addon_Repository_Test.php
  • vendor/bin/phpunit --filter Addon_Repository_Test
  • git diff --check

aidevops.sh v3.31.17 plugin for OpenCode v1.17.13 with gpt-5.5 spent 19m and 183,175 tokens on this with the user in an interactive session.


Merged via PR #1601 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/class-addon-repository.php`:
- Line 59: The conditional in the Addon repository code has a non-Yoda length
comparison, which violates the production PHP coding guideline. Update the
length check in the relevant branch of the class-addon-repository logic so the
comparison stays in Yoda form, matching the existing style used alongside the
iv_length validation and preserving the same behavior.
- Around line 66-70: The decryption loop in class-addon-repository::decrypt
should not accept the first non-false openssl_decrypt result as valid, since a
wrong key can still produce a string and stop the legacy key fallback too early.
Update the encryption/decryption flow to include an authenticated/versioned
marker in the stored addon credentials, and in the decrypt path verify that
marker before returning the plaintext; reject any payload missing it and
continue trying the remaining keys in get_decryption_keys().

In `@tests/WP_Ultimo/Addon_Repository_Test.php`:
- Around line 262-264: The test around Addon_Repository_Test::test_... is only
asserting that the decrypted result is a string, which does not verify the
fail-closed behavior. Update the assertion to check the actual outcome of
Addon_Repository::decrypt_credentials (or the invoked method on $this->repo) for
malformed payloads, and assert that it returns an empty string rather than any
string value. Keep the existing invalid input setup, but replace the broad type
check with a strict value assertion for '' so the test matches the hardened
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16a817fc-d999-45c5-990f-83b7e41dc3e2

📥 Commits

Reviewing files that changed from the base of the PR and between 6847654 and 884e717.

📒 Files selected for processing (2)
  • inc/class-addon-repository.php
  • tests/WP_Ultimo/Addon_Repository_Test.php


$iv_length = openssl_cipher_iv_length('aes-256-cbc');

if (false === $iv_length || strlen($data) <= $iv_length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the length comparison in Yoda form.

Line 59 introduces a non-Yoda comparison in production code.

Proposed fix
-		if (false === $iv_length || strlen($data) <= $iv_length) {
+		if (false === $iv_length || $iv_length >= strlen($data)) {

As per coding guidelines, "inc/**/*.php: Yoda conditions are required in production code."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (false === $iv_length || strlen($data) <= $iv_length) {
if (false === $iv_length || $iv_length >= strlen($data)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/class-addon-repository.php` at line 59, The conditional in the Addon
repository code has a non-Yoda length comparison, which violates the production
PHP coding guideline. Update the length check in the relevant branch of the
class-addon-repository logic so the comparison stays in Yoda form, matching the
existing style used alongside the iv_length validation and preserving the same
behavior.

Source: Coding guidelines

Comment on lines +66 to +70
foreach ($this->get_decryption_keys() as $key) {
$decrypted = openssl_decrypt($cipher_text, 'aes-256-cbc', $key, 0, $iv);

if (false !== $decrypted) {
return $decrypted;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and nearby references to decryption / key handling.
git ls-files | rg '^inc/class-addon-repository\.php$|^inc/.*\.php$|^README\.md$|^composer\.json$'

echo '--- outline: inc/class-addon-repository.php ---'
ast-grep outline inc/class-addon-repository.php --view expanded || true

echo '--- relevant symbols / strings ---'
rg -n "get_decryption_keys|openssl_decrypt|aes-256-cbc|decrypt|cipher_text|iv|credential|license|token|payload|base64|json_decode|unserialize" inc/class-addon-repository.php inc -g '*.php' -g '*.md' | sed -n '1,240p'

echo '--- target file excerpt ---'
sed -n '1,220p' inc/class-addon-repository.php | cat -n

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 50393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- inc/class-addon-repository.php (lines 1-220) ---'
sed -n '1,220p' inc/class-addon-repository.php | cat -n

echo '--- encryption / storage references ---'
rg -n "LEGACY_CREDENTIAL_KEY|decrypt_value\\(|openssl_encrypt|save_access_token|wu-save-option|wu-refresh-token|client_id|client_secret|addon repository|oauth/token" inc/class-addon-repository.php inc -g '*.php' | sed -n '1,220p'

echo '--- credential usage in request construction ---'
sed -n '220,360p' inc/class-addon-repository.php | cat -n

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 25528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on payload shape and any validation helpers that might constrain decrypted text.
rg -n "client_id|client_secret|refresh_token|api_key|api_secret|credential|encrypt|decrypt|openssl_" inc -g '*.php' | sed -n '1,260p'

echo '--- composer / runtime hints ---'
sed -n '1,220p' composer.json | cat -n

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 38096


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- inc/stuff.php ---'
sed -n '1,120p' inc/stuff.php | cat -n

echo '--- encryption helper details ---'
sed -n '1,220p' inc/helpers/class-credential-store.php | cat -n

echo '--- any format validation for addon OAuth credentials ---'
rg -n "client_id.*regex|client_secret.*regex|oauth.*format|credential.*format|validate.*client_id|validate.*client_secret|sanitize_text_field\\(.*client_id|sanitize_text_field\\(.*client_secret" inc -g '*.php' | sed -n '1,200p'

Repository: Ultimate-Multisite/ultimate-multisite

Length of output: 7900


Add an authenticated marker before accepting decrypted addon credentials.
openssl_decrypt() can return a non-false string for the wrong key, which makes the loop stop before the legacy key is tried. These OAuth values are opaque, so a format check won’t reliably catch it; use a versioned/authenticated prefix (or an AEAD mode) and reject payloads without it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/class-addon-repository.php` around lines 66 - 70, The decryption loop in
class-addon-repository::decrypt should not accept the first non-false
openssl_decrypt result as valid, since a wrong key can still produce a string
and stop the legacy key fallback too early. Update the encryption/decryption
flow to include an authenticated/versioned marker in the stored addon
credentials, and in the decrypt path verify that marker before returning the
plaintext; reject any payload missing it and continue trying the remaining keys
in get_decryption_keys().

Comment on lines +262 to 264
$result = $method->invoke($this->repo, base64_encode($fake_data)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// Result will be empty string or decrypted string (likely empty since data is invalid)
$this->assertIsString($result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the fail-closed result, not just the type.

The PR hardens malformed credential payloads to return '', but this test still allows any string.

Proposed fix
 		$result    = $method->invoke($this->repo, base64_encode($fake_data)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
-		// Result will be empty string or decrypted string (likely empty since data is invalid)
-		$this->assertIsString($result);
+		// Malformed payloads should fail closed.
+		$this->assertSame('', $result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$result = $method->invoke($this->repo, base64_encode($fake_data)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// Result will be empty string or decrypted string (likely empty since data is invalid)
$this->assertIsString($result);
$result = $method->invoke($this->repo, base64_encode($fake_data)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
// Malformed payloads should fail closed.
$this->assertSame('', $result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WP_Ultimo/Addon_Repository_Test.php` around lines 262 - 264, The test
around Addon_Repository_Test::test_... is only asserting that the decrypted
result is a string, which does not verify the fail-closed behavior. Update the
assertion to check the actual outcome of Addon_Repository::decrypt_credentials
(or the invoked method on $this->repo) for malformed payloads, and assert that
it returns an empty string rather than any string value. Keep the existing
invalid input setup, but replace the broad type check with a strict value
assertion for '' so the test matches the hardened behavior.

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

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant