Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom authentication in a Symfony 3 using external REST API

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);
    }
}
like image 606
Kiruahxh Avatar asked Sep 22 '17 10:09

Kiruahxh


1 Answers

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
like image 179
Kiruahxh Avatar answered Sep 18 '22 10:09

Kiruahxh