Symfony 5.1: Аутентификация LDAP с помощью Entity User Provider - PullRequest
1 голос
/ 14 июля 2020

Я использую Symfony 5.1 и пытаюсь реализовать аутентификацию LDAP, в то время как свойства пользователя (имя пользователя, роли и т. Д. c.) Хранятся в MySQL БД. Таким образом, я добавил пользовательскую сущность для Doctrine и настроил services.yml и security.yml в соответствии с документацией.

Я также использовал пакет Maker для создания LoginFormAuthenticator, который, похоже, использует аутентификатор Guard. Модуль.

Когда я пытаюсь войти в систему, похоже, что он не делает ничего, связанного с LDAP. Я также прослушивал пакеты TCP с помощью tcpdump и не видел трафика c на сервер LDAP.

Вот мой код:

services.yml:

services:
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    Symfony\Component\Ldap\Ldap:
        arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
    Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
        arguments:
            -   host: <ldap-IP>
                port: 389
                options:
                    protocol_version: 3
                    referrals: false

security.yml:

security:
    encoders:
        App\Entity\User:
            algorithm: auto

        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

        my_ldap:
            ldap:
                service: Symfony\Component\Ldap\Ldap
                base_dn: "<base_dn>"
                search_dn: "<search_dn>"
                search_password: "<password>"
                default_roles: ROLE_USER
                uid_key: sAMAccountName

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: my_ldap

            form_login_ldap:
                login_path: login
                check_path: login
                service: Symfony\Component\Ldap\Ldap
                dn_string: 'uid={username},OU=Test,DC=domain,DC=domain'

            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                target: index

    access_control:
        - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

LoginFormAuthenticator, думаю, проблема здесь в функции checkCredentials. Я нашел класс LdapBindAuthenticationProvider, целью которого, похоже, является именно такая проверка учетных данных пользователя по сравнению с LDAP, но я совершенно не уверен, как мне это делать:

<?php

namespace App\Security;

use Psr\Log\LoggerInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
    use TargetPathTrait;

    public const LOGIN_ROUTE = 'app_login';

    private $logger;
    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;
    private $ldap;

    public function __construct(LoggerInterface $logger, EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, Ldap $ldap, LdapBindAuthenticationProvider $form_login_ldap)
    {
        $this->logger = $logger;
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
        $this->ldap = $ldap;
    }

    public function supports(Request $request): ?bool
    {
        return self::LOGIN_ROUTE === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'username' => $request->request->get('username'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['username']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['username']]);

        if (!$user) {
            // user not found in db, but may exist in ldap:
            $user = $userProvider->loadUserByUsername($credentials['username']);
            if (!$user) {
                // user simply doesn't exist
                throw new CustomUserMessageAuthenticationException('Email could not be found.');
            } else {
                // user never logged in before, create user in DB and proceed...
                // TODO
            }
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // TODO: how to use the LdapBindAuthenticationProvider here to check the users credentials agains LDAP?
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    /**
     * Used to upgrade (rehash) the user's password automatically over time.
     */
    public function getPassword($credentials): ?string
    {
        return $credentials['password'];
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
}

К сожалению, я не нашел ни одного примера код для этого.

ОБНОВЛЕНИЕ:

Благодаря ответу Т. ван ден Берга мне, наконец, удалось заставить работать аутентификационную часть. Я удалил LoginFormAuthenticator Guard из security.yml и немного изменил form_login_ldap. его имя входа (sAMAccountName), а затем во втором запросе использовать отличительное имя (DN) этого объекта LDAP, чтобы выполнить еще одну аутентификацию на сервере LDAP с предоставленным паролем. Пока все в порядке.

Единственное, чего не хватает, - это сохраненный в базе данных объект User. Моя идея заключалась в следующем:

  • отправлена ​​форма входа
  • поиск в базе данных сущности пользователя с предоставленным именем пользователя
  • , если сущность пользователя не найдена используйте LDAPUserProvider, чтобы запросить у LDAP имя пользователя
  • , если Пользователь существует в LDAP, создайте объект User в базе данных
  • аутентифицируйте пользователя в LDAP с помощью предоставленного пароля

Пароль не сохраняется в базе данных, но информация о другом приложении c недоступна в LDAP (например, последнее действие).

Кто-нибудь знает, как это реализовать?

...