Search code examples
phpsymfonysymfony4symfony-securitysymfony5

How do I redirect an anonymous user to custom page instead of the login page if they do not have access to a URL?


I created a login form for my Symfony 5 project.

I want redirect anonymous user to custom page (not the login page) when the user does not have access to my admin panel.

The application controller:

// TestController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

class TestController extends AbstractController
{
    /**
     * @Route("/admin/panel", name="test")
     * @IsGranted("ROLE_ADMIN")
     */
    public function index()
    {

        return $this->render('test/index.html.twig', [
            'controller_name' => 'TestController',
        ]);
    }
}

And my configuration settings:

# security.yaml
security:
    encoders:
        App\Entity\Admin:
            algorithm: auto

    providers:
        admin:
            entity:
                class: App\Entity\Admin
                property: username

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

            guard:
                authenticators:
                    - App\Security\AdminLoginFormAuthenticator
            logout:
                path: app_logout
        main:
            anonymous: lazy

My application authenticator:

// AdminLoginFormAuthenticator.php 
namespace App\Security;

use App\Entity\Admin;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class AdminLoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
    use TargetPathTrait;

    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;

    public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function supports(Request $request)
    {
        return 'app_admin' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'username' => $request->request->get('username'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['username']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(Admin::class)->findOneBy(['username' => $credentials['username']]);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('...!');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    /**
     * Used to upgrade (rehash) the user's password automatically over time.
     */
    public function getPassword($credentials): ?string
    {
        return $credentials['password'];
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        return new RedirectResponse($this->urlGenerator->generate('homepage'));
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate('app_admin');
    }
}

Solution

  • Your AdminLoginFormAuthenticator extends on AbstractFormLoginAuthenticator, which includes the following:

       /**
         * Override to control what happens when the user hits a secure page
         * but isn't logged in yet.
         *
         * @return RedirectResponse
         */
        public function start(Request $request, AuthenticationException $authException = null)
        {
            $url = $this->getLoginUrl();
    
            return new RedirectResponse($url);
        }
    

    Reading this, what you need to do is obvious.

    Just create your own start() method to control what happens (e.g. where are they redirected) if they do not have access.

    namespace App\Security;
    
    class AdminLoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
    {
    // the rest of your class
    
        public function start(Request $request, AuthenticationException $authException = null)
        {
            $url = 'https:://example.com/whateverurlyouwanttoredirectto';
    
            return new RedirectResponse($url);
        }
    }