6
votes

I would like to write a basic login form, which authenticates users by sending a request to an external REST API. The external API receives the login/password and return 200 (ok) if the credentials are correct. However, I can't implement it via the UserProviderInterface, because the external REST API give me the password in the reply. (I can't fill the user password in the loadUserByUsername method).

I found a valid solution here, but it uses classes that have been removed in Symfony 3 : Symfony2 custom connection by web service

I made a test with a custom Authenticator, which only checks that the password is "toto", but I get a redirection loop and my dummy UserProvider is still called :

<?php
namespace AppBundle\Security\User;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface;

class WebserviceAuthenticator implements SimpleFormAuthenticatorInterface
{
    private $encoder;

    public function __construct(UserPasswordEncoderInterface $encoder)
    {
        $this->encoder = $encoder;
    }

    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        $user = new WebserviceUser($token->getUsername(), $token->getCredentials(), null, ['ROLE_ADMIN']);

        // HERE : call the external REST API
        if ($token->getCredentials() === 'toto') {
            $token = new UsernamePasswordToken(
                $user,
                $user->getPassword(),
                'main',
                $user->getRoles()
            );
            return $token;
        }
        throw new CustomUserMessageAuthenticationException('Invalid username or password');
    }

    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof UsernamePasswordToken
        && $token->getProviderKey() === $providerKey;
    }

    public function createToken(Request $request, $username, $password, $providerKey)
    {
        return new UsernamePasswordToken($username, $password, $providerKey);
    }
}
1
Did you look at Guard Authentication already? It is a super flexible but still easy way to create custom authentication mechanisms.Tobias Xy
Yes. I just made it work when I removed the "refreshUser" implementation of the UserProvider. Now, I do : public function refreshUser(UserInterface $user) { if (!$user instanceof WebserviceUser) { throw new UnsupportedUserException( sprintf('Instances of "%s" are not supported.', get_class($user)) ); } return $user; } Is the no risks ?Kiruahxh
Should be fine, I think.Tobias Xy
Thanks you, it's working. Would you have an idea for this post ? stackoverflow.com/questions/46368737/…Kiruahxh

1 Answers

8
votes

I got it working with that implementation :

security.yml

providers:
    webservice:
        id: AppBundle\Security\User\WebserviceUserProvider

encoders:
    AppBundle\Entity\WebserviceUser: plaintext

firewalls:
    # disables authentication for assets and the profiler, adapt it according to your needs
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    main:
        anonymous: ~
        provider: webservice
        pattern: ^/
        form_login:
            check_path: login
            login_path: login
            use_forward: true
        logout: ~
        guard:
            authenticators:
                - app.webservice_authenticator

    login:
        pattern: ^/login$
        anonymous: ~

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

role_hierarchy:
    ROLE_ADMIN:       ROLE_USER

services.yml

services:
    app.webservice_authenticator:
        class: AppBundle\Security\User\WebserviceAuthenticator

User Provider

namespace AppBundle\Security\User;

use AppBundle\Entity\WebserviceUser;
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;

class WebserviceUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        return new WebserviceUser($username, null, null, ['ROLE_USER']);
    }

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

    public function supportsClass($class)
    {
        return WebserviceUser::class === $class;
    }
}

Authenticator

<?php

namespace AppBundle\Security\User;

use AppBundle\Service\RestClient;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;


class WebserviceAuthenticator extends AbstractFormLoginAuthenticator
{
    private $container;
    private $restClient;

    public function __construct(ContainerInterface $container, RestClient $restClient)
    {
        $this->container = $container;
        $this->restClient = $restClient;
    }

    public function getCredentials(Request $request)
    {
        if ($request->getPathInfo() != '/login' || $request->getMethod() != 'POST') {
            return;
        }

        $username = $request->request->get('_username');
        $request->getSession()->set(Security::LAST_USERNAME, $username);
        $password = $request->request->get('_password');

        return array(
            'username' => $username,
            'password' => $password
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        //dump($credentials); die();
        if (array_key_exists('username', $credentials) == false) {
            return null;
        }
        $username = $credentials['username'];
        $password = strtoupper($credentials['password']);
        if ($username == '') {
            return null;
        }

        // Here the business code, provide your own implementtion
        if ($this->restClient->IsValidLogin($username, $password)) {
            return new WebserviceUser($username, $password, null, ['ROLE_USER']);
        } else {
            throw new CustomUserMessageAuthenticationException('Invalid credentials');
        }
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        // AJAX! Return some JSON
        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(array('message' => $exception->getMessageKey()), 403);
        }

        // for non-AJAX requests, return the normal redirect
        return parent::onAuthenticationFailure($request, $exception);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // AJAX! Return some JSON
        if ($request->isXmlHttpRequest()) {
            return new JsonResponse(array('userId' => $token->getUser()->getId()));
        }

        // for non-AJAX requests, return the normal redirect
        return parent::onAuthenticationSuccess($request, $token, $providerKey);
    }

    protected function getLoginUrl()
    {
        return $this->container->get('router')
        ->generate('login');
    }

    protected function getDefaultSuccessRedirectUrl()
    {
        return $this->container->get('router')
        ->generate('homepage');
    }
}

The trick seems to be :

  1. to implement password validation in the getUser method of the authenticator, and have checkCredentials method always return true.
  2. to disable the refreshUser method of UserProvider