Authentification automatique des utilisateurs après l'enregistrement

114

Nous construisons une application professionnelle à partir de zéro dans Symfony 2, et j'ai rencontré un petit problème avec le flux d'enregistrement des utilisateurs: une fois que l'utilisateur a créé un compte, il doit être automatiquement connecté avec ces informations d'identification, à la place. d'avoir été immédiatement obligé de fournir à nouveau leurs pouvoirs

Quelqu'un a-t-il eu une expérience dans ce domaine ou a-t-il pu me diriger dans la bonne direction?

Problématique
la source

Réponses:

146

Symfony 4.0

Ce processus n'a pas changé de symfony 3 à 4 mais voici un exemple utilisant le nouveau AbstractController recommandé. Les deuxsecurity.token_storagesession services et les services sont enregistrés dans la getSubscribedServicesméthode parente , vous n'avez donc pas à les ajouter dans votre contrôleur.

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends AbstractController{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->container->get('security.token_storage')->setToken($token);
        $this->container->get('session')->set('_security_main', serialize($token));
        // The user is now logged in, you can redirect or do whatever.
    }

}

Symfony 2.6.x - Symfony 3.0.x

À partir de symfony 2.6 security.context est obsolète au profit de security.token_storage. Le contrôleur peut maintenant être simplement:

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends Controller{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->get('security.token_storage')->setToken($token);
        $this->get('session')->set('_security_main', serialize($token));
    }

}

Bien que cela soit obsolète, vous pouvez toujours l'utiliser security.contextcar il a été conçu pour être rétrocompatible. Soyez juste prêt à le mettre à jour pour Symfony 3

Vous pouvez en savoir plus sur les changements 2.6 pour la sécurité ici: https://github.com/symfony/symfony/blob/2.6/UPGRADE-2.6.md

Symfony 2.3.x

Pour accomplir cela dans symfony 2.3, vous ne pouvez plus simplement définir le jeton dans le contexte de sécurité. Vous devez également enregistrer le jeton dans la session.

En supposant un fichier de sécurité avec un pare-feu comme:

// app/config/security.yml
security:
    firewalls:
        main:
            //firewall settings here

Et une action de contrôleur similaire aussi:

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use YourNameSpace\UserBundle\Entity\User;

class LoginController extends Controller{

    public function registerAction()
    {    
        $user = //Handle getting or creating the user entity likely with a posted form
        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $this->get('security.context')->setToken($token);
        $this->get('session')->set('_security_main',serialize($token));
        //Now you can redirect where ever you need and the user will be logged in
    }

}

Pour la création du jeton, vous voudrez créer un UsernamePasswordToken, Cela accepte 4 paramètres: entité utilisateur, informations d'identification de l'utilisateur, nom du pare-feu, rôles d'utilisateur. Vous n'avez pas besoin de fournir les informations d'identification de l'utilisateur pour que le jeton soit valide.

Je ne suis pas sûr à 100% que la définition du jeton sur le security.contextsoit nécessaire si vous allez simplement rediriger tout de suite. Mais cela ne semble pas faire mal, donc je l'ai laissé.

Ensuite, la partie importante, la définition de la variable de session. Les variables convention de nommage est _security_suivi de votre nom pare - feu, dans ce cas , la mainprise_security_main

Chasse
la source
1
J'ai implémenté le code, l'utilisateur connecté avec succès, mais l'objet $ this-> getUser () renvoie NULL. Une idée?
sathish
2
Des choses folles se passaient sans $this->get('session')->set('_security_main', serialize($token));. Merci, @Chausser!
Dmitriy le
1
Avec Symfony 2.6, si vous définissez le jeton pour un pare-feu nommé mainET que vous êtes authentifié avec un autre pare-feu nommé admin(car vous usurpez l'identité de l'utilisateur), une chose étrange se produit: _security_adminobtient le UsernamePasswordTokenavec l'utilisateur que vous avez fourni, c'est-à-dire que vous êtes "déconnecté" de votre adminpare - feu. Une idée comment maintenir le jeton pour le pare-feu "admin"?
gremo
1
Pour être honnête, je ne suis pas sûr que vous puissiez être authentifié pour 2 pare-feu en même temps, je vais le regarder mais en attendant, vous devriez poser une question distincte
Chase le
3
@Chausser a réussi à le faire fonctionner. Votre réponse est parfaitement correcte (et elle est mise à jour), la seule chose qui fonctionne uniquement lorsque vous appelez setToken(..)sous le même pare - feu cible ou sans être encore authentifié .
gremo
65

J'ai finalement compris celui-ci.

Après l'enregistrement de l'utilisateur, vous devriez avoir accès à une instance d'objet de tout ce que vous avez défini comme entité utilisateur dans la configuration de votre fournisseur. La solution consiste à créer un nouveau jeton avec cette entité utilisateur et à le transmettre dans le contexte de sécurité. Voici un exemple basé sur ma configuration:

RegistrationController.php:

$token = new UsernamePasswordToken($userEntity, null, 'main', array('ROLE_USER'));
$this->get('security.context')->setToken($token);

mainest le nom du pare-feu de votre application (merci, @Joe). C'est vraiment tout ce qu'il y a à faire; le système considère désormais votre utilisateur entièrement connecté comme l'utilisateur qu'il vient de créer.

EDIT: Selon le commentaire de @ Miquel, j'ai mis à jour l'exemple de code du contrôleur pour inclure un rôle par défaut raisonnable pour un nouvel utilisateur (bien que cela puisse évidemment être ajusté en fonction des besoins spécifiques de votre application).

Problématique
la source
2
Ce n'est pas tout à fait correct avec la version commerciale de Symfony 2. Vous devez passer les rôles de l'utilisateur comme quatrième argument au constructeur UsernamePasswordToken, sinon il sera marqué comme non authentifié et l'utilisateur n'aura aucun de ses rôles.
Michael
Qu'en est-il du drapeau "Remember me"? Comment connecter les utilisateurs manuellement, mais ils doivent également être connectés pour toujours. Ce morceau de code ne résout pas ce problème.
maectpo
@maectpo qui n'était pas dans le cadre de mes exigences d'origine, mais cela semble être une excellente réponse de suivi. Faites-nous savoir ce que vous proposez.
Problématique
J'ai un problème. Je peux me connecter de cette manière, mais la variable app.user est vide. Connaissez-vous un moyen de renseigner cette variable dans ce processus de connexion? - J'envoie l'utilisateur (chaîne) et le mot de passe (chaîne) comme le dit la référence: api.symfony.com/2.0/Symfony/Component/Security/Core/…
unairoldan
1
Comme Marc l'a dit ci-dessous, vous devez enregistrer l'espace de noms UsernamePasswordToken:use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
MrGlass
6

Si vous avez un objet UserInterface (et cela devrait être le cas la plupart du temps), vous souhaiterez peut-être utiliser la fonction getRoles qu'il implémente pour le dernier argument. Donc, si vous créez une fonction logUser, cela devrait ressembler à ça:

public function logUser(UserInterface $user) {
    $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
    $this->container->get('security.context')->setToken($token);
}
Cédric Nirousset
la source
6

J'utilise Symfony 2.2 et mon expérience était légèrement différente de celle de Problematic , c'est donc une version combinée de toutes les informations de cette question plus certaines des miennes.

Je pense que Joe se trompe sur la valeur de $providerKey, le troisième paramètre pour le UsernamePasswordTokenconstructeur. C'est censé être la clé d'un fournisseur d'authentification (pas d'utilisateur). Il est utilisé par le système d'authentification pour distinguer les jetons créés pour différents fournisseurs. Tout fournisseur qui descend de UserAuthenticationProvidern'authentifiera que les jetons dont la clé de fournisseur correspond à la sienne. Par exemple, le UsernamePasswordFormAuthenticationListenerdéfinit la clé du jeton qu'il crée pour correspondre à celle de son correspondant DaoAuthenticationProvider. Cela permet à un seul pare-feu d'avoir plusieurs fournisseurs de nom d'utilisateur et de mot de passe sans qu'ils se marient les uns les autres. Nous devons donc choisir une clé qui n'entrera en conflit avec aucun autre fournisseur. J'utilise 'new_user'.

J'ai quelques systèmes dans d'autres parties de mon application qui dépendent de l' événement de réussite d'authentification , et qui ne sont pas déclenchés simplement en définissant le jeton sur le contexte. Je devais récupérer le EventDispatchercontenu du conteneur et déclencher l'événement manuellement. J'ai décidé de ne pas déclencher également un événement de connexion interactif car nous authentifions l'utilisateur implicitement, et non en réponse à une demande de connexion explicite.

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;

$user = // get a Symfony user instance somehow
$token = new UsernamePasswordToken(
        $user, null, 'new_user', $user->getRoles() );
$this->get( 'security.context' )->setToken( $token );
$this->get( 'event_dispatcher' )->dispatch(
        AuthenticationEvents::AUTHENTICATION_SUCCESS,
        new AuthenticationEvent( $token ) );

Notez que l'utilisation de $this->get( .. )suppose que l'extrait de code est dans une méthode de contrôleur. Si vous utilisez le code ailleurs, vous devrez les modifier pour les appeler ContainerInterface::get( ... )d'une manière appropriée à l'environnement. En l'occurrence, mes entités utilisateur sont implémentées UserInterfacepour que je puisse les utiliser directement avec le jeton. Si ce n'est pas le cas, vous devrez trouver un moyen de les convertir enUserInterface instances.

Ce code fonctionne, mais j'ai l'impression qu'il pirate l'architecture d'authentification de Symfony plutôt que de travailler avec. Il serait probablement plus correct d' implémenter un nouveau fournisseur d'authentification avec sa propre classe de jeton plutôt que de détourner le fichier UsernamePasswordToken. En outre, l'utilisation d'un fournisseur approprié signifierait que les événements ont été gérés pour vous.

Sam Hanes
la source
4

Au cas où quelqu'un aurait la même question de suivi qui m'a fait revenir ici:

Appel

$this->container->get('security.context')->setToken($token); 

affecte uniquement le courant security.contextde l'itinéraire utilisé.

C'est-à-dire que vous ne pouvez connecter un utilisateur qu'à partir d'une URL sous le contrôle du pare-feu.

(Ajoutez une exception pour l'itinéraire si nécessaire - IS_AUTHENTICATED_ANONYMOUSLY)

démonl
la source
1
savez-vous comment vous faites cela pour une session? Plutôt que pour la demande actuelle?
Jake N
3

Avec Symfony 4.4, vous pouvez simplement faire ce qui suit dans votre méthode de contrôleur (voir dans la documentation Symfony: https://symfony.com/doc/current/security/guard_authentication.html#manually-authenticating-a-user ):

// src/Controller/RegistrationController.php
// ...

use App\Security\LoginFormAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request)
    {
        // ...

        // after validating the user and saving them to the database
        // authenticate the user and use onAuthenticationSuccess on the authenticator
        return $guardHandler->authenticateUserAndHandleSuccess(
            $user,          // the User object you just created
            $request,
            $authenticator, // authenticator whose onAuthenticationSuccess you want to use
            'main'          // the name of your firewall in security.yaml
        );
    }
}

Une chose importante, assurez-vous que votre pare-feu n'est pas défini sur lazy. Si tel est le cas, le jeton ne sera jamais stocké dans la session et vous ne serez jamais connecté.

firewalls:
    main:
        anonymous: ~ # this and not 'lazy'
Etienne Noël
la source
2

Comme Problematic ici déjà mentionné, ce paramètre insaisissable $ providerKey n'est en réalité rien de plus que le nom de votre règle de pare-feu, 'foobar' dans le cas de l'exemple ci-dessous.

firewalls:
    foobar:
        pattern:    /foo/
Nim
la source
Pouvez-vous m'expliquer pourquoi si je passe la chaîne any par exemple en blablablatant que troisième paramètre à UsernamePasswordToken, cela fonctionnera également? que signifie ce paramètre?
Mikhail
1
Ce paramètre lie votre jeton à un fournisseur de pare-feu spécifique. Dans la plupart des cas, vous n'aurez qu'un seul fournisseur, alors ne vous inquiétez pas.
Gottlieb Notschnabel
2

J'ai essayé toutes les réponses ici et aucune n'a fonctionné. La seule façon dont je pourrais authentifier mes utilisateurs sur un contrôleur est de faire une sous-demande puis de rediriger. Voici mon code, j'utilise silex mais vous pouvez facilement l'adapter à symfony2:

$subRequest = Request::create($app['url_generator']->generate('login_check'), 'POST', array('_username' => $email, '_password' => $password, $request->cookies->all(), array(), $request->server->all());

$response = $app->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, false);

return $app->redirect($app['url_generator']->generate('curriculos.editar'));
Diego Castro
la source
1

Sur Symfony version 2.8.11 (fonctionnant probablement pour les versions plus anciennes et plus récentes), si vous utilisez FOSUserBundle, faites simplement ceci:

try {
    $this->container->get('fos_user.security.login_manager')->loginUser(
    $this->container->getParameter('fos_user.firewall_name'), $user, null);
} catch (AccountStatusException $ex) {
    // We simply do not authenticate users which do not pass the user
    // checker (not enabled, expired, etc.).
}

Pas besoin de répartir l'événement comme je l'ai vu dans d'autres solutions.

inspiré de FOS \ UserBundle \ Controller \ RegistrationController :: authenticateUser

(à partir de la version composer.json FOSUserBundle: "friendsofsymfony / user-bundle": "~ 1.3")

Nico
la source