diff --git a/app/code/core/Mage/Api2/Model/Auth/Adapter/Oauth2.php b/app/code/core/Mage/Api2/Model/Auth/Adapter/Oauth2.php new file mode 100644 index 00000000000..c6e1e89498d --- /dev/null +++ b/app/code/core/Mage/Api2/Model/Auth/Adapter/Oauth2.php @@ -0,0 +1,82 @@ + null, 'id' => null]; + + try { + $token = $this->_validateToken($request); + $userType = $token->getUserType(); + + if ($userType === 'admin') { + $userParamsObj->id = $token->getAdminId(); + } else { + $userParamsObj->id = $token->getCustomerId(); + } + $userParamsObj->type = $userType; + } catch (Exception $e) { + throw new Mage_Api2_Exception($e->getMessage(), Mage_Api2_Model_Server::HTTP_UNAUTHORIZED); + } + + return $userParamsObj; + } + + /** + * Validate the OAuth2 token + * + * @return Mage_Oauth2_Model_AccessToken + * @throws Exception + */ + protected function _validateToken(Mage_Api2_Model_Request $request) + { + $authorizationHeader = $request->getHeader('Authorization'); + if (!$authorizationHeader || strpos($authorizationHeader, 'Bearer ') !== 0) { + throw new Exception('Missing or invalid Authorization header'); + } + + $accessToken = substr($authorizationHeader, 7); + $token = Mage::getModel('oauth2/accessToken')->load($accessToken, 'access_token'); + if (!$token->getId() || $token->getExpiresIn() < time() || $token->getRevoked()) { + throw new Exception('Invalid or expired access token'); + } + + return $token; + } + + /** + * Check if request contains authentication info for adapter + * + * @return bool + */ + public function isApplicableToRequest(Mage_Api2_Model_Request $request) + { + $headerValue = $request->getHeader('Authorization'); + return $headerValue && strtolower(substr($headerValue, 0, 7)) === 'bearer '; + } +} diff --git a/app/code/core/Mage/Api2/etc/config.xml b/app/code/core/Mage/Api2/etc/config.xml index 5b963c45995..e8868284e48 100644 --- a/app/code/core/Mage/Api2/etc/config.xml +++ b/app/code/core/Mage/Api2/etc/config.xml @@ -97,6 +97,12 @@ 1 10 + + api2/auth_adapter_oauth2 + + 1 + 20 + diff --git a/app/code/core/Mage/Oauth2/Block/Adminhtml/Client.php b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client.php new file mode 100644 index 00000000000..2bb96cdbba0 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client.php @@ -0,0 +1,21 @@ +_blockGroup = 'oauth2'; + $this->_controller = 'adminhtml_client'; + + $this->_headerText = $this->__('Manage OAuth2 Clients'); + $this->_addButtonLabel = $this->__('Add New Client'); + + parent::__construct(); + } +} diff --git a/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit.php b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit.php new file mode 100644 index 00000000000..75d5ff7f779 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit.php @@ -0,0 +1,78 @@ +_blockGroup = 'oauth2'; + $this->_controller = 'adminhtml_client'; + $this->_mode = 'edit'; + + $this->_updateButton('save', 'label', $this->__('Save')); + $this->_updateButton('save', 'id', 'save_button'); + $this->_updateButton('delete', 'label', $this->__('Delete')); + $this->_updateButton('delete', 'onclick', 'if(confirm(\'' . Mage::helper('core')->jsQuoteEscape( + $this->__('Are you sure you want to do this?') + ) . '\')) editForm.submit(\'' . $this->getUrl('*/*/delete', ['id' => $this->getModel()->getId()]) . '\'); return false;'); + + if (!$this->_isAllowedAction('delete')) { + $this->_removeButton('delete'); + } + + $this->_addButton('save_and_continue', [ + 'label' => $this->__('Save and Continue Edit'), + 'onclick' => 'saveAndContinueEdit()', + 'class' => 'save' + ], 100); + + $this->_formScripts[] = 'function saveAndContinueEdit()' . + "{editForm.submit($('edit_form').action + 'back/edit/');}"; + } + + /** + * Prepares the layout for the block. + * + */ + public function getHeaderText() + { + return $this->getModel()->getId() + ? $this->__("Edit Client '%s'", $this->escapeHtml($this->getModel()->getName())) + : $this->__('New Client'); + } + + /** + * Check if the current user is allowed to perform the specified action. + * + * @param string $action The action to check. + * @return bool Returns true if the user is allowed, false otherwise. + */ + protected function _isAllowedAction($action) + { + return Mage::getSingleton('admin/session')->isAllowed('system/oauth2/client/' . $action); + } + + /** + * Retrieves the model object from the registry if it is not already set. + * + * @return mixed The model object from the registry. + */ + protected function getModel() + { + if (null === $this->_model) { + $this->_model = Mage::registry('current_oauth2_client'); + } + return $this->_model; + } +} diff --git a/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit/Form.php b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit/Form.php new file mode 100644 index 00000000000..46a6e69de75 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Edit/Form.php @@ -0,0 +1,86 @@ + 'edit_form', + 'action' => $this->getData('action'), + 'method' => 'post' + ] + ); + + $fieldset = $form->addFieldset('base_fieldset', [ + 'legend' => $this->__('Client Information'), + 'class' => 'fieldset-wide' + ]); + + $fieldset->addType('text', Mage::getConfig()->getBlockClassName('oauth2/adminhtml_text')); + + $fieldset->addField('name', 'text', [ + 'label' => $this->__('Client Name'), + 'name' => 'name', + 'required' => true, + 'value' => $this->getModel()->getName(), + ]); + $fieldset->addField('secret', 'text', [ + 'label' => $this->__('Client Secret'), + 'name' => 'secret', + 'required' => true, + 'disabled' => true, + 'data-copy-text' => $this->getModel()->getSecret(), + 'value' => $this->getModel()->getSecret(), + ]); + + $fieldset->addField('redirect_uri', 'text', [ + 'label' => $this->__('Redirect URI'), + 'name' => 'redirect_uri', + 'required' => true, + 'value' => $this->getModel()->getRedirectUri(), + ]); + $fieldset->addField('grant_types', 'multiselect', [ + 'label' => $this->__('Grant Types'), + 'class' => 'required-entry', + 'required' => true, + 'name' => 'grant_types[]', + 'values' => [ + ['value' => 'authorization_code', 'label' => $this->__('Authorization Code')], + ['value' => 'refresh_token', 'label' => $this->__('Refresh Token')], + ], + 'value' => $this->getModel()->getGrantTypes(), + ]); + + $fieldset->addField('current_password', 'obscure', [ + 'name' => 'current_password', + 'label' => $this->__('Current Admin Password'), + 'required' => true + ]); + + $form->setAction($this->getUrl('*/*/save', ['id' => $this->getModel()->getId()])); + $form->setUseContainer(true); + $this->setForm($form); + return parent::_prepareForm(); + } + + /** + * Retrieves the model object from the registry if it is not already set. + * + * @return mixed The model object from the registry. + */ + protected function getModel() + { + if (null === $this->_model) { + $this->_model = Mage::registry('current_oauth2_client'); + } + return $this->_model; + } +} diff --git a/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Grid.php b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Grid.php new file mode 100644 index 00000000000..fb04860e691 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Adminhtml/Client/Grid.php @@ -0,0 +1,102 @@ +setId('oauth2_client_grid') + ->setDefaultSort('entity_id') + ->setDefaultDir('DESC') + ->setSaveParametersInSession(true); + + $this->_editAllow = Mage::getSingleton('admin/session')->isAllowed('system/oauth/consumer/edit'); + } + + /** + * Prepare collection + * + * @return Mage_Oauth2_Block_Adminhtml_Client_Grid + */ + protected function _prepareCollection() + { + $collection = Mage::getModel('oauth2/client')->getCollection(); + $this->setCollection($collection); + return parent::_prepareCollection(); + } + + /** + * Prepare columns + * + * @return Mage_Oauth2_Block_Adminhtml_Client_Grid + */ + protected function _prepareColumns() + { + $this->addColumn('entity_id', [ + 'header' => $this->__('Entity ID'), + 'index' => 'entity_id', + 'type' => 'number', + ]); + + $this->addColumn('secret', [ + 'header' => $this->__('Secret'), + 'index' => 'secret', + ]); + + $this->addColumn('redirect_uri', [ + 'header' => $this->__('Redirect URI'), + 'index' => 'redirect_uri', + ]); + + $this->addColumn('grant_types', [ + 'header' => $this->__('Grant Types'), + 'index' => 'grant_types', + ]); + + $this->addColumn('created_at', [ + 'header' => $this->__('Created At'), + 'index' => 'created_at', + 'type' => 'datetime', + ]); + + $this->addColumn('updated_at', [ + 'header' => $this->__('Updated At'), + 'index' => 'updated_at', + 'type' => 'datetime', + ]); + + return parent::_prepareColumns(); + } + + /** + * Get grid URL + * + * @return string + */ + public function getGridUrl() + { + return $this->getUrl('*/*/grid', ['_current' => true]); + } + + /** + * Get row URL + * + * @param Mage_Core_Model_Abstract $row + * @return string|null + */ + public function getRowUrl($row) + { + return $this->_editAllow ? $this->getUrl('*/*/edit', ['id' => $row->getId()]) : null; + } +} diff --git a/app/code/core/Mage/Oauth2/Block/Adminhtml/Text.php b/app/code/core/Mage/Oauth2/Block/Adminhtml/Text.php new file mode 100644 index 00000000000..be9d38554a2 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Adminhtml/Text.php @@ -0,0 +1,19 @@ +setTemplate('oauth2/authorize.phtml'); + } + + /** + * Get OAuth2 client + * + * @return Mage_Oauth2_Model_Client + */ + public function getClient() + { + $clientId = $this->getRequest()->getParam('client_id'); + return Mage::getModel('oauth2/client')->load($clientId, 'entity_id'); + } + + /** + * Get state parameter + * + * @return string|null + */ + public function getState() + { + return $this->getRequest()->getParam('state'); + } + + /** + * Get redirect URI + * + * @return string|null + */ + public function getRedirectUri() + { + return $this->getRequest()->getParam('redirect_uri'); + } + + /** + * Get form action URL + * + * @return string + */ + public function getFormActionUrl() + { + return $this->getUrl('*/*/index', ['_secure' => true]); + } +} diff --git a/app/code/core/Mage/Oauth2/Block/Device/Verify.php b/app/code/core/Mage/Oauth2/Block/Device/Verify.php new file mode 100644 index 00000000000..6396c84365e --- /dev/null +++ b/app/code/core/Mage/Oauth2/Block/Device/Verify.php @@ -0,0 +1,35 @@ +setTemplate('oauth2/device/verify.phtml'); + } + + /** + * Get form action URL + * + * @return string + */ + public function getFormActionUrl() + { + return $this->getUrl('oauth2/device/authorize'); + } + + /** + * Get user code + * + * @return string|null + */ + public function getUserCode() + { + return Mage::registry('current_device_code'); + } +} diff --git a/app/code/core/Mage/Oauth2/Controller/BaseController.php b/app/code/core/Mage/Oauth2/Controller/BaseController.php new file mode 100644 index 00000000000..d28ecd4ad6a --- /dev/null +++ b/app/code/core/Mage/Oauth2/Controller/BaseController.php @@ -0,0 +1,29 @@ + $code, + 'message' => $message, + ]; + + if ($data !== null) { + $response['data'] = $data; + } + + $this->getResponse() + ->setHttpResponseCode($code) + ->setHeader('Content-Type', 'application/json', true) + ->setBody(json_encode($response)); + } +} diff --git a/app/code/core/Mage/Oauth2/Helper/Data.php b/app/code/core/Mage/Oauth2/Helper/Data.php new file mode 100644 index 00000000000..77c1026bbb8 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Helper/Data.php @@ -0,0 +1,42 @@ +generateRandomString($length); + } + + /** + * Generate a token + * + * @param int $length The length of the token (default: 40) + * @return string The generated token + * @throws Exception if unable to generate random bytes + */ + public function generateToken($length = 40) + { + return $this->generateRandomString($length); + ; + } + + /** + * Generate a random string of specified length + * + * @param int $length The desired length of the string + * @return string The generated random string + * @throws Exception if unable to generate random bytes + */ + private function generateRandomString($length) + { + $bytes = openssl_random_pseudo_bytes((int) ceil($length / 2)); + return substr(bin2hex($bytes), 0, $length); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/AccessToken.php b/app/code/core/Mage/Oauth2/Model/AccessToken.php new file mode 100644 index 00000000000..82ec6335753 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/AccessToken.php @@ -0,0 +1,29 @@ +_init('oauth2/accessToken'); + } + + /** + * Get user type associated with the token + * + * @return string|null + * @throws Mage_Core_Exception + */ + public function getUserType() + { + if ($this->getAdminId()) { + return self::USER_TYPE_ADMIN; + } elseif ($this->getCustomerId()) { + return self::USER_TYPE_CUSTOMER; + } else { + Mage::throwException(Mage::helper('oauth2')->__('User type is unknown')); + } + } +} diff --git a/app/code/core/Mage/Oauth2/Model/AuthCode.php b/app/code/core/Mage/Oauth2/Model/AuthCode.php new file mode 100644 index 00000000000..22377f81eb5 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/AuthCode.php @@ -0,0 +1,9 @@ +_init('oauth2/authCode'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Client.php b/app/code/core/Mage/Oauth2/Model/Client.php new file mode 100644 index 00000000000..d24e7550364 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Client.php @@ -0,0 +1,9 @@ +_init('oauth2/client'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/DeviceCode.php b/app/code/core/Mage/Oauth2/Model/DeviceCode.php new file mode 100644 index 00000000000..5199bdce5c6 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/DeviceCode.php @@ -0,0 +1,9 @@ +_init('oauth2/deviceCode'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/AccessToken.php b/app/code/core/Mage/Oauth2/Model/Resource/AccessToken.php new file mode 100644 index 00000000000..c2c8627b4ab --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/AccessToken.php @@ -0,0 +1,13 @@ +_init('oauth2/access_token', 'access_token'); + $this->_isPkAutoIncrement = false; + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/AccessToken/Collection.php b/app/code/core/Mage/Oauth2/Model/Resource/AccessToken/Collection.php new file mode 100644 index 00000000000..ddfc6e1a8af --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/AccessToken/Collection.php @@ -0,0 +1,12 @@ +_init('oauth2/access_token'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/AuthCode.php b/app/code/core/Mage/Oauth2/Model/Resource/AuthCode.php new file mode 100644 index 00000000000..b528aaff126 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/AuthCode.php @@ -0,0 +1,13 @@ +_init('oauth2/auth_code', 'authorization_code'); + $this->_isPkAutoIncrement = false; + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/AuthCode/Collection.php b/app/code/core/Mage/Oauth2/Model/Resource/AuthCode/Collection.php new file mode 100644 index 00000000000..7875225a299 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/AuthCode/Collection.php @@ -0,0 +1,9 @@ +_init('oauth2/auth_code'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/Client.php b/app/code/core/Mage/Oauth2/Model/Resource/Client.php new file mode 100644 index 00000000000..77664d6e9e1 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/Client.php @@ -0,0 +1,12 @@ +_init('oauth2/client', 'entity_id'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/Client/Collection.php b/app/code/core/Mage/Oauth2/Model/Resource/Client/Collection.php new file mode 100644 index 00000000000..6d9cf73899e --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/Client/Collection.php @@ -0,0 +1,12 @@ +_init('oauth2/client'); + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode.php b/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode.php new file mode 100644 index 00000000000..c38bc39c42f --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode.php @@ -0,0 +1,13 @@ +_init('oauth2/device_code', 'device_code'); + $this->_isPkAutoIncrement = false; + } +} diff --git a/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode/Collection.php b/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode/Collection.php new file mode 100644 index 00000000000..a370ae54625 --- /dev/null +++ b/app/code/core/Mage/Oauth2/Model/Resource/DeviceCode/Collection.php @@ -0,0 +1,12 @@ +_init('oauth2/device_code'); + } +} diff --git a/app/code/core/Mage/Oauth2/controllers/Adminhtml/Oauth2/ClientController.php b/app/code/core/Mage/Oauth2/controllers/Adminhtml/Oauth2/ClientController.php new file mode 100644 index 00000000000..631b119823b --- /dev/null +++ b/app/code/core/Mage/Oauth2/controllers/Adminhtml/Oauth2/ClientController.php @@ -0,0 +1,278 @@ +_clientModel === null) { + $this->_clientModel = Mage::getModel('oauth2/client'); + } + return $this->_clientModel; + } + + /** + * Get admin session + * + * @return Mage_Adminhtml_Model_Session + */ + protected function _getSession() + { + if ($this->_session === null) { + $this->_session = Mage::getSingleton('adminhtml/session'); + } + return $this->_session; + } + + /** + * Pre-dispatch actions + * + * @return Mage_Oauth2_Adminhtml_Oauth2_ClientController + */ + public function preDispatch() + { + $this->_setForcedFormKeyActions(['delete']); + $this->_title($this->__('System')) + ->_title($this->__('OAuth2')) + ->_title($this->__('Clients')); + return parent::preDispatch(); + } + + /** + * Index action - display list of OAuth2 clients + */ + public function indexAction() + { + $this->loadLayout() + ->_addContent($this->getLayout()->createBlock('oauth2/adminhtml_client')) + ->renderLayout(); + } + + /** + * New client action - display form for creating new OAuth2 client + */ + public function newAction() + { + $model = $this->_initClientModel(); + $formData = $this->_getFormData(); + + if ($formData) { + $model->addData($formData); + } else { + $model->setSecret(Mage::helper('oauth2')->generateClientSecret()); + } + + $this->_setFormData($formData ?: $model->getData()); + Mage::register('current_oauth2_client', $model); + + $this->loadLayout() + ->_addContent($this->getLayout()->createBlock('oauth2/adminhtml_client_edit')) + ->renderLayout(); + } + + /** + * Edit client action - display form for editing existing OAuth2 client + */ + public function editAction() + { + $id = $this->getRequest()->getParam('id'); + $model = $this->_initClientModel()->load($id); + + if ($model->getId() || $id == 0) { + Mage::register('current_oauth2_client', $model); + $this->loadLayout() + ->_addContent($this->getLayout()->createBlock('oauth2/adminhtml_client_edit')) + ->renderLayout(); + } else { + $this->_getSession()->addError(Mage::helper('oauth2')->__('Client does not exist')); + $this->_redirect('*/*/'); + } + } + + /** + * Save client action - save new or update existing OAuth2 client + */ + public function saveAction() + { + if (!$this->_validateFormKey()) { + return $this->_redirectToFormPage(); + } + + $data = $this->_filter($this->getRequest()->getParams()); + $id = $this->getRequest()->getParam('id'); + + if (!$this->_validateCurrentPassword($this->getRequest()->getParam('current_password'))) { + return $this->_redirectToFormPage($id); + } + + $model = $this->_initClientModel(); + + if ($id) { + if (!$this->_loadModelById($model, $id)) { + return; + } + } else { + $data['secret'] = $this->_getFormData()['secret'] ?? Mage::helper('oauth2')->generateClientSecret(); + } + + try { + $model->addData($data)->save(); + $this->_getSession()->addSuccess($this->__('The client has been saved.')); + $this->_setFormData(null); + } catch (Mage_Core_Exception $e) { + $this->_handleSaveException($e, $data); + } catch (Exception $e) { + $this->_handleSaveException($e); + } + + $this->_redirectAfterSave($model); + } + + /** + * Delete client action + */ + public function deleteAction() + { + $clientId = $this->getRequest()->getParam('id'); + + if ($clientId) { + try { + $this->_initClientModel()->load($clientId)->delete(); + $this->_getSession()->addSuccess('Client deleted.'); + } catch (Exception $e) { + $this->_getSession()->addError('Error: ' . $e->getMessage()); + } + } else { + $this->_getSession()->addError('Unable to find client to delete.'); + } + + $this->_redirect('*/*/index'); + } + + /** + * Get form data from session + * + * @return mixed + */ + protected function _getFormData() + { + return $this->_getSession()->getData('oauth2_client_data', true); + } + + /** + * Set form data to session + * + * @param mixed $data + */ + protected function _setFormData($data) + { + $this->_getSession()->setData('oauth2_client_data', $data); + } + + /** + * Filter input data + * + * @return array + */ + protected function _filter(array $data) + { + $fieldsToRemove = ['id', 'back', 'form_key', 'secret']; + foreach ($fieldsToRemove as $field) { + unset($data[$field]); + } + + if (isset($data['grant_types'])) { + $data['grant_types'] = implode(',', $data['grant_types']); + } + + return $data; + } + + /** + * Redirect to appropriate form page + * + * @param int|null $id + */ + private function _redirectToFormPage($id = null) + { + $this->_redirect($id ? '*/*/edit' : '*/*/new', ['id' => $id]); + } + + /** + * Load model by ID and validate its existence + * + * @param Mage_Core_Model_Abstract $model + * @param int $id + * @return bool + */ + private function _loadModelById($model, $id) + { + if (!(int) $id) { + $this->_getSession()->addError($this->__('Invalid ID parameter.')); + $this->_redirect('*/*/index'); + return false; + } + + $model->load($id); + + if (!$model->getId()) { + $this->_getSession()->addError($this->__('Entry with ID #%s not found.', $id)); + $this->_redirect('*/*/index'); + return false; + } + + return true; + } + + /** + * Handle exceptions during save action + * + * @param Exception $e + * @param array|null $data + */ + private function _handleSaveException($e, $data = null) + { + if ($data !== null) { + $this->_setFormData($data); + } + $this->_setFormData(null); + $message = $e instanceof Mage_Core_Exception ? Mage::helper('core')->escapeHtml($e->getMessage()) : $this->__('An error occurred on saving client data.'); + $this->_getSession()->addError($message); + $this->getRequest()->setParam('back', 'edit'); + + if ($e instanceof Exception && !($e instanceof Mage_Core_Exception)) { + Mage::logException($e); + } + } + + /** + * Redirect after save action + * + * @param Mage_Core_Model_Abstract $model + */ + private function _redirectAfterSave($model) + { + if ($this->getRequest()->getParam('back')) { + $this->_redirect('*/*/edit', ['id' => $model->getId()]); + } else { + $this->_redirect('*/*/index'); + } + } +} diff --git a/app/code/core/Mage/Oauth2/controllers/AuthorizeController.php b/app/code/core/Mage/Oauth2/controllers/AuthorizeController.php new file mode 100644 index 00000000000..066d5fa2ebf --- /dev/null +++ b/app/code/core/Mage/Oauth2/controllers/AuthorizeController.php @@ -0,0 +1,173 @@ +getRequest()->getParam('client_id'); + $redirectUri = $this->getRequest()->getParam('redirect_uri'); + + if (!$this->_validateParams($clientId, $redirectUri)) { + return; + } + + if ($this->getRequest()->isPost() && $this->getRequest()->getParam('authorized') !== null) { + $this->_processAuthorization(); + return; + } + + if (!$this->_getCustomerSession()->isLoggedIn()) { + $this->_redirect('customer/account/login'); + return; + } + + $this->_renderAuthorizationForm(); + } + + /** + * Validate request parameters + * + * @param string $clientId + * @param string $redirectUri + * @return bool + */ + protected function _validateParams($clientId, $redirectUri) + { + if (!$clientId || !$redirectUri) { + $this->_sendResponse(400, 'Invalid parameters.'); + return false; + } + + $client = Mage::getModel('oauth2/client')->load($clientId, 'entity_id'); + if (!$client->getId()) { + $this->_sendResponse(400, 'Invalid client.'); + return false; + } + if (!$client->getRedirectUri() || $client->getRedirectUri() != $redirectUri) { + $this->_sendResponse(400, 'Invalid redirect_uri.'); + return false; + } + return true; + } + + /** + * Render authorization form + */ + protected function _renderAuthorizationForm() + { + $this->loadLayout(); + $this->getLayout()->getBlock('content')->append( + $this->getLayout()->createBlock('oauth2/authorize', 'oauth2.authorize') + ); + $this->renderLayout(); + } + + /** + * Process authorization + */ + protected function _processAuthorization() + { + try { + $params = $this->getRequest()->getParams(); + $client = $this->_validateClient($params['client_id']); + $customerId = $this->_getCustomerId($params); + + if ($params['authorized'] === 'yes') { + $this->_authorizeClient($client, $customerId, $params['redirect_uri']); + } else { + $this->_sendResponse(401, 'User denied the request.'); + } + } catch (Exception $e) { + $this->_sendResponse(500, $e->getMessage()); + Mage::logException($e); + } + } + + /** + * Validate client + * + * @param string $clientId + * @return Mage_Oauth2_Model_Client + * @throws Exception + */ + protected function _validateClient($clientId) + { + $client = Mage::getModel('oauth2/client')->load($clientId, 'entity_id'); + if (!$client || !$client->getId()) { + throw new Exception('Invalid client.'); + } + return $client; + } + + /** + * Get customer ID + * + * @param array $params + * @return int + * @throws Exception + */ + protected function _getCustomerId($params) + { + if ($this->_getCustomerSession()->isLoggedIn()) { + return $this->_getCustomerSession()->getCustomerId(); + } + + $customer = Mage::getModel('customer/customer')->load($params['customer_id']); + if (!$customer->getId()) { + throw new Exception('Invalid customer.'); + } + if (!empty($params['email']) && $params['email'] != $customer->getEmail()) { + throw new Exception('Invalid customer email.'); + } + + return $customer->getId(); + } + + /** + * Authorize client + * + * @param Mage_Oauth2_Model_Client $client + * @param int $customerId + * @param string $redirectUri + */ + protected function _authorizeClient($client, $customerId, $redirectUri) + { + $authorizationCode = $this->_getOauth2Helper()->generateToken(); + $model = Mage::getModel('oauth2/authCode'); + $model->setAuthorizationCode($authorizationCode) + ->setClientId($client->getId()) + ->setRedirectUri($redirectUri) + ->setCustomerId($customerId) + ->setExpiresIn(time() + 600) + ->save(); + + $redirectUri = $redirectUri . '?code=' . urlencode($authorizationCode); + $this->_redirectUrl($redirectUri); + } + + /** + * Get customer session + * + * @return Mage_Customer_Model_Session + */ + protected function _getCustomerSession() + { + return Mage::getSingleton('customer/session'); + } + + /** + * Get OAuth2 helper + * + * @return Mage_Oauth2_Helper_Data + */ + protected function _getOauth2Helper() + { + return Mage::helper('oauth2'); + } +} diff --git a/app/code/core/Mage/Oauth2/controllers/DeviceController.php b/app/code/core/Mage/Oauth2/controllers/DeviceController.php new file mode 100644 index 00000000000..c8f31f68af3 --- /dev/null +++ b/app/code/core/Mage/Oauth2/controllers/DeviceController.php @@ -0,0 +1,257 @@ +getRequest()->getParam('client_id'); + $client = $this->_loadClient($clientId); + + if (!$client->getId()) { + $this->_sendResponse(400, 'Invalid client'); + return; + } + + $deviceCode = $this->_generateDeviceCode($clientId); + $userCode = $this->_generateUserCode(); + + $this->_saveDeviceCode($deviceCode, $userCode, $clientId); + $this->_sendSuccessResponse($deviceCode, $userCode); + } + + /** + * Authorize action - Process device authorization + */ + public function authorizeAction() + { + $userCode = $this->getRequest()->getParam('user_code'); + $clientSecret = $this->getRequest()->getParam('client_secret'); + $clientId = $this->getRequest()->getParam('client_id'); + $userType = $this->getRequest()->getParam('user_type'); + $id = $this->getRequest()->getParam('id'); + $email = $this->getRequest()->getParam('email'); + $client = $this->_loadClient($clientId); + + if (!$client->getId() || $client->getSecret() !== $clientSecret) { + $this->_sendResponse(400, 'Invalid client'); + return; + } + + if ($id && $email && $userType) { + $admin = $customer = null; + if ($userType === 'admin') { + $admin = Mage::getModel('admin/user')->load($id); + if (!$admin->getId() || $admin->getEmail() !== $email || !$admin->getIsActive()) { + $this->_sendResponse(400, 'Invalid admin'); + return; + } + } elseif ($userType === 'customer') { + $customer = Mage::getModel('customer/customer')->load($id); + if (!$customer->getId() || $customer->getEmail() !== $email) { + $this->_sendResponse(400, 'Invalid customer'); + return; + } + } + } else { + $this->_sendResponse(400, 'Invalid parameters'); + return; + } + $deviceCodeModel = $this->_loadDeviceCode($userCode, 'user_code'); + + try { + $deviceCodeModel->setAuthorized(true); + if ($admin) { + $deviceCodeModel->setAdminId($admin->getId()); + } elseif ($customer) { + $deviceCodeModel->setCustomerId($customer->getId()); + } + $deviceCodeModel->save(); + + $this->_sendResponse(200, 'Success'); + } catch (Exception $e) { + $this->_sendResponse(500, 'Failed to authorize device, contact administrator'); + Mage::logException($e); + } + } + + /** + * Poll action - Check authorization status and provide access token + */ + public function pollAction() + { + $deviceCode = $this->getRequest()->getParam('device_code'); + $userType = $this->getRequest()->getParam('user_type'); + $id = $this->getRequest()->getParam('id'); + $deviceCodeModel = $this->_loadDeviceCode($deviceCode, 'device_code'); + + if (!$this->_isValidDeviceCode($deviceCodeModel)) { + $this->_sendResponse(400, 'Invalid or expired device code'); + return; + } + + if (!$id || !$userType) { + $this->_sendResponse(400, 'Invalid parameters'); + return; + } + if ($userType === 'admin') { + if (!$deviceCodeModel->getAdminId() || $deviceCodeModel->getAdminId() !== $id) { + $this->_sendResponse(400, 'Invalid admin'); + return; + } + } elseif ($userType === 'customer') { + if (!$deviceCodeModel->getCustomerId() || $deviceCodeModel->getCustomerId() !== $id) { + $this->_sendResponse(400, 'Invalid customer'); + return; + } + } + + if ($deviceCodeModel->getAuthorized()) { + $model = $this->_generateAccessToken($deviceCodeModel->getClientId(), $deviceCodeModel->getAdminId(), $deviceCodeModel->getCustomerId()); + $this->_sendResponse(200, 'Success', [ + 'access_token' => $model->getAccessToken(), + 'token_type' => 'Bearer', + 'expires_in' => $model->getExpiresIn(), + 'refresh_token' => $model->getRefreshToken(), + ]); + $deviceCodeModel->delete(); + } else { + $this->_sendResponse(202, 'Authorization pending'); + } + } + + /** + * Load client by ID + * + * @param string $clientId + * @return Mage_Oauth2_Model_Client + */ + protected function _loadClient($clientId) + { + return Mage::getModel('oauth2/client')->load($clientId, 'entity_id'); + } + + /** + * Generate device code + * + * @param string $clientId + * @return string + */ + protected function _generateDeviceCode($clientId) + { + return uniqid($clientId); + } + + /** + * Generate user code + * + * @return string + */ + protected function _generateUserCode() + { + return strtoupper(bin2hex(random_bytes(3))); + } + + /** + * Save device code + * + * @param string $deviceCode + * @param string $userCode + * @param string $clientId + */ + protected function _saveDeviceCode($deviceCode, $userCode, $clientId) + { + Mage::getModel('oauth2/deviceCode') + ->setDeviceCode($deviceCode) + ->setUserCode($userCode) + ->setClientId($clientId) + ->setExpiresIn(time() + 600) + ->setAuthorized(false) + ->save(); + } + + /** + * Send success response for device code request + * + * @param string $deviceCode + * @param string $userCode + */ + protected function _sendSuccessResponse($deviceCode, $userCode) + { + $this->_sendResponse(200, 'Success', [ + 'device_code' => $deviceCode, + 'user_code' => $userCode, + ]); + } + + /** + * Load device code model + * + * @param string $code + * @param string $field + * @return Mage_Oauth2_Model_DeviceCode + */ + protected function _loadDeviceCode($code, $field) + { + return Mage::getModel('oauth2/deviceCode')->load($code, $field); + } + + /** + * Check if device code is valid + * + * @param Mage_Oauth2_Model_DeviceCode $deviceCodeModel + * @return bool + */ + protected function _isValidDeviceCode($deviceCodeModel) + { + return $deviceCodeModel->getId() && $deviceCodeModel->getExpiresIn() >= time(); + } + + /** + * Render verification form + * + * @param string $userCode + */ + protected function _renderVerificationForm($userCode) + { + Mage::register('current_device_code', $userCode); + $this->loadLayout(); + $this->getLayout()->getBlock('content')->append( + $this->getLayout()->createBlock('oauth2/device_verify', 'oauth2.device.verify') + ); + $this->renderLayout(); + Mage::unregister('current_device_code'); + } + + /** + * Generate access token + * + * @param string $clientId + * @param int $customerId + * @return Mage_Oauth2_Model_AccessToken + */ + protected function _generateAccessToken($clientId, $adminId, $customerId) + { + $model = Mage::getModel('oauth2/accessToken')->load($clientId, 'client_id'); + + if ($model->getId() && $model->getExpiresIn() > time()) { + return $model; + } + + $helper = Mage::helper('oauth2'); + $model->setAccessToken($helper->generateToken()) + ->setClientId($clientId) + ->setAdminId($adminId) + ->setCustomerId($customerId) + ->setRefreshToken($helper->generateToken()) + ->setExpiresIn(time() + 3600) + ->save(); + + return $model; + } +} diff --git a/app/code/core/Mage/Oauth2/controllers/TokenController.php b/app/code/core/Mage/Oauth2/controllers/TokenController.php new file mode 100644 index 00000000000..b4ed75eea1f --- /dev/null +++ b/app/code/core/Mage/Oauth2/controllers/TokenController.php @@ -0,0 +1,155 @@ +helper = Mage::helper('oauth2'); + + try { + $grantType = $this->getRequest()->getParam('grant_type'); + $clientId = $this->getRequest()->getParam('client_id'); + $clientSecret = $this->getRequest()->getParam('client_secret'); + + $this->validateClient($clientId, $clientSecret, $grantType); + + switch ($grantType) { + case 'authorization_code': + $response = $this->handleAuthorizationCodeGrant(); + break; + case 'refresh_token': + $response = $this->handleRefreshTokenGrant(); + break; + default: + throw new Exception('Invalid grant_type', 400); + } + + $this->_sendResponse(200, 'Success', $response); + } catch (Exception $e) { + $this->_sendResponse($e->getCode() ?: 400, $e->getMessage()); + } + } + + /** + * Validate client credentials + * + * @param string $clientId + * @param string $clientSecret + * @param string $grantType + * @throws Exception if client credentials are invalid + */ + protected function validateClient($clientId, $clientSecret, $grantType) + { + $client = Mage::getModel('oauth2/client')->load($clientId, 'entity_id'); + if (!$client->getId() || $client->getSecret() !== $clientSecret || !in_array($grantType, explode(',', $client->getGrantTypes()))) { + throw new Exception('Invalid client.', 401); + } + } + + /** + * Handle authorization code grant type + * + * @return array Token response + * @throws Exception if authorization code is invalid + */ + protected function handleAuthorizationCodeGrant() + { + $code = $this->getRequest()->getParam('code'); + $redirectUri = $this->getRequest()->getParam('redirect_uri'); + $clientId = $this->getRequest()->getParam('client_id'); + + $authCode = $this->validateAuthorizationCode($code, $clientId, $redirectUri); + + $token = Mage::getModel('oauth2/accessToken'); + $token->setAccessToken($this->helper->generateToken()) + ->setRefreshToken($this->helper->generateToken()) + ->setCustomerId($authCode->getCustomerId()) + ->setClientId($clientId) + ->setExpiresIn(time() + 3600) + ->save(); + + $authCode->setUsed(true)->save(); + + return $this->formatTokenResponse($token); + } + + /** + * Validate authorization code + * + * @param string $code + * @param string $clientId + * @param string $redirectUri + * @return Mage_Oauth2_Model_AuthCode + * @throws Exception if authorization code is invalid + */ + protected function validateAuthorizationCode($code, $clientId, $redirectUri) + { + $authCode = Mage::getModel('oauth2/authCode')->load($code, 'authorization_code'); + if (!$authCode->getId() || $authCode->getClientId() != $clientId || $authCode->getRedirectUri() != $redirectUri || $authCode->getExpiresIn() < time() || $authCode->getUsed()) { + throw new Exception('Invalid authorization code, try to authorize again', 400); + } + return $authCode; + } + + /** + * Handle refresh token grant type + * + * @return array Token response + * @throws Exception if refresh token is invalid + */ + protected function handleRefreshTokenGrant() + { + $refreshToken = $this->getRequest()->getParam('refresh_token'); + $clientId = $this->getRequest()->getParam('client_id'); + + $token = $this->validateRefreshToken($refreshToken, $clientId); + $newtoken = Mage::getModel('oauth2/accessToken'); + $newtoken->setAccessToken($this->helper->generateToken()) + ->setRefreshToken($this->helper->generateToken()) + ->setCustomerId($token->getCustomerId()) + ->setAdminId($token->getAdminId()) + ->setClientId($clientId) + ->setExpiresIn(time() + 3600) + ->save(); + $token->delete(); + return $this->formatTokenResponse($newtoken); + } + + /** + * Validate refresh token + * + * @param string $refreshToken + * @param string $clientId + * @return Mage_Oauth2_Model_AccessToken + * @throws Exception if refresh token is invalid + */ + protected function validateRefreshToken($refreshToken, $clientId) + { + $token = Mage::getModel('oauth2/accessToken')->load($refreshToken, 'refresh_token'); + if (!$token->getId() || $token->getClientId() != $clientId) { + throw new Exception('Invalid refresh token', 400); + } + return $token; + } + + /** + * Format token response + * + * @param Mage_Oauth2_Model_AccessToken $token + * @return array + */ + protected function formatTokenResponse($token) + { + return [ + 'access_token' => $token->getAccessToken(), + 'token_type' => 'Bearer', + 'expires_in' => $token->getExpiresIn(), + 'refresh_token' => $token->getRefreshToken(), + ]; + } +} diff --git a/app/code/core/Mage/Oauth2/etc/adminhtml.xml b/app/code/core/Mage/Oauth2/etc/adminhtml.xml new file mode 100644 index 00000000000..16a8f60eec7 --- /dev/null +++ b/app/code/core/Mage/Oauth2/etc/adminhtml.xml @@ -0,0 +1,30 @@ + + + + + + + + + REST - OAuth2 Clients + 100 + adminhtml/oauth2_client/index + + + + + + + + + + + + OAuth2 Clients + 10 + + + + + + \ No newline at end of file diff --git a/app/code/core/Mage/Oauth2/etc/config.xml b/app/code/core/Mage/Oauth2/etc/config.xml new file mode 100644 index 00000000000..962f6667144 --- /dev/null +++ b/app/code/core/Mage/Oauth2/etc/config.xml @@ -0,0 +1,85 @@ + + + + + 1.0.0 + + + + + + standard + + Mage_Oauth2 + oauth2 + + + + + + + + Mage_Oauth2_Block + + + + + Mage_Oauth2_Helper + + + + + Mage_Oauth2_Model + oauth2_resource + + + Mage_Oauth2_Model_Resource + + + oauth2_client
+
+ + oauth2_auth_code
+
+ + oauth2_access_token
+
+ + oauth2_device_code
+
+
+
+
+ + + + Mage_Oauth2 + + + core_setup + + + + + core_write + + + + + core_read + + + +
+ + + + + + Mage_Oauth2_Adminhtml + + + + + +
\ No newline at end of file diff --git a/app/code/core/Mage/Oauth2/sql/oauth2_setup/install-1.0.0.php b/app/code/core/Mage/Oauth2/sql/oauth2_setup/install-1.0.0.php new file mode 100644 index 00000000000..d9127f894a3 --- /dev/null +++ b/app/code/core/Mage/Oauth2/sql/oauth2_setup/install-1.0.0.php @@ -0,0 +1,77 @@ +startSetup(); + +$installer->run(" + CREATE TABLE `{$installer->getTable('oauth2/client')}` ( + `entity_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(200), + `secret` VARCHAR(80) NOT NULL, + `redirect_uri` VARCHAR(2000), + `grant_types` VARCHAR(2000), + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_oauth2_client_created_at` (`created_at`) + ) ENGINE=InnoDB; +"); + +$installer->run(" + CREATE TABLE `{$installer->getTable('oauth2/auth_code')}` ( + `authorization_code` VARCHAR(100) NOT NULL, + `customer_id` INT(10) UNSIGNED, + `redirect_uri` VARCHAR(2000), + `client_id` BIGINT NOT NULL, + `expires_in` INT NOT NULL, + `used` BOOLEAN DEFAULT FALSE, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`authorization_code`), + CONSTRAINT `fk_oauth2_auth_code_client_id` FOREIGN KEY (`client_id`) REFERENCES `{$installer->getTable('oauth2/client')}` (`entity_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + CONSTRAINT `fk_oauth2_auth_code_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `{$installer->getTable('customer/entity')}` (`entity_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + INDEX `idx_oauth2_auth_code_created_at` (`created_at`), + INDEX `idx_oauth2_auth_code_client_id` (`client_id`), + INDEX `idx_oauth2_auth_code_customer_id` (`customer_id`) + ) ENGINE=InnoDB; +"); + +$installer->run(" + CREATE TABLE `{$installer->getTable('oauth2/access_token')}` ( + `access_token` VARCHAR(100) NOT NULL, + `refresh_token` VARCHAR(100), + `admin_id` INT(10) UNSIGNED, + `customer_id` INT(10) UNSIGNED, + `client_id` BIGINT NOT NULL, + `expires_in` INT NOT NULL, + `revoked` BOOLEAN DEFAULT FALSE, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`access_token`), + CONSTRAINT `fk_oauth2_access_token_client_id` FOREIGN KEY (`client_id`) REFERENCES `{$installer->getTable('oauth2/client')}` (`entity_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + CONSTRAINT `fk_oauth2_access_token_admin_id` FOREIGN KEY (`admin_id`) REFERENCES `{$installer->getTable('admin/user')}` (`user_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + CONSTRAINT `fk_oauth2_access_token_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `{$installer->getTable('customer/entity')}` (`entity_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + INDEX `idx_oauth2_access_token_created_at` (`created_at`), + INDEX `idx_oauth2_access_token_client_id` (`client_id`), + INDEX `idx_oauth2_access_token_admin_id` (`admin_id`), + INDEX `idx_oauth2_access_token_customer_id` (`customer_id`) + ) ENGINE=InnoDB; +"); + + +$installer->run(" +CREATE TABLE `{$installer->getTable('oauth2/device_code')}` ( + `device_code` VARCHAR(32) PRIMARY KEY NOT NULL, + `admin_id` INT(10) UNSIGNED, + `customer_id` INT(10) UNSIGNED, + `user_code` VARCHAR(8) NOT NULL, + `client_id` BIGINT NOT NULL, + `expires_in` INT NOT NULL, + `authorized` BOOLEAN DEFAULT FALSE, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_oauth2_device_code_user_code` (`user_code`), + CONSTRAINT `fk_oauth2_device_code_admin_id` FOREIGN KEY (`admin_id`) REFERENCES `{$installer->getTable('admin/user')}` (`user_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + CONSTRAINT `fk_oauth2_device_code_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `{$installer->getTable('customer/entity')}` (`entity_id`) ON UPDATE NO ACTION ON DELETE NO ACTION, + CONSTRAINT `fk_oauth2_device_code_client_id` FOREIGN KEY (`client_id`) REFERENCES `{$installer->getTable('oauth2/client')}` (`entity_id`) ON DELETE CASCADE +) ENGINE=InnoDB; +"); + +$installer->endSetup(); diff --git a/app/design/frontend/base/default/template/oauth2/authorize.phtml b/app/design/frontend/base/default/template/oauth2/authorize.phtml new file mode 100644 index 00000000000..718fb9dab35 --- /dev/null +++ b/app/design/frontend/base/default/template/oauth2/authorize.phtml @@ -0,0 +1,10 @@ +
+ getBlockHtml('formkey') ?> + + +

__('Authorize %s', $this->htmlEscape($this->getClient()->getName())); ?>

+
+ + +
+
\ No newline at end of file diff --git a/app/design/frontend/base/default/template/oauth2/device/verify.phtml b/app/design/frontend/base/default/template/oauth2/device/verify.phtml new file mode 100644 index 00000000000..1d240840cab --- /dev/null +++ b/app/design/frontend/base/default/template/oauth2/device/verify.phtml @@ -0,0 +1,6 @@ +
+ getBlockHtml('formkey') ?> + + + +
\ No newline at end of file diff --git a/app/etc/modules/Mage_Oauth2.xml b/app/etc/modules/Mage_Oauth2.xml new file mode 100644 index 00000000000..7653346ee06 --- /dev/null +++ b/app/etc/modules/Mage_Oauth2.xml @@ -0,0 +1,12 @@ + + + + + true + core + + + + + + \ No newline at end of file diff --git a/app/locale/en_US/Mage_Oauth.csv b/app/locale/en_US/Mage_Oauth.csv index 9e5c81a718b..0664eb0e595 100644 --- a/app/locale/en_US/Mage_Oauth.csv +++ b/app/locale/en_US/Mage_Oauth.csv @@ -110,6 +110,7 @@ "Token Status Change Email Template","Token Status Change Email Template" "Unable to find a consumer.","Unable to find a consumer." "User ID","User ID" +"User Code","User Code" "User Name","User Name" "User Type","User Type" "Verifier code: %s","Verifier code: %s"