Configurer WSSE sur Symfony avec le FOSRestBundle

Share Button

Read the English version

Comme REST doit être sans état (stateless) :

chaque requête d’un client vers un serveur doit contenir toute l’information nécessaire pour permettre au serveur de comprendre la requête, sans avoir à dépendre d’un contexte conservé sur le serveur. Cela libère de nombreuses interactions entre le client et le serveur.

Article Wikipedia sur REST

Nous avions besoin d’ajouter un nouvel authentication provider dans Symfony2 pour respecter cette contrainte. Nous avons choisi WSSE.

Cet article explique comment configurer WSSE sur Symfony, comment l’utiliser avec FOSRestBundle et tester le tout avec Google Chrome.

Quelques mots à propos du salt

Avant tout, nous voulions expliquer la gestion du mot de passe dans ce cas particulier.

Dans FOSUserBundle, un salt est utilisé pour améliorer la sécurité. Comme vous allez le constater dans la configuration du WSSE, le mot de passe utilisé pour vérification du digest est celui de l’utilisateur présent en base de données (encodé avec le salt).

Pour le bon fonctionnement de notre authentification, nous aurons donc besoin de générer un passwordDigest coté client. Ce passwordDigest est bien entendu également encodé a partir du mot de passe utilisateur et du salt.

En d’autres termes :

Pour conserver uniquement le mot de passe encrypté coté client, et non le mot de passe en clair, nous aurons besoin de connaitre le salt : ce salt doit être une propriété publique dans la méthode getUser de l’API.

Nous avons été surpris par cette conclusion, puis nous avons lu quelques articles au sujet de la sécurité, et du salt en particulier.
Ce qu’il faut comprendre, c’est le but du salt. Il est utilisé pour éviter les attaques de type “Rainbow tables” sur un mot de passe encrypté.
“Si une mauvaise personne récupère toute votre table d’utilisateurs, elle ne peut pas décoder tous les mots de passe en utilisant une rainbow table générée”.

Le salt sert donc uniquement dans le cas où quelqu’un récupère votre base de données… Et s’il récupère votre table d’utilisateurs, il récupère le salt. Nous pouvons donc partager le salt sans crainte et le mot de passe utilisateur ne transitera pas sur le réseau.

Informations

Outils

Des Bundles existent déjà pour faire de l’authentification WSSE (Merci Google). Mais comme ce composant touche à la sécurité, et par curiosité, nous avons décidé de configurer cette partie depuis zéro pour comprendre profondément le fonctionnement du WSSE.

Nous vous invitons à lire cet article à propos du WSSE.

Comment installer le WSSE sur Symfony

Cette partie est décrite dans le Cookbook de Symfony2 sur le WSSE

Suivez simplement cet article. Soyez attentif aux problèmes de firewall si vous utilisez plusieurs providers.

Voici comment nous avons fait, et comment nous l’avons inseré comme premier firewall.

#app/config/security.yml
  # ...
  security:
    # ...
    firewalls:  
       wsse_secured:
          pattern:   ^/api/.*
          stateless:    true
          wsse: true
          anonymous : true
       # ...

Nous avons fait le choix du pattern en accord avec le pattern de notre routing dans la configuration Rest :

#app/routing_yml
#REST 
rest : 
  type : rest 
  resource : "routing_rest.yml"
  prefix : /api

Et voila, votre authentification WSSE est prête.

Dans notre cas, l’utilisateur est fourni par FOSUserBundle, mais authentifié par WSSE.

Nous avons aussi réécrit une partie de l’AuthenticationProvider de cette manière en ajoutant des AuthentificationException(s).

//Obtao\UserBundle\Security\Authentication\Provider 

  // ...

  public function authenticate(TokenInterface $token){
    $user = $this->userProvider->loadUserByUsername($token->getUsername());
    if(!$user){
      throw new AuthenticationException("Bad credentials... Did you forgot your username ?");
    }
    if ($user && 
    $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
      // ...
    }
  }

  protected function validateDigest($digest, $nonce, $created, $secret){
        
    // Check created time is not in the future
    if (strtotime($created) > time()) {
      throw new AuthenticationException("Back to the future...");
    }

    // Expire timestamp after 5 minutes
    if (time() - strtotime($created) > 300) {
      throw new AuthenticationException("Too late for this timestamp... Watch your watch.");
    }

    // Validate nonce is unique within 5 minutes
    if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
      throw new NonceExpiredException('Previously used nonce detected');
    }

    // If cache directory does not exist we create it
    if (!is_dir($this->cacheDir)) {
      mkdir($this->cacheDir, 0777, true);
    }

    file_put_contents($this->cacheDir.'/'.$nonce, time());

    // Validate Secret
    $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));

    if($digest !== $expected){
      throw new AuthenticationException("Bad credentials ! Digest is not as expected.");
    }

    return true;
  }

Logguer les erreurs et retourner une 403 avec message

Voici les changements à réaliser pour sauvegarder les logs du WSSE dans des fichiers séparés.

Dans le services.yml de votre bundle, injectez le service logger, et choisissez un canal (ici”wsse“) :

#Obtao\UserBundle\Resources\config\services.yml
services : 
  wsse.security.authentication.listener:
    class:  Obtao\UserBundle\Security\Firewall\WsseListener
    arguments: ["@security.context", "@security.authentication.manager","@logger"]
    tags:
      - { name: monolog.logger, channel: wsse }

Dans le listener WSSE, injectez les services et ajoutez une écriture de log en cas de AuthenticationException. Nous avons choisi de ne pas rediriger l’utilisateur, mais plutôt d’afficher un message d’erreur (qui sera retraité par notre “client”).

namespace Obtao\UserBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Obtao\UserBundle\Security\Authentication\Token\WsseUserToken;
use Symfony\Component\HttpKernel\Log\LoggerInterface;

class WsseListener implements ListenerInterface
{
    protected $securityContext;
    protected $authenticationManager;
    protected $logger;

    public function __construct(SecurityContextInterface $securityContext,
                               AuthenticationManagerInterface $authenticationManager,
                               LoggerInterface $logger
    ){
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
        $this->logger = $logger;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
        if (!$request->headers->has('x-wsse')
         || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)
        ) {
            return;
        }

        $token = new WsseUserToken();
        $token->setUser($matches[1]);

        $token->digest   = $matches[2];
        $token->nonce    = $matches[3];
        $token->created  = $matches[4];

        try {
            $authToken = $this->authenticationManager->authenticate($token);
            $this->securityContext->setToken($authToken);
            return;
        } catch (AuthenticationException $failed) {
            $failedMessage = 'WSSE Login failed for '.$token->getUsername().'. Why ? '.$failed->getMessage();
            $this->logger->err($failedMessage);
            
            // Deny authentication with a '403 Forbidden' HTTP response
            $response = new Response();
            $response->setStatusCode(403);
            $response->setContent($failedMessage);
            $event->setResponse($response);
            return; 
            
        }
        // By default deny authorization
        $response = new Response();
        $response->setStatusCode(403);
        $event->setResponse($response);
    }
}

Dans le fichier config_prod.yml, déclarez votre canal et associez lui un nom de fichier.

//app/config/config_prod.yml
monolog:
    handlers:
        main:
            type          : fingers_crossed
            action_level  : error
            handler       : nested
            channels      : [!wsse]
            
        wsse:
            type: stream
            path: %kernel.logs_dir%/%kernel.environment%.wsse.log
            level: error
            channels: [wsse]
            
        nested:
            type:  stream
            path:  %kernel.logs_dir%/%kernel.environment%.log
            level: debug

Pour plus d’informations vous pouvez lire notre article comment créer un nouveau canal de log pour un service Symfony2 (en anglais)

Tester l’accès via WSSE à votre API (GET).

La communication WSSE se fait via HTTP et est basé sur un header.

Pour tester notre API nous avons donc à :

- Générer le header WSSE (avec le nonce et la date)
- L’envoyer au serveur

Pour la génération du header wsse, nous utilisons : http://www.teria.com/~koseki/tools/wssegen/ .Simple, rapide, efficace. (“auto nonce”, “auto date”. Pas de “before wsse”. “User = [username dans votre base]“, “password = [password dans votre base])

Cliquez pour générer le header et copiez la valeur. Par exemple :

UsernameToken Username="francois",PasswordDigest="LlsDqVDaw5nMs1iasbladXWvs5c=",Nonce="YzdlMzQ3NWQ4MTc1YTI3OA==", Created="2013-05-30T07:53:54Z"

Installez et ouvrez la Rest Console de Chrome. Champs à utiliser :

  • Request API : Adresse de votre serveur (http://obtao.localhost/app_dev.php/api/me chez nous)
  • Custom Header (+) :
    • Parameter = “x-wsse“,
    • Value
      UsernameToken Username=”francois”,PasswordDigest=”LlsDqVDaw5nMs1iasbladXWvs5c=”,Nonce=”YzdlMzQ3NWQ4MTc1YTI3OA==”, Created=”2013-05-30T07:53:54Z”[valeur générée au dessus]
  • Authorization Hearder :  Authorization profile=”UsernameToken”

Appuyez sur le bouton GET ! :)

Si le header username est envoyé au wsse(coté serveur), Symfony essaiera d’authentifier l’utilisateur. Si aucun utilisateur n’est fourni, Symfony autorisera une connexion anonyme (voir le security.yml).

Vous pouvez maintenant tester l’accès à votre API via WSSE. Aucun lien/etat/cookie n’est requis. Ni coté client, ni coté serveur.  Vous pouvez vérifier et récupérer votre user dans vos controlleurs d’API.

Pour plus d’information, et pour comprendre l’utilisation qui peut être faite dans une application Android, vous pouvez lire cet article : “Comment utiliser wsse dans une application Android”

Share Button

40 thoughts on “Configurer WSSE sur Symfony avec le FOSRestBundle

  1. Merci beaucoup pour le tutoriel !

    Et merci pour le tuto suivant visant à utiliser wsse dans une application Android =)

    Attention à tous ceux qui lisent en travers comme moi :
    le firewall wsse_secured doit bien être placé AVANT celui du main !

  2. Bonjour,

    J’ai suivi votre tutoriel à la lettre et ai une erreur lors de l’envoi du header:

    UsernameToken Username=”pierre”, PasswordDigest=”isvuv7EO434yFKWbVKyABCdRQO4=”, Nonce=”NmE1YTA2NDAxN2NmMTk5OA==”, Created=”2014-04-16T23:40:01Z”

    Le mot de passe est “pierre” et le nom d’utilisateur “pierre” (très difficile je sais :)). Cet utilisateur est présent en base de donnée, mais j’ai malgré tout une erreur lors de l’envois de la requête —> WSSE Login failed for pierre. Why ? Bad credentials ! Digest is not as expected.
    Je ne sais vraiment pas d’ou cette erreur peut venir, surtout que l’utilisateur est bien trouvé en base de donnée (la requête est bonne dans les logs).

    Auriez-vous une petite idée du problème à tout hasard ?

    Merci !

    • J’ai oublié d’ajouter que mon digest est effectivement différent de mon expected.

      var_dump($expected) ===> I9bcC8F30JADDCdzE3LfCP9r4vQ
      var_dump($digest) ===> xR2M1fUs2QrNqwKjsyVq056vHk8

    • Bonjour,

      La première idée qui me vient à l’esprit :
      Le password utilisé pour générer le password digest doit être tel qu’il est dans votre base : c’est a dire le password hashé et pas le mot de passe en clair.
      Cela évite coté “client” de devoir stocker le mot de passe utilisateur en clair.

      Qu’avez vous utilisé pour générer le digest?

      François

      • Si tu utilises fos user bundle pour hashé le mot de passe avec le salt il faut utilisé le service lié:
        dans mon Security controller :

        public function postTokenCreateAction()
            {
        
                $view = FOSView::create();
                $request = $this->getRequest();
        
                $username = $request->get('username');
                $password = $request->get('password');
        
                $em  =$this->getDoctrine()->getManager();
                $user = $em->getRepository('ApplicationSonataUserBundle:User')->findOneByUsername($username);
                $encoder_service = $this->get('security.encoder_factory');
                $encoder = $encoder_service->getEncoder($user);
                $encoded_pass = $encoder->encodePassword($password, $user->getSalt());
        
                if (!$user) {
                    throw new AccessDeniedException("Wrong user");
                }
        
                $created = date('c');
                $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
                $nonceHigh = base64_encode($nonce);
                $passwordDigest = base64_encode(sha1($nonce . $created . $encoded_pass . "{".$user->getSalt()."}", true));
                $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
                $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
                $view->setHeader("x-wsse", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"");
                $data = array('x-wsse' => $header);
                $view->setStatusCode(200)->setData($data);
                return $view;
            }
        
  3. En effet ! Je gère les utilisateur avec FOSUserBundle, les mots de passes sont donc encodés (cryptés avec un sha512 + salt).
    J’ai fais un test en mettant directement le mot de passe crypté en base (encodés avec la teria) et ça passe bien.
    Mais cette fois-ci avec une autre erreur:
    WSSE Login failed for pierre. Why ? No Authentication Provider found for token of class “Acme\UserBundle\Security\Authentication\Token\WsseUserToken”.

    Merci beaucoup pour votre réponse !

    • Il faut vérifier le code et la déclaration du WsseProvider (Security\Authentication\Provider\WsseProvider.php)

      Une méthode “supports” doit être présente et retourner “true” si le token est de type WsseUserToken. (Pensez à vérifier les use en début de fichier)

          public function supports(TokenInterface $token)
          {
              return $token instanceof WsseUserToken;
          }
      

      Si tout est bon, vérifiez la chaine de déclaration du WsseProvider (services.yml, wssefactory)

      • J’ai vérifié tous mes use et ils sont tous bon. Pareil pour la méthode supports, elle est implémenté.
        Qu’entendez-vous par “vérifiez la chaine de déclaration du WsseProvider (services.yml, wssefactory)”?

    • Oui elle est appelée et je passe bien dans la seconde condition

      public function authenticate(TokenInterface $token){
      $user = $this->userProvider->loadUserByUsername($token->getUsername());
      if(!$user){
      throw new AuthenticationException(“Bad credentials… Did you forgot your username ?”);
      }
      if ($user &&
      $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword(), $user->getSalt(), $user)) {
      }
      }

      • $user->getSalt(), $user // Retiré des paramètres depuis (c’était pour du debug)

          • C’est un petit oubli dans la méthode authenticate, elle ne retourne pas de token si tout s’est bien passé ;)

            public function authenticate(TokenInterface $token){
                    $user = $this->userProvider->loadUserByUsername($token->getUsername());
                    if(!$user){
                      throw new AuthenticationException("Bad credentials... Did you forgot your username ?");
                    }
                    if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
                        $authenticatedToken = new WsseUserToken($user->getRoles());
                        $authenticatedToken->setUser($user);
            
                        return $authenticatedToken;
                    }
                    throw new AuthenticationException('The WSSE authentication failed.');
                }
            
  4. Mon dieu !
    Je ne me suis pas posé une seule la fois la question du “mais que retourne la méthode authenticate si tout ce passe bien ?”…
    Votre réponse servira certainement à débloquer les futurs lecteurs :)

    Autre chose,

    Maintenant que tout fonctionne bien et que l’authentification fonctionne, concrètement, comment cela ce gère avec un front réalisé avec Backbone ?
    Je dois générer le header avec:
    - Username
    - PasswordDigest
    - Nonce
    - Created

    Tout ça du côté client ? Cela veut donc dire qu’il faut envoyer le password encrypté avec le salt au client ?

    Merci infiniment pour le temps que vous prenez à répondre !

    • Dans notre application sf2 le salt est public et disponible via l’API sur le user sans authentification.
      Coté client (android) nous demandons Username/Mot de passe à l’utilisateur.

      Nous récupérons coté serveur le salt pour le username et encryptons le mot de passe dès qu’il est saisi. C’est ce mot de passe encrypté que nous stockons coté client et que nous réutilisons pour générer les digest.

      De ce fait :
      - Seul le salt et les digest transitent sur le réseau
      - Seul le mot de passe encrypté est stocké coté client (impossible de retrouver le mot de passe, il est directement encrypté)

      Si ce n’est pas clair n’hésitez pas ;)

      François

      • Je pense vous avoir compris, mais pour être certain, voici la manière dont je vois la chose:

        L’utilisateur se connecte sur le site (on vérifie que le mot de passe est le nom d’utilisateur est ok), et ensuite on redirige sur l’application backbone avec le mot de passe encrypté (directement pris depuis la db) et on stock celui-ci côté client.

        Ensuite, à chaque requête exécuté vers l’api, on régénère le header wsse grâce au mot de passe stocké côté client.

        Vous ai-je compris ?

        • Je ne connais pas bien backbone, mais dans l’idée :

          L’utilisateur U arrive sur votre site, la première page de l’application backbone est un formulaire de login (user/mdp)

          -> U rentre son login/mdp
          -> l’application backbone récupère le salt correspondant à U en anonymous : une requête /api/user/U/infos par exemple qui retourne les infos non sensibles d’un user (dont le salt).
          -> backbone encrypte le mdp saisi et tente une requête qui necessite une authentification (/api/me) qui retourne les infos completes de l’utilisateur si connecté, une erreur sinon.
          -> Si aucune erreur n’est retournée, le mot de passe encrypté est stocké coté client dans votre application backbone
          -> Si une erreur est retournée, ses identifiants sont erronés, il recommence.

          Le mot de passe encrypté ne doit pas transiter sur le réseau. (Sinon n’importe qui pourrait récuperer salt + mdp encrypté et usurper l’identité de U).
          Ce système n’est pas parfait, surtout si vous stockez longuement le mot de passe encrypté coté client. Mais c’est un bon début.

          Si cela pose problème dans votre appli, préférez un cookie (meme si REST est stateless).

  5. Le salt peut être connu publiquement que si le mot de passe est encodé grâce à une clef privée, partagé par les applications essayant de s’authentifier au serveur ressource. C’est valable uniquement pour les applications dont le code source n’est pas connu du client comme du javascript coté client par exemple (là il faudra passer par oauth, ou créer un utilisateur wsse avec un rôle restrictif).

    Le salt est donc la clef publique, et les applications ont la clef privée (jamais transmise). Par défaut dans symfony2 la clef privée est représenté par les itérations de l’encoder. Le fait d’avoir accès à la base est donc insuffisant pour connaitre les mdp, le nombre d’itération étant inscrit dans le code source. Pour les API visant des plateformes mobiles, etc je n’utilise pas d’itération car c’est très gourmand ! Pour les pierres à feux utilisant encore shaXXX ou md5 ça reste rapide mais quand on le fait sur du pbkdf2 avec 10 itérations sur un i7 2.7ghz, on est proche de 5 secondes … Du coup je procède de la façon suivante :

    Pour faire il faut donc définir un custom encoder dans sf2 pour avoir quelque chose du genre :

    $password = mySuperHasingFunctionOverkill(‘secret123′.$salt.$raw.$salt);

    L’application connais également la clef privé ‘secret123′, en dur, elle ne fait pas de req vers quoi que se soit pour la récupérer. (car on pourrait être tenté de la centraliser, mais non)

    De cette manière, si la base est hacké, le salt n’est pas l’unique facteur de création du mdp et depuis la base il n’y aucun moyen de connaitre la clef privée et les rainbows table ne pourront rien faire sans cette clef privé. (car il existe des outils pour matcher si le salt est connu via différent pattern e.g : {salt}{raw} / {salt}{raw}/{salt} / etc …

    Le seul moyen de connaitre les mdp en base est donc d’avoir accès aux codes sources.

    Pour la création de clef privée j’ai pour habitude d’utiliser un UUID : D0222160-C80B-11E3-9C1A-0800200C9A66

    Faire transiter le mdp crypté sur le réseaux peut rester sensible, normalement on transport sous SSL (via https) la protection est normalement suffisante, mais pour ajouter encore un degré de protection dessus, il suffit de le décorer / parasiter / signer, appeler ça comme vous voulez :D

    L’application ayant fraichement encrypté le mdp, juste avant l’envoie elle va décorer le mdp en ajoutant un a tout les 5 caractères puis faire un reverse string. Au moment de la lecture il suffira de faire le processus inverse. faire le reverse puis de supprimer tout les ($cursorPos += 5) + 1 caractère qui sera note ‘a’ parasite. Ne décoré pas en hashant ! La décoration se veut réversible, chose que le hash c’est pas.

    Voilà donc la technique que j’utilise, peut être un peu overkill mais au moins je la trouve satisfaisante.

    Qu’en pensez vous ?

    PS: Sur les applications de symfony standard, je vous conseille de changer le comportement d’itération pour la créations des mdp ;) Le code étant open source, le nombre d’itération peut être trouvé en tâtonnant étant donnée que le pattern de création est visible. Cela est évidement extrêmement dur de le trouver en tâtonnant, je vous l’accorde, mais pas impossible.

    • Bonjour,
      Mieux faut être trop prudent qu’inconscient ;-)
      Votre technique semble en effet assez construite ! Et merci pour les petites précisions

      L’avantage du wsse est également qu’il empêche de rejouer une requête : le mdp est encrypté avec un nonce généré a partir d’une date. Conservé pendant certain un laps de temps, coté serveur nous verifions :
      - que ce nonce n’a pas été utilisé depuis par exemple 5minutes
      - que la date utilisée pour générer le nonce est bien dans les 5 dernières minutes.

      Impossible ainsi d’écouter le réseau et de réutiliser un token pour rejouer une requête!

  6. Hello ! Merci pour ce tuto, cela m’a vraiment appris des choses. Je n’ai pas encore tout réussi à faire fonctionner mais voyons si je suis sur le bon chemin : du coups maintenant pour mon Appli Android ou Web qui utilise l’API, je dois générer le WSSE moi-même. Vu que la comparaison ce fait avec le mot de passe hashé je dois l’encoder et le salé moi-même, mais comment le fait S2 ? sha1(mdp+salt) ?
    Sinon du coups on a une partie public de l’API, et une sécurisé. Comment on paramètre cela ? par exemple une route non secure pour récupérer des infos public et les salt “api/public” par exemple et l’autre sécurisé avec “api/secure” par exemple. A priori je dirais : dans security.yml :
    wsse_secured:
    pattern: ^/api/secure/*
    stateless: true
    wsse: true
    anonymous : false (<- ici si on met true, à quoi sert l'authentification ?)

    et rien pour api/public. Non ?
    Je vais continuer les test, au fait la REST Console ne permet pas d'indiquer un SALT ? on peut pas tester S2 avec ça ? ou on peux passer direct le mdp hashé ?
    Merci en tout cas =)

    • Pour voir comment ca se passait, nous sommes allé dans le code coté Sf2 ;)
      Vous pouvez vous aider de l’article de jb sur Andoid : http://obtao.com/blog/2014/01/comment-utiliser-wsse-dans-une-appli-android/

      A propos de l’anonymous sur Symfony2 : il permet simplement de dire si oui ou non un accès anonyme est possible. Vous pouvez quand même faire du contrôle de droits.

      En résumé
      - sans anonymous : le pare feu refuse l’accès à toute ressource pour un utilisateur non authentifié.
      - avec anonymous : ~ , le pare feu (authentification) laissera passer un utilisateur anonyme, mais vous pourrez restreindre ses accès via le contrôle d’accès (autorisation). La documentation de Symfony2 est vraiment très bien faite à ce sujet : http://symfony.com/fr/doc/current/book/security.html

      Dans notre cas nous laissons passer un utilisateur anonyme, mais nous restreignons son accès à l’API : il ne peut accéder qu’à l’api user info (voire même une méthode user/{username}/salt)

      François

  7. comment je peux utiliser wsse avec une appli titanium qui assure une authentif vers un serveur symfony2 , votre aide svp c urgent

    • Bonjour, l’implémentation de wsse est décrite dans le billet.
      Concernant le reste, je ne pourrai pas vous aider vu que je ne connais pas Titanium et que vous ne donnez aucune info.

  8. Du coups j’ai bien réussi à avoir une API public ( tout le monde peut récupérer le JSON sans rien faire ) et j’ai aussi une API privée qui requiert le wsse !
    Mais j’arrive à l’avoir avec la REST Console qui fait la requête ! Si par exemple je veux faire une requete Ajax avec JQuery ou AngularJS, comment doit on faire ? Il faut inclure un header comme le fait la Rest Console non ? Je suis entrain de lire les docs de $.ajax(jquery) et $http(angular) pour répondre à ça, je mettrais la solution ici si/quand j’aurais trouvé.

    Et autre question plus importante, ici avec RestBundle on récupère un objet et on renvoi l’objet, les champs @Expose permet d’avoir les bon champs, c’est super ça marche bien, c’est propre, ça fait du beau JSON ou XML, bref … Mais si je veux un truc un peu plus compliqué, et que je suis forcé de passer par une de mes requête SQL perso qui me renvoi un tableau ou un object pas forcément prévu comme il faut par le bundle, comment fait-on pour profiter de ce bundle ? Ma requête renvoi un tableau bien rempli visible avec var_dump par exemple mais après en ajax j’ai un tableau nul d’objet : “[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]“

    • Oui, il faut ajouter le wsse dans le header. Si cela s’avère impossible, il est toujours envisageable d’utiliser une valeur passée en get.

      Pour votre soucis de serialisation, vous pouvez créer un objet (model) non persisté sur lequel vous pourrez ajouter les annotations de JMSSerializer. A quoi ressemble votre var_dump ?

      • Bonne idée pour l’object non persisté je pense que c’est une bonne solution et assez propre. même si cela demande un peu plus d’effort. LE var_dump était “[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]“

  9. Très bon tuto! Pas évident pour un débutant mais j’ai réussi a mettre en place! Merci Francois! :)
    Maintenant, je voudrai créer un nouveau “user” a partir une app android (client) et en utilisant FOSUserBundle + FOSRestBundle(serveur). Avez-vous un example pour commencer du coté serveur?

    Cdlt,

  10. Re-bonjour à tous.
    Pour les besoins de mon application j’ai du faire une fonction javascript pour faire un header pour le token wsse. Pour cela je me suis inspiré du site que vous avez donné pour wssegen indiqué. Je suis allé voir dans le javascript comment ils ont fait. Mais une License est sous BSD et l’autre copyright avec écrit ” This library is free. You can redistribute it and/or modify it. ” Et celui de la page n’a rien du tout. Savez-vous si il est possible de l’utiliser du coups ? j’aurais aussi aimé le partager ici ou sur Github. Ou leur demander ?

  11. Bonjour tout le monde, merci du tuto il m’a beaucoup aidé j’étais perdu !

    Mais j’ai un soucis, j’arrive à me connecter dans le navigateur directement depuis le site sans problème. Mais quand j’utilise la console Rest de Chrome, ca me renvoi une erreur 500 “file_put_contents(…) failed to open stream : no such fil or directory… in WsseProvider line 62 !
    Je ne vois pas le soucis, j’imagine que le dossier qu’il cherche est introuvable.. Mais pourquoi ? Et comment le résoudre ?

    Si vous avez une idée, je suis preneur ! :)

    • Bonjour,

      Je pense que le dossier de cache des nonces n’existe pas. (app/cache/[dev|prod]/security/nonces).
      Par contre les lignes précédentes devraient le créer si il n’est pas présent.


      // If cache directory does not exist we create it
      if (!is_dir($this->cacheDir)) {
      mkdir($this->cacheDir, 0777, true);
      }

      Après avoir essayé de vous connecter, est ce que le dossier est créé/present dans votre dossier cache?

      François

      • Merci pour la réponse !

        En effet j’ai corriger le problème, il me manquait la partie qui créait le dossier s’il n’existait pas.

        J’ai une autre erreur si vous permettez, le digest du token et celui attendu ne correspondent pas. Ce qui bien sur ne fonctionne pas, j’ai pourtant mis les bons identifiants.

        Et je ne vois pas ce qui fait leur différence ?

  12. Bonjour.
    Merci pour le tuto :)

    J’ai suivi le post sur l’api REST et ce post sur wsse. Le premier a marché directement.
    En revanche pour celui-ci, j’ai une erreur que je ne comprends pas.

    Tout mon site est beugé avec cette exception :

    ContextErrorException: Notice: Undefined index: lifetime in C:\wamp\…\vendor\escapestudios\wsse-authentication-bundle\Escape\WSSEAuthenticationBundle\DependencyInjection\Security\Factory\Factory.php line 55

    J’ai alors décidé de mettre en place la partie “extra” de la doc symfony2 qui a résolu le problème apparement. Mais maintenant j’ai cette erreur relativement similaire :p

    ContextErrorException: Notice: Undefined index: date_format in C:\wamp\…\vendor\escapestudios\wsse-authentication-bundle\Escape\WSSEAuthenticationBundle\DependencyInjection\Security\Factory\Factory.php line 56

    Pourrez-vous m’éclairer ?

    Merci d’avance.

    • Bonjour, la première erreur était effectivement dû à une mauvaise conf. Concernant la deuxième, vous utilisez visiblement un bundle tiers pour la partie authentification. Il faut donc voir dans la documentation de celui-ci.

      • Ah mince, j’avais oublié ce bundle…
        A force de le voir, il était devenu familier.

        Merci bien, je l’ai simplement supprimé et l’erreur à disparu :)

  13. Bonjour :)
    J’ai mis en place une API Rest sur un premier serveur (S1). L’API recrache bien le JSON en fonction du header et du paramètre x-wsse.
    J’ai donc mis en place un “client” (également avec symfony2) sur un autre serveur (S2). Je fais appel à mon API pour récupérer des infos sur l’utilisateur.
    Tout ça marche parfaitement bien, en fonction des paramètres envoyés via “curl” de S2 à S1.

    Maintenant, je souhaite mettre en place un système de login sur mon serveur S2. Il n’y a pas de bdd pour S2, uniquement pour S1.
    Je ne suis pas sûr d’avoir bien compris comment ça marche.

    Le seul moyen pour recevoir les infos de l’utilisateur est bien d’envoyer ma requête curl (avec x-wsse dans le header) à chaque fois de S2 à S1 ?
    Pour l’instant j’ai fait un service qui va chercher les infos en fonction de l’url, mais j’ai l’impression de ne pas avoir bien compris, ça paraît bizarre tout ça :p

    Merci d’avance.

    • Si je reformule, en faite ma question est la suivante.

      Dans Symfony2, avec wsse authentication, comment puis-je récupérer le token d’un utilisateur et l’envoyer au client après une authentification réussie dans l’API.

      Et de même par la suite, comment renvoyer ce token à l’API.

      Merci d’avance. :p

      • Bonjour,

        Je ne suir pas sur de bien comprendre votre question. Si vous souhaitez que votre serveur S2 s’authentifie en tant que le user sur le serveur S1 il faut effectivement utiliser les header wsse entre vos serveurs.

        Pour ce genre de besoin, je vous conseille de regarder OAuth qui sera peut être plus approprié. (Mais je ne maitrise pas totalement le sujet).
        Il existe des bundles pour gérer ces connexions.

        François