Kategorien
Symfony

Symfony Brute-Force Guard für SonataAdmin erstellen

Für das SonataAdmin Bundle gibt es die Möglichkeit über Symofny Guards eigene Logik in den Loginprozess einzubauen, wie z.B.:

  • Abwehr von Brute Force Angriffen durch eine maximale Anzahl von Login-Versuchen

In dem folgenden Beispiel habe ich einen Guard konfiguriert für den Administrationsbereich, der mitzählt, wie oft sich ein User falsch eingeloggt hat.

Der UserManager muss dann die Logik enthalten, um bei jedem User die Anzahl der Logins mitzuzählen und ihn ggf. auch zu bannen für eine bestimmte Zeit.

<?php

namespace App\Security;

use App\Core\UserManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

class AdminAuthenticator extends AbstractFormLoginAuthenticator
{

    /**
     * @var UserManager
     */
    protected $userManager;

    /**
     * @var RouterInterface
     */
    protected $router;

    /**
     * Constructor.
     *
     * @param UserManager $userManager
     * @param RouterInterface $router
     */
    public function __construct(
        UserManager $userManager,
        RouterInterface $router
    )
    {
        $this->userManager = $userManager;
        $this->router = $router;
    }

    /**
     * Return the URL to the login page.
     *
     * @return string
     */
    protected function getLoginUrl()
    {
        return $this->router->generate('sonata_user_admin_security_login');
    }

    /**
     * Does the authenticator support the given Request?
     *
     * If this returns false, the authenticator will be skipped.
     *
     * @param Request $request
     *
     * @return bool
     */
    public function supports(Request $request)
    {
        return ($request->getPathInfo() == '/admin/login_check' && $request->getMethod() === 'POST');
    }

    /**
     * Get the authentication credentials from the request and return them
     * as any type (e.g. an associate array).
     *
     * Whatever value you return here will be passed to getUser() and checkCredentials()
     *
     * For example, for a form login, you might:
     *
     *      return [
     *          'username' => $request->request->get('_username'),
     *          'password' => $request->request->get('_password'),
     *      ];
     *
     * Or for an API token that's on a header, you might use:
     *
     *      return ['api_key' => $request->headers->get('X-API-TOKEN')];
     *
     * @param Request $request
     *
     * @return mixed Any non-null value
     *
     * @throws \UnexpectedValueException If null is returned
     */
    public function getCredentials(Request $request)
    {
        return [
               'username' => $request->request->get('_username'),
               'password' => $request->request->get('_password'),
           ];
    }

    /**
     * Return a UserInterface object based on the credentials.
     *
     * The *credentials* are the return value from getCredentials()
     *
     * You may throw an AuthenticationException if you wish. If you return
     * null, then a UsernameNotFoundException is thrown for you.
     *
     * @param mixed $credentials
     * @param UserProviderInterface $userProvider
     *
     * @throws AuthenticationException
     *
     * @return UserInterface|null
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return  $userProvider->loadUserByUsername($credentials['username']);
    }

    /**
     * Returns true if the credentials are valid.
     *
     * If any value other than true is returned, authentication will
     * fail. You may also throw an AuthenticationException if you wish
     * to cause authentication to fail.
     *
     * The *credentials* are the return value from getCredentials()
     *
     * @param mixed $credentials
     * @param UserInterface $user
     *
     * @return bool
     *
     * @throws AuthenticationException
     */
    public function checkCredentials($credentials, UserInterface $user)
    {
        if ($this->userManager->checkUserIsAllowedToLogin($user)) {
            return $this->userManager->checkPasswordValid($credentials['password'], $user);
        } else {
            throw new CustomUserMessageAuthenticationException('user is banned for 1 hour');
        }
    }

    /**
     * Called when authentication executed and was successful!
     *
     * This should return the Response sent back to the user, like a
     * RedirectResponse to the last page they visited.
     *
     * If you return null, the current request will continue, and the user
     * will be authenticated. This makes sense, for example, with an API.
     *
     * @param Request $request
     * @param TokenInterface $token
     * @param string $providerKey The provider (i.e. firewall) key
     *
     * @return Response|null
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $user = $token->getUser();
        $this->userManager->onLoginSuccess($user);
    }

    /**
     * @param Request $request
     * @param AuthenticationException $exception
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        if ($exception instanceof BadCredentialsException) {
            $userName = $request->request->get('_username');
            if (!empty($userName)) {
                $user = $this->userManager->getUserByUserName($userName);
                if (!empty($user)) {
                    $this->userManager->incrementPasswordFailedCount($user);
                }
            }
        }

        return parent::onAuthenticationFailure($request, $exception);
    }
}

security.yaml:

security:
  providers:
    fos_userbundle:
      id: App\Security\UserProvider
  admin:
    pattern:            /admin(.*)
    context:            user
    form_login:
        provider:       fos_userbundle
        login_path:     /admin/login
        use_forward:    false
        check_path:     /admin/login_check
        failure_path:   null
        default_target_path: sonata_admin_dashboard
    logout:
        path:           /admin/logout
        target:         /admin/login
    anonymous:          true
    guard:
        authenticators:
            - App\Security\AdminAuthenticator
        provider:       fos_userbundle
access_control:
    # Admin login page needs to be accessed without credential
    - { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/admin/, role: ROLE_SONATA_ADMIN }

UserProvider:

<?php

namespace App\Security;

use App\Entity\User;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
    /**
     * @var UserManagerInterface
     */
    protected $userManager;

    /**
     * Constructor.
     *
     * @param UserManagerInterface $userManager
     */
    public function __construct(UserManagerInterface $userManager)
    {
        $this->userManager = $userManager;
    }

    /**
     * {@inheritdoc}
     */
    public function loadUserByUsername($username)
    {
        /** @var User $user */
        $user = $this->findUser($username);

        if (!$user) {
            throw new BadCredentialsException();
        }

        return $user;
    }

    /**
     * {@inheritdoc}
     */
    public function refreshUser(SecurityUserInterface $user)
    {
        if (!$user instanceof UserInterface) {
            throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\UserInterface, but got "%s".', get_class($user)));
        }

        if (!$this->supportsClass(get_class($user))) {
            throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userManager->getClass(), get_class($user)));
        }

        if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
            throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
        }

        return $reloadedUser;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsClass($class)
    {
        $userClass = $this->userManager->getClass();

        return $userClass === $class || is_subclass_of($class, $userClass);
    }

    /**
     * Finds a user by username.
     *
     * This method is meant to be an extension point for child classes.
     *
     * @param string $username
     *
     * @return UserInterface|null
     */
    protected function findUser($username)
    {
        return $this->userManager->findUserByUsername($username);
    }
}

services.yaml:

App\Security\AdminAuthenticator:
  - '@App\Core\UserManager'
  - '@router'

App\Security\UserProvider:
  - '@fos_user.user_manager'