9484d6bde9
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
- #107: Fix testConnection() broken event dispatch (Joomla 5+ ArrayAccess pattern) and add CSRF + ACL checks - #108: Add CSRF checkToken() to OauthController::authorize() - #109: Add core.manage ACL check to REST dispatch endpoint - #110: Fix LinkedIn null-coalesce on organization_id - #111: Add CURLOPT_PROTOCOLS to webhook, mastodon, ghost, bluesky to prevent SSRF via user-controlled URLs - #112: Encrypt credentials at rest using sodium_crypto_secretbox with key derived from Joomla secret; backward-compat with existing plaintext JSON credentials - #113: Fix unclosed <script> tag in dashboard template - #114: Fix hasPendingWork() to use exponential backoff matching processQueue() instead of linear delay - #115: Fix timestamp lock TOCTOU race with atomic UPDATE + WHERE - #120: Add CSRF token to dashboard migration link Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
199 lines
6.4 KiB
PHP
199 lines
6.4 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoJoomCross
|
|
* @subpackage com_mokojoomcross
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoJoomCross\Administrator\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
use Joomla\CMS\Plugin\PluginHelper;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\Component\MokoJoomCross\Administrator\Helper\OAuthHelper;
|
|
|
|
/**
|
|
* OAuth controller for handling browser-based authorization flows.
|
|
*
|
|
* Endpoints:
|
|
* task=oauth.authorize — Initiate OAuth flow (redirect to platform)
|
|
* task=oauth.callback — Handle platform redirect with auth code
|
|
*/
|
|
class OauthController extends BaseController
|
|
{
|
|
/**
|
|
* Initiate OAuth authorization for a service.
|
|
*
|
|
* Expects: service_id (int) in request
|
|
*/
|
|
public function authorize(): void
|
|
{
|
|
$this->checkToken();
|
|
|
|
$serviceId = $this->input->getInt('service_id', 0);
|
|
|
|
if (!$serviceId) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::_('COM_MOKOJOOMCROSS_OAUTH_NO_SERVICE'),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$db = \Joomla\CMS\Factory::getDbo();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from($db->quoteName('#__mokojoomcross_services'))
|
|
->where($db->quoteName('id') . ' = ' . $serviceId);
|
|
|
|
$db->setQuery($query);
|
|
$service = $db->loadObject();
|
|
|
|
if (!$service) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::_('COM_MOKOJOOMCROSS_OAUTH_SERVICE_NOT_FOUND'),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Get client ID from plugin params
|
|
PluginHelper::importPlugin('mokojoomcross');
|
|
$pluginParams = PluginHelper::getPlugin('mokojoomcross', $service->service_type);
|
|
$params = json_decode($pluginParams->params ?? '{}', true) ?: [];
|
|
|
|
$clientId = $params['client_id'] ?? '';
|
|
|
|
if (empty($clientId)) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::sprintf('COM_MOKOJOOMCROSS_OAUTH_NO_CLIENT_ID', ucfirst($service->service_type)),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Generate CSRF nonce and store in session
|
|
$nonce = bin2hex(random_bytes(16));
|
|
Factory::getApplication()->getSession()->set('mokojoomcross.oauth_nonce', $nonce);
|
|
|
|
$url = OAuthHelper::getAuthorizeUrl($service->service_type, $serviceId, $clientId, $nonce);
|
|
|
|
if (!$url) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::sprintf('COM_MOKOJOOMCROSS_OAUTH_NOT_SUPPORTED', ucfirst($service->service_type)),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->app->redirect($url);
|
|
}
|
|
|
|
/**
|
|
* Handle OAuth callback from platform.
|
|
*
|
|
* Expects: code (string), state (base64 JSON with service_id)
|
|
*/
|
|
public function callback(): void
|
|
{
|
|
$code = $this->input->getString('code', '');
|
|
$state = $this->input->getString('state', '');
|
|
$error = $this->input->getString('error', '');
|
|
|
|
if ($error) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::sprintf('COM_MOKOJOOMCROSS_OAUTH_PLATFORM_ERROR', $error),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if (empty($code) || empty($state)) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::_('COM_MOKOJOOMCROSS_OAUTH_INVALID_CALLBACK'),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$stateData = json_decode(base64_decode($state), true);
|
|
$serviceId = (int) ($stateData['service_id'] ?? 0);
|
|
$serviceType = $stateData['type'] ?? '';
|
|
$stateNonce = $stateData['nonce'] ?? '';
|
|
|
|
if (!$serviceId || !$serviceType) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::_('COM_MOKOJOOMCROSS_OAUTH_INVALID_STATE'),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// CSRF nonce validation — compare state nonce against session
|
|
$session = Factory::getApplication()->getSession();
|
|
$sessionNonce = $session->get('mokojoomcross.oauth_nonce', '');
|
|
$session->clear('mokojoomcross.oauth_nonce');
|
|
|
|
if (empty($stateNonce) || !hash_equals($sessionNonce, $stateNonce)) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&view=services', false),
|
|
Text::_('COM_MOKOJOOMCROSS_OAUTH_INVALID_STATE'),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Get client credentials from plugin params
|
|
PluginHelper::importPlugin('mokojoomcross');
|
|
$pluginParams = PluginHelper::getPlugin('mokojoomcross', $serviceType);
|
|
$params = json_decode($pluginParams->params ?? '{}', true) ?: [];
|
|
|
|
$clientId = $params['client_id'] ?? '';
|
|
$clientSecret = $params['client_secret'] ?? '';
|
|
|
|
$tokenData = OAuthHelper::exchangeCode($serviceType, $code, $clientId, $clientSecret);
|
|
|
|
if (!empty($tokenData['error'])) {
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&task=service.edit&id=' . $serviceId, false),
|
|
Text::sprintf('COM_MOKOJOOMCROSS_OAUTH_TOKEN_ERROR', $tokenData['error']),
|
|
'error'
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
OAuthHelper::storeToken($serviceId, $tokenData);
|
|
|
|
$this->setRedirect(
|
|
Route::_('index.php?option=com_mokojoomcross&task=service.edit&id=' . $serviceId, false),
|
|
Text::sprintf('COM_MOKOJOOMCROSS_OAUTH_SUCCESS', ucfirst($serviceType)),
|
|
'success'
|
|
);
|
|
}
|
|
}
|