* @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' ); } }