简体   繁体   中英

Symfony: Authenticate users against LDAP server, but allow login only if their username is in custom db table

First a short explanation of my task. I'm using Symfony 2.8 and have an application with REST API and SonataAdminBundle. Visitors of the website can post certain data over the REST API that is persisted to the database. A certain group of employees should manage those data through the admin area.

The access to the admin area should be protected with username and password. There is entity Employee with a property username , but no password. The authentication should be done against the LDAP server, but the access to admin area should be restricted only to those employees that are present in the entity Employee ie the referring database table.

For the LDAP authentication I am using the new LDAP component in Symfony 2.8.

Beyond that there should be an admin account as in_memory user.

This is what I have now:

services:
    app.ldap:
        class: Symfony\Component\Ldap\LdapClient
        arguments: ["ldaps://ldap.uni-rostock.de"]

    app.db_user_provider:
        class: AppBundle\Security\DbUserProvider
        arguments: ["@doctrine.orm.entity_manager"]

security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        chain_provider:
            chain:
                providers: [db_user, app_users]

        in_memory:
            memory:
                users:
                    admin: { password: adminpass, roles: 'ROLE_ADMIN' }

        app_users:
            ldap:
                service: app.ldap
                base_dn: ou=people,o=uni-rostock,c=de
                search_dn: uid=testuser,ou=people,o=uni-rostock,c=de
                search_password: testpass
                filter: (uid={username})
                default_roles: ROLE_USER

        db_user:
            id: app.db_user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin:
            anonymous: true
            pattern:   ^/
            form_login_ldap:
                provider: chain_provider
                service: app.ldap
                dn_string: "uid={username},ou=people,o=uni-rostock,c=de"
                check_path: /login_check
                login_path: /login
            form_login:
                provider: in_memory
                check_path: /login_check
                login_path: /login
            logout:
                path: /logout
                target: /

    access_control:
        - { path: ^/admin, roles: ROLE_USER }

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        AppBundle\Entity\Employee: bcrypt

namespace AppBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Doctrine\ORM\Mapping as ORM;

class Employee implements UserInterface, EquatableInterface
{
    // other properties

    private $username;

    // getters and setters for the other properties

    public function getUsername()
    {
        return $this->username;
    }

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function getPassword()
    {
        return null;
    }

    public function getSalt()
    {
        return null;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof Employee) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

<?php

namespace AppBundle\Security;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\Employee;

class DbUserProvider implements UserProviderInterface
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function loadUserByUsername($username)
    {
        $repository = $this->em->getRepository('AppBundle:Employee');
        $user = $repository->findOneByUsername($username);

        if ($user) {
            return new Employee();
        }

        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof Employee) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'AppBundle\Entity\Employee';
    }
}

The authentication against LDAP works like a charm. When an employee from the database is trying to login, he/she is redirected to the homepage('/') and the login is failed. All other users, who are not in the database, can login without a problem.

If I chain the providers like this:

chain_provider:
    chain:
        providers: [app_users, db_user]

then the method loadUserByUsername is not even called and all users can login, those who are in the database and those who are not.

The in_memory user admin can login without a problem in any case.

I appreciate any help. If someone thinks that my entire approach is bad and knows a better way, please don't spare with critics.

I know there is FOSUserBundle and SonataUserBundle, but I would prefer a custom user provider as I don't want to bloat the entity Employee, since I really don't need all those properties like password, salt, isLocked, etc. Also I don't think configuring SonataUserBundle in my particular case would be much simpler. Should you still think there is a more elegant way to fulfill my task with these two bundles, I'll be grateful to get a good advice.

you can configure user-checker per firewall, your db user provider isn't really a user-provider because it doesn't have all the information that's needed to authenticate a user (eg password) so what i would do is , i would remove the db user provider and add a user checker instead, the main idea of the user checker is to add additional checks during the authentication process , in your case we need to check if the user is in the employee table or not

you need to do three things to implement this

implement UserCheckerInterface Symfony\\Component\\Security\\Core\\User\\UserCheckerInterface

check if the user is in the employee table or not in checkPostAuth() method

expose your new user-checker as a service

services:
    app.employee_user_checker:
        class: Path\To\Class\EmployeeUserChecker

change your firewall config to use the new user-checker

security:
    firewalls:
        admin:
            pattern: ^/admin
            user_checker: app.employee_user_checker
            #...

Read more

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM