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 @@
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+ Mage_Oauth2
+ oauth2
+
+
+
+
+
+
+
+ Mage_Oauth2_Block
+
+
+
+
+ Mage_Oauth2_Helper
+
+
+
+
+ Mage_Oauth2_Model
+ oauth2_resource
+
+
+ Mage_Oauth2_Model_Resource
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mage_Oauth2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+
\ 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 @@
+
\ 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"