Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fredden committed Mar 13, 2024
0 parents commit e8fc74f
Show file tree
Hide file tree
Showing 25 changed files with 741 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/composer.lock
/vendor
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
it: coding-standards dependency-analysis

coding-standards: vendor
composer normalize
vendor/bin/phpcbf || true
vendor/bin/phpcs

dependency-analysis: vendor
vendor/bin/composer-require-checker check --verbose

vendor: composer.json composer.lock
composer validate --strict
composer --no-interaction install --no-progress

composer.lock:
test -f composer.lock || echo '{}' > composer.lock
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Fredden_AdminAuth - fredden/magento2-module-admin-authentication

## Installation
This extension is hosted on packagist.org should be installed from there:

* `composer require fredden/magento2-module-admin-authentication`
15 changes: 15 additions & 0 deletions composer-require-checker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"symbol-whitelist": [
"array",
"bool",
"callable",
"false",
"int",
"parent",
"self",
"string",
"true",
"void",
"Magento\\User\\Model\\UserFactory"
]
}
57 changes: 57 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "fredden/magento2-module-admin-authentication",
"description": "A Magento 2 module that allows access to Magento admin via Fredden Auth Portal",
"license": "CC-BY-NC-SA-4.0",
"type": "magento2-module",
"authors": [
{
"name": "Dan Wallis",
"email": "[email protected]"
}
],
"homepage": "https://github.com/fredden/magento2-module-admin-authentication",
"require": {
"php": "~8.2.0 || ~8.3.0",
"ext-json": "*",
"ext-random": "*",
"firebase/php-jwt": "^5.5 || ^6.0",
"magento/framework": "^102.0 || ^103.0",
"magento/module-backend": "^101.0 || ^102.0",
"magento/module-captcha": "^100.0.2",
"magento/module-config": "^101.0",
"magento/module-two-factor-auth": "^1.0",
"magento/module-user": "^101.0"
},
"require-dev": {
"bitexpert/phpstan-magento": "^0.11.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"maglnet/composer-require-checker": "^3.8",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10",
"squizlabs/php_codesniffer": "^3.9"
},
"repositories": {
"magento": {
"type": "composer",
"url": "https://repo-magento-mirror.fooman.co.nz/"
}
},
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"Fredden\\AdminAuth\\": "src"
},
"files": [
"src/registration.php"
]
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"magento/composer-dependency-version-audit-plugin": false,
"magento/magento-composer-installer": false,
"phpstan/extension-installer": true
}
}
}
15 changes: 15 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<file>src</file>

<arg name="basepath" value="."/>
<arg name="cache"/>
<arg name="colors"/>
<arg name="extensions" value="php,phtml" />
<arg value="p"/>

<rule ref="PSR12" />

<config name="testVersion" value="8.2-8.3"/>
<rule ref="PHPCompatibility" />
</ruleset>
142 changes: 142 additions & 0 deletions src/Controller/Adminhtml/Login/Refresh.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace Fredden\AdminAuth\Controller\Adminhtml\Login;

use Exception;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Fredden\AdminAuth\Scope\Config;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Controller\Adminhtml\Auth as AuthController;
use Magento\Backend\Model\Auth as AuthModel;
use Magento\Backend\Model\Auth\Session as AuthSession;
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\User\Model\ResourceModel\User as UserResource;
use Magento\User\Model\User;
use Magento\User\Model\UserFactory;
use Throwable;

/**
* This controller is thusly named to as a hack. 'refresh' is one of few names that
* are not subject to authentication verification, which is exactly what this
* controller is designed to bypass.
*
* @see \Magento\Backend\App\Action\Plugin\Authentication::$_openActions
*/
class Refresh extends AuthController implements HttpPost
{
public function __construct(
Context $context,
private readonly AuthModel $authModel,
private readonly AuthSession $authSession,
private readonly Config $config,
private readonly JsonFactory $resultJsonFactory,
private readonly UserFactory $userFactory,
private readonly UserResource $userResource,
) {
parent::__construct($context);
}

/**
* @return ResultInterface
*/
public function execute()
{
$token = $this->getRequest()->getPost('token');

try {
$payload = $this->parseToken($token);
} catch (Exception $e) {
$result = $this->resultJsonFactory->create();
return $result->setData(
[
'status' => 'failure',
'message' => 'Unable to verify token - ' . $e->getMessage(),
]
);
}

$errors = [];
foreach ($payload['emails'] as $emailAddress) {
$user = $this->userFactory->create();
$user->setEmail($emailAddress);
$data = $this->userResource->userExists($user);

if (!$data) {
$errors[$emailAddress] = 'No account for ' . $emailAddress;
continue;
}

// Existing user found
$user->load($data['user_id']);

if (!$user->getIsActive()) {
$errors[$emailAddress] = 'Account for ' . $emailAddress . ' is disabled.';
continue;
}

return $this->doLogin($user);
}

if ($errors === []) {
$errors[] = 'No email addresses provided';
}

$result = $this->resultJsonFactory->create();
return $result->setData(
[
'status' => 'failure',
'message' => implode("\n\n", $errors),
]
);
}

private function doLogin(User $user): ResultInterface
{
$result = $this->resultJsonFactory->create();

$password = bin2hex(random_bytes(32));
$user->setPassword($password);
$user->setPasswordConfirmation($password);
$user->save();

try {
$this->authModel->login($user->getUserName(), $password);
} catch (Exception $e) {
return $result->setData(
[
'status' => 'failure',
'message' => $e->getMessage(),
'username' => $user->getUserName(),
'password' => $password,
]
);
}

// This is used elsewhere in this module to restrict other features to
// only when logging in via this tool.
$this->authSession->setFreddenAdminAuth(true);

return $result->setData(
[
'status' => 'success',
'message' => 'Logged in as ' . $user->getUserName(),
'username' => $user->getUserName(),
'password' => $password,
]
);
}

private function parseToken(string $token): array
{
try {
$keys = JWK::parseKeySet(['keys' => $this->config->getAuthKeys()]);
return (array) JWT::decode($token, $keys);
} catch (Throwable) {
$keys = JWK::parseKeySet(['keys' => $this->config->getAuthKeys(false)]);
return (array) JWT::decode($token, $keys);
}
}
}
28 changes: 28 additions & 0 deletions src/Plugin/Magento/Captcha.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Fredden\AdminAuth\Plugin\Magento;

use Magento\Captcha\Model\DefaultModel;
use Magento\Framework\App\RequestInterface;

class Captcha
{
public function __construct(
private readonly RequestInterface $request,
) {
}

public function afterIsRequired(DefaultModel $subject, bool $result): bool
{
if (
$result
&& $this->request->getRouteName() === 'Fredden_AdminAuth'
&& $this->request->getControllerName() === 'login'
&& $this->request->getActionName() === 'refresh'
) {
return false;
}

return $result;
}
}
84 changes: 84 additions & 0 deletions src/Plugin/Magento/SetAdminUserLocale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Fredden\AdminAuth\Plugin\Magento;

use Magento\Framework\App\RequestInterface;
use Magento\Framework\Locale\OptionInterface;
use Magento\User\Model\User;

class SetAdminUserLocale
{
public function __construct(
private readonly OptionInterface $deployedLocales,
private readonly RequestInterface $request,
) {
}

/**
* Ensure that the user always has a valid interface locale set
*/
public function afterBeforeSave(User $subject, User $result): User
{
$availableOptions = $this->deployedLocales->getOptionLocales();

if (!$availableOptions) {
return $result;
}

$currentUserLocale = $subject->getInterfaceLocale();
$locales = [];

foreach ($availableOptions as $option) {
if ($option['value'] === $currentUserLocale) {
// User's current locale is available here; there's nothing for us to do.
return $result;
}

$locales[$option['value']] = $option['value'];
$locales[substr($option['value'], 0, 2)] = $option['value'];
}

$preferredLocales = [];

if ($header = $this->request->getHeader('accept-language')) {
$languages = explode(',', $header);
foreach ($languages as $languageOption) {
// TODO: parse and use ';q=' values to weight/sort options.
$languageOption = explode(';', $languageOption)[0];
$languageOption = trim($languageOption);
$languageOption = str_replace('-', '_', $languageOption);

$preferredLocales[] = $languageOption;
}
}

$preferredLocales[] = 'en'; // Add English as a fallback option

foreach ($preferredLocales as $localeOption) {
if ($localeOption === 'en' && isset($locales['en_GB'])) {
// English is preferred.
$localeOption = 'en_GB';
}

if ($localeOption === 'en' && isset($locales['en_US'])) {
// American is next best if English isn't available.
$localeOption = 'en_US';
}

if (isset($locales[$localeOption])) {
$subject->setInterfaceLocale($locales[$localeOption]);
return $result;
}
}

// We didn't find a locale that suits the user's preferences. Let's just
// give them ANY locale that exists. A working admin in a foreign
// language is better than a completely broken admin. The user can set
// another locale when they work out where to click.
$subject->setInterfaceLocale($option['value']);
// $option['value'] comes from the foreach() on line 44. There will
// always be at least one locale in that loop due to Magento internals.

return $result;
}
}
Loading

0 comments on commit e8fc74f

Please sign in to comment.