knpuniversity / oauth2-client-bundle

Easily talk to an OAuth2 server for social functionality in Symfony
https://symfonycasts.com
MIT License
787 stars 145 forks source link

Invalid state parameter passed in callback URL. #390

Closed Chirikumo89 closed 1 year ago

Chirikumo89 commented 1 year ago

I have a problem with Oauth2. It returns me as error "Invalid state parameter passed in callback URL."

I'm on symfony 6

I don't know where this can come from, in localhost everything works but in production I have this error.

I looked everywhere for a solution without finding anything.

knpu_oauth2_client.yaml

knpu_oauth2_client:
    clients:
      azure:
            type: azure
            client_id: '%env(OAUTH_AZURE_CLIENT_ID)%'
            client_secret: '%env(OAUTH_AZURE_CLIENT_SECRET)%'
            redirect_route: connect_azure_check
            redirect_params: {}
            # scope: {}
            tenant: '%env(AZURE_TENANT_ID)%'

security.yaml

security:
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords

    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        users_in_memory: { memory: null }
        my_provider:
            entity: {class: App\Entity\User, property: uuid}
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: my_provider
            custom_authenticators:
               - App\Security\AzureAuthenticator
            logout: true

      # activate different ways to authenticate
      # https://symfony.com/doc/current/security.html#firewalls-authentication

      # https://symfony.com/doc/current/security/impersonating_user.html
      # switch_user: true

  # Easy way to control access for large sections of your site
  # Note: Only the *first* access control that matches will be used
    access_control:
     - { path: ^/connect/azure, role: PUBLIC_ACCESS }
     - { path: ^/, roles: ROLE_USER}

AzureController.php

<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class AzureController extends AbstractController
{
    /**
     * Cette fonction effectue la connexion avec Azure
     * Ex: Si vous allez sur cette route, un formulaire microsoft vous demandera de vous connecter
     */
    #[Route('/connect/azure', name: 'connect_azure', )]
    public function connectAction(ClientRegistry $clientRegistry)
    {
        return $clientRegistry
        ->getClient('azure')
        ->redirect([
            'openid', 'profile', 'email'
        ], []);

    }

    /**
     * Cette fonction permet de savoir si l'authentification à réussi
     * Ex: Après vous être connecté ci-dessus, vous serez rediriger sur cette route qui vous redirigera à son tour vers la route home
     */
    #[Route('/connect/azure/check', name: 'connect_azure_check', schemes:['http'])]
    public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
    {
        try {
            return $this->redirectToRoute('home');
        } catch (IdentityProviderException $e) {
            return new JsonResponse(array('status' => false, 'message' => "User not found!", 'error' => $e->getMessage()));
        }

    }
}

AzureAuthenticator.php

<?php

namespace App\Security;

use App\Entity\User;
use League\OAuth2\Client\Provider\azureUser;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class AzureAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
    private ClientRegistry $clientRegistry;
    private EntityManagerInterface $entityManager;
    private RouterInterface $router;

    public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
    {
        $this->clientRegistry = $clientRegistry;
        $this->entityManager = $entityManager;
        $this->router = $router;
    }

    /**
     * Cette fonction renvoie true alors la fonction authenticate sera appelée
     * @param Request $request
     * @return bool|null
     */
    public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_route') === 'connect_azure_check';
    }

    /**
     * Cette fonction permet de traiter les données et de les utiliser. Elle vérifie également si l'utilisateur est déjà existant en base de donnée, si se n'est pas le cas elle l'ajoute.
     * @param Request $request
     * @return Passport
     */
    public function authenticate(Request $request): Passport
    {
        $client = $this->clientRegistry->getClient('azure');
        $accessToken = $this->fetchAccessToken($client);

        return new SelfValidatingPassport(
            new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
                /** @var AzureUser $AzureUser */
                $AzureUser = $client->fetchUserFromToken($accessToken);
                // 1) have they logged in with Azure before? Easy!
                $existingUser = $this->entityManager->getRepository(User::class)->findOneBy(['uuid' => $AzureUser->getId()]);

                if ($existingUser) {
                    return $existingUser;
                }

                $user = new User();
                $user->setUuid($AzureUser->getId());
                $user->setNom($AzureUser->claim('family_name'));
                $user->setPrenom($AzureUser->claim('given_name'));
                $user->setEmail($AzureUser->claim('upn'));
                $this->entityManager->persist($user);
                $this->entityManager->flush();
                return $user;
            })
        );
    }

    /**
     * Si l'authentification réussi, l'utilisateur sera renvoyé sur la route home
     * @param Request $request
     * @param TokenInterface $token
     * @param string $firewallName
     * @return Response|null
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        $targetUrl = $this->router->generate('home');
        return new RedirectResponse($targetUrl);
    }

    /**
     * Si l'authentification échoue, l'utilisateur sera informé avec un message d'erreur
     * @param Request $request
     * @param AuthenticationException $exception
     * @return Response|null
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    /**
     * Cette fonction permet de rediriger l'utilisateur sur la route de connexion
     * Dès que l'utilisateur sera sur une route qu'il n'a pas le droit d'avoir accès il sera rediriger à cet endroit (Dans le cas de notre application toutes les routes sont par défaut interdite)
     * @param Request $request
     * @param AuthenticationException|null $authException
     * @return Response
     */
    public function start(Request $request, AuthenticationException $authException = null): Response
    {
        return new RedirectResponse(
            '/connect/azure',
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }

}

When I add use_state: true in parameter another error is returned: "Authentication failed! Did you authorize our app?"

weaverryan commented 1 year ago

Hi! I'm not sure the problem, but here is a similar issue https://github.com/knpuniversity/oauth2-client-bundle/issues/168

Chirikumo89 commented 1 year ago

No,

I have identified my problem, but can't seem to fix it.

On my /connect/azure route when I pass a random parameter to see if the GET is working nothing is returned

example: /connect/azure?foo=bar

So if I dump $_GET or $request->query->get('foo') an empty array and null are returned

But when I try another route in my form for example no problem !

Any ideas ?

weaverryan commented 1 year ago

Hmm, I'm not really sure how that's possible. If the current URL has ?foo=bar on it, then $_GET will absolutely contain that - I can't think of any situation where that hasn't worked. Sorry :/

Chirikumo89 commented 1 year ago

No worries, locally everything works fine but on the server it doesn't, is it due to the server configuration?

weaverryan commented 1 year ago

It could be - that SMELLS very strange... - I would also dump $_SERVER - iirc, some of the vars in there should contain the current URL - you can see if it shows the query params, just to help understand what's going on.

Good luck!

Chirikumo89 commented 1 year ago

The request ?foo=bar works only on my route home '/', on the other routes nothing works