Paweł Koralewski

Paweł Koralewski architekt aplikacji,
team leader

Temat: [Sf2] własna autentykacja

Potrzebuję napisać własną część odpowiedzialną za autoryzację użytkowników. Zabezpieczona ma być część witryny rozpoczynająca się od /hello. Po wpisaniu adresu /hello/xyz pokazuje się formularz, niestety po wysłaniu go użytkownik nie jest logowany, tylko ponownie pokazuje się formularz.

Posiłkując się cookbookiem, wyrzeźbiłem:
#app/config/routing.yml
#app/config/routing.yml
SunwearBundle_controllers:
resource: "@SunwearBundle/Controller"
type: annotation
prefix: /
login_check:
pattern: /login_check
logout:
pattern: /logout

#app/config/security.yml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
users:
u: { password: p, roles: [ 'ROLE_USER' ] }
a: { password: p, roles: [ 'ROLE_ADMIN' ] }

firewalls:
login:
pattern: ^/login$
security: false
mssql_security_secured:
pattern: ^/
mssql_security: true
form_login: ~
logout: ~
access_control:
- { path: ^/hello, roles: ROLE_USER }
factories:
- "%kernel.root_dir%/../src/Alden/SunwearBundle/Resources/config/security_factories.xml"

#/Alden/SunwearBundle/Resources/config/security_factories.xml
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="security.authentication.factory.mssql_security"
class="Alden\SunwearBundle\DependencyInjection\Security\Factory\mssql_securityFactory" public="false">
<tag name="security.listener.factory" />
</service>
</services>
</container>

#/Alden/SunwearBundle/Resources/config/services.yml
services:
mssql_security.security.authentication.provider:
class: Alden\SunwearBundle\Security\Authentication\Provider\mssql_securityProvider
arguments: ['', %kernel.cache_dir%/security/nonces]
mssql_security.security.authentication.listener:
class: Alden\SunwearBundle\Security\Firewall\mssql_securityListener
arguments: [@security.context, @security.authentication.manager]

W celu oszczędzenia miejsca klasy podam bez namespace oraz use - tutaj Sf2 nie zgłasza mi błędów.
#Alden\SunwearBundle\DependencyInjection\Security\Factory\mssql_securityFactory.php
class mssql_securityFactory implements SecurityFactoryInterface {
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.mssql_security.' . $id;
$container->setDefinition($providerId, new DefinitionDecorator('mssql_security.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider));
$listenerId = 'security.authentication.listener.mssql_security.' . $id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('mssql_security.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}

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

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

public function addConfiguration(NodeDefinition $node) {}
}

#Alden\SunwearBundle\Security\Authentication\Token\mssql_securityUserToken
class mssql_securityUserToken extends AbstractToken {
public $created;
public $digest;
public $nonce;
public function getCredentials() { return ''; }
}

W kolejnej klasie zostawiłem kilka przykładów kodu, które testowałem, również bezskutecznie
#Alden\SunwearBundle\Security\Firewall\mssql_securityListener
class mssql_securityListener implements ListenerInterface {

protected $securityContext;
protected $authenticationManager;

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

public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->getMethod() == 'POST')
{
$u = $request->request->get('_username');
$p = $request->request->get('_password');
$token = new mssql_securityUserToken(array('ROLE_USER'));
$token->setUser($u);

$token->digest = 'any';
$token->nonce = 'any';
$token->created = date('Y-m-d H:i');
$token->password = $p;
// $token = new UsernamePasswordToken($u, $p, 'mssql_security_secured', array('ROLE_USER'));
// return $this->securityContext->setToken($token);
// $this->container->get('security.context')->setToken($token);
// return $token;
try
{
$returnValue = $this->authenticationManager->authenticate($token);

if ($returnValue instanceof TokenInterface)
{
return $this->securityContext->setToken($returnValue);
}
else if ($returnValue instanceof Response)
{
return $event->setResponse($returnValue);
}
} catch (AuthenticationException $e)
{
// you might log something here
$rr = 4;
}
}
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
}

W kolejnej klasie w metodzie authenticate() zmienna user jest typu Symfony\Component\Security\Core\User\User i jest poszukiwana w providerze InMemory, który ma użytkowników zdefiniowanych w security.yml. Akurat w tym przypadku nazwy użytkowników się pokrywają i jest on odnajdywany, ale wiem, że to jest luka
#Alden\SunwearBundle\Security\Authentication\Provider\mssql_securityProvider
class mssql_securityProvider implements AuthenticationProviderInterface {

private $userProvider;
private $cacheDir;

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

public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
// if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword()))
if ($user && $token->password == 'pp')
{
$authenticatedToken = new mssql_securityUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The authentication failed.');
}

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

#Alden\SunwearBundle\Controller\DefaultController
class DefaultController extends Controller {

/**
* @Route("/hello/{name}", name="hello")
* @Template
* @param string $name
*/
public function helloAction($name) { }
/**
* @Route("/login")
* @Template
*/
public function loginAction()
{
$request = $this->getRequest();
$session = $this->getRequest()->getSession();
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR))
{
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
}
else
{
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
}
return array('error' => $error);
}
}

Formularz ma specjalnie wpisane wartości, aby przyspieszyć testowanie, dla tych wartości użytkownik powinien się zalogować
#Alden\SunwearBundle\Resources\views\login.html.twig
<h1>login form</h1>
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form method="post" action="{{ path('login_check') }}">
<input name="_username" value="u"/>
<input name="_password" value="pp"/>
<input type="submit" value="Zaloguj"/>
</form>