3

I'm working on a web application with Symfony 2. More specifically, I'm trying to make a Custom Authentication Provider thanks to the instructions of the Symfony Cookbook.

I have to make custom authentication provider because I must not check the username and the password entered in my login form with the ones which are in stored in my database. But, anyway, that's not my problem here.

The problem is, reading my code (pasted below), anyone should be able to login entering a username which is already in my database, no matter the password is correct or not (compared to the one stored in the database) since I only check if the username entered exists in the database. But that's not the case : magically, the password IS checked and I get the 'Bad credentials' error message when I try to login with a wrong password.

I paste my code. My app/config/security.yml :

jms_security_extra:
    secure_all_services: false
    expressions: true

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        Epilille\UserBundle\Entity\User: plaintext

    role_hierarchy:
        ROLE_STUDENT:     ROLE_USER
        ROLE_AER:         ROLE_STUDENT
        ROLE_PROF:        ROLE_AER
        ROLE_ADMIN:       ROLE_PROF
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
                    student:  { password: studentpass, roles: ['ROLE_STUDENT'] }
                    mestag_a: { password: mestag_apass, roles: ['ROLE_AER'] }
        intra_provider:
            entity: { class: EpililleUserBundle:User, property: username }

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

        login:
            pattern:  ^/login$
            anonymous: true

        intra_secured:
            pattern:  ^/
            intra: true
            provider: intra_provider
            form_login:
              login_path: login
              check_path: login_check
              default_target_path: epilille_home_homepage
            logout:
                path:   logout
                target: login

    access_control:
        # - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY}

The architecture of my UserBundle :

.
├── Controller/
│   └── SecurityController.php
├── DataFixtures/
│   └── ORM/
│       └── Users.php
├── DependencyInjection/
│   ├── Configuration.php
│   ├── EpililleUserExtension.php
│   └── Security/
│       └── Factory/
│           └── IntraFactory.php
├── Entity/
│   ├── User.php
│   └── UserRepository.php
├── EpililleUserBundle.php
├── EpiLogin.class.php
├── Resources/
│   ├── config/
│   │   └── services.yml
│   ├── doc/
│   │   └── index.rst
│   ├── public/
│   │   ├── css/
│   │   ├── images/
│   │   └── js/
│   ├── translations/
│   │   └── messages.fr.xlf
│   └── views/
│       ├── layout.html.twig
│       └── User/
│           └── login.html.twig
└── Security/
    ├── Authentication/
    │   ├── Provider/
    │   │   └── IntraProvider.php
    │   └── Token/
    │       └── IntraUserToken.php
    ├── Firewall/
    │   └── IntraListener.php
    └── User/
        └── IntraUserProvider.php

My factory :

<?php

namespace       Epilille\UserBundle\DependencyInjection\Security\Factory;

use         Symfony\Component\DependencyInjection\ContainerBuilder;
use         Symfony\Component\DependencyInjection\Reference;
use         Symfony\Component\DependencyInjection\DefinitionDecorator;
use         Symfony\Component\Config\Definition\Builder\NodeDefinition;
use         Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;

class           IntraFactory implements SecurityFactoryInterface
{
  public function   create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
  {
    $providerId = 'security.authentication.provider.intra.'.$id;
    $container
      ->setDefinition($providerId, new DefinitionDecorator('intra.security.authentication.provider'))
      ->replaceArgument(0, new Reference($userProvider))
      ;

    $listenerId = 'security.authentication.listener.intra.'.$id;
    $listener = $container->setDefinition($listenerId, new DefinitionDecorator('intra.security.authentication.listener'));

    return array($providerId, $listenerId, $defaultEntryPoint);
  }

  public function   getPosition()
  {
    return 'pre_auth';
  }

  public function   getKey()
  {
    return 'intra';
  }

  public function   addConfiguration(NodeDefinition $node)
  {
  }
}

My token :

<?php

namespace       Epilille\UserBundle\Security\Authentication\Token;

use         Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class           IntraUserToken extends AbstractToken
{
  public function   __construct(array $roles = array())
  {
    parent::__construct($roles);
    $this->setAuthenticated(count($roles) > 0);
  }

  public function   getCredentials()
  {
    return '';
  }
}

My authentication provider :

<?php

namespace       Epilille\UserBundle\Security\Authentication\Provider;

use         Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use         Symfony\Component\Security\Core\User\UserProviderInterface;
use         Symfony\Component\Security\Core\Exception\AuthenticationException;
use         Symfony\Component\Security\Core\Exception\NonceExpiredException;
use         Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use         Epilille\UserBundle\Security\Authentication\Token\IntraUserToken;

class           IntraProvider implements AuthenticationProviderInterface
{
  private       $userProvider;

  public function   __construct(UserProviderInterface $userProvider)
  {
    $this->userProvider = $userProvider;
  }

  public function   authenticate(TokenInterface $token)
  {
    $user = $this->userProvider->loadUserByUsername($token->getUsername());

    if ($user) {
      $authenticatedToken = new IntraUserToken($user->getRoles());
      $authenticatedToken->setUser($user);

      return $authenticatedToken;
    }

    throw new AuthenticationException('The Intranet authentication failed.');
  }

  public function   supports(TokenInterface $token)
  {
    // I noticed something with this method I tell you below
    return $token instanceof IntraUserToken;
  }
}

My listener :

<?php

namespace       Epilille\UserBundle\Security\Firewall;

use         Symfony\Component\HttpFoundation\Response;
use         Symfony\Component\HttpKernel\Event\GetResponseEvent;
use         Symfony\Component\Security\Http\Firewall\ListenerInterface;
use         Symfony\Component\Security\Core\Exception\AuthenticationException;
use         Symfony\Component\Security\Core\SecurityContextInterface;
use         Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use         Epilille\UserBundle\Security\Authentication\Token\IntraUserToken;
use         Epilille\UserBundle\Security\Authentication\Provider\IntraProvider;

class           IntraListener implements ListenerInterface
{
  protected     $securityContext;
  protected     $authenticationManager;

  public function   __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
  {
    $this->securityContext = $securityContext;
    $this->authenticationManager = $authenticationManager;
  }

  public function   handle(GetResponseEvent $event)
  {
    // I have to do this check because it seems like this function is called several times and if
    // it's not the first time, it will try to reset a new IntraUserToken with _username and _password not set.
    if ($this->securityContext->getToken()) {
      return;
    }

    $request = $event->getRequest();

    $token = new IntraUserToken();
    $token->setUser($request->get('_username'));
    $token->password = $request->get('_password');

    try {
      $authToken = $this->authenticationManager->authenticate($token);

      $this->securityContext->setToken($authToken);
    } catch (AuthenticationException $failed) {
      // ... you might log something here

      // To deny the authentication clear the token. This will redirect to the login page.
      $this->securityContext->setToken(null);
      return;

      // Deny authentication with a '403 Forbidden' HTTP response
      /* $response = new Response(); */
      /* $response->setStatusCode(403); */
      /* $event->setResponse($response); */
    }
  }
}

So, with this code, that's what happens :

  • If I enter a username which doesn't exist in my database, I get the error message 'Bad credentials'.
  • If I enter a username which does exist in my database :
    • with a wrong password, I get 'Bad credentials'.
    • with the correct password, I'm authenticated and the profiler says I'm logged with the Token class 'UsernamePasswordToken'.

Now I want to tell you about the method IntraProvider::supports. Now, it's like that :

public function       supports(TokenInterface $token)
{
  return $token instanceof IntraUserToken;
}

And if I change it like that :

public function       supports(TokenInterface $token)
{
  return (true);
}

There's a differences with the case in which I enter a good username (no matters the password is) : I'm authenticated and the profiler now says I'm with the Token class 'IntraUserToken' (it seems to be exactly what I want).

So there my problems are :

  • Why doesn't my authentication solution work with the first version of the IntraProvider::supports method ?
  • Why does it work when I return true every time in the IntraProvider::supports method ?

In the Cookbook, they do it as I do in the first version of this method, so I don't think it's a good idea to return true every time, is it ?

I hope I made myself clear about my problem, thanks for your time and your answers !

Unda
  • 1,827
  • 3
  • 23
  • 35
  • Symfony Security component iterate providers and check which one supports given token. It seems that you got other Token than IntaUserToken and other provider is used... quite strange indeed Can you check what Token do you have in support method ? – l3l0 Jun 26 '13 at 20:04
  • @l3l0 [Sorry I answered quite a long time ago but I just figured out how to notify you that I have answered your comment]. So I opened a file in the IntraProvider::supports method to write in it which token was in the parameters, and that's what it contains : $token : Epilille\UserBundle\Security\Authentication\Token\IntraUserToken $token : Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken It looks like the method is called two times, one time with IntraUserToken and another on with UsernamePasswordToken ... – Unda Jun 30 '13 at 18:10

0 Answers0