Archives de catégorie : Web Application

10 astuces pour éviter des failles de sécurité

Pour plus de simplicité, j’utilise le terme application pour désigner les applications web et les sites web (par exemple, un site vitrine, etc.).

Filtrer, filtrer, filtrer

La première astuce consiste à filtrer côté serveur Web toutes les données reçues de l’utilisateur.

Si vous développez une librairie, les entrées correspondent à tous les arguments des méthodes publiques mise à disposition du développeur qui utilise votre librairie.
Il est également nécessaire de bloquer les contournements possibles de vos protections en prévoyant des points d’extension contrôler et en bloquant la modification du fonctionnement de votre code.

Si vous développez une application, les entrées utilisateurs sont toutes les données reçues via une requête HTTP, les arguments reçus lors de l’exécution d’une commande dans le terminal.

Pour filtrer, il existe différentes méthodes selon les cas.

  • Un texte libre où l’utilisateur est libre de saisir ce qu’il souhaite, un filtre avec une expression rationnelle est efficace, mais ne suffit pas. J’en reparlerai plus bas.
  • Une date
  • Un numéro de téléphone
  • Une couleur
  • Un entier ou nombre décimal
  • Une case à cocher
  • Une liste de choix

Contrôler les URL fournies

Cette astuce est d’un niveau supérieur, il existe une donnée qui est extrêmement difficile de contrôler, c’est les URL.

Si votre application demande à l’utilisateur une URL pour faire un rappel (callback) ou charger des données qui seront affichées plus tard (ex.: agrégateur RSS) vous avez un risque d’exposer vos API internes.

Il existe des services sur Internet permettant de tromper les vérifications DNS. Par exemple, le domaine 192.168.0.1.fake-dns.com passeront les filtres de validité de l’URL, mais lors de la résolution DNS enverra la requête sur le réseau interne.

Certains services proposent de résoudre le nom de domaine avec deux IP fourni l’une après l’autre… Ainsi lors de la première résolution, l’IP retourné est bien une IP externe, mais lors de la seconde résolution (cette fois pour envoyer la requête) l’IP retourné par le DNS est une IP interne.

Extrême prudence donc sur les URL fournies pas les utilisateurs.

Il existe selon les clients HTTP la possibilité de fournir la liste des plages IP interdite permettant de limiter les accès. Cependant, comme indiqué plus haut l’efficacité est liée à l’implémentation.

À mon sens réaliser une résolution DNS manuelle (pour filtrer) avant l’exécution de la requête est une bonne chose uniquement s’il est possible de fournir au client HTTP l’IP du serveur sur lequel se connecter indépendamment du hostname.

La validation navigatrice n’est pas suffisante

Avec l’évolution du développement coté navigateur, nombreux sont ceux qui font confiance au filtre mis en place avec JavaScript. Ces filtres sont une bonne chose, mais s’ils ne sont pas également exécutés sur le serveur qui reçoit les données cela ne vous protège de rien.

Voici un scoop, les malveillants n’utilisent pas de navigateur pour exfiltrer les données. Les filtres JavaScript ne sont donc pas exécutés et votre serveur est vulnérable !

Préparer vos requêtes SQL

Cette astuce consiste à protéger les éléments variables que vous utilisez dans vos requêtes SQL.

Pour toute requête, les données utilisées pour la valeur des colonnes des tables doivent être passées en argument avec une requête préparer. Je vous laisse lire la documentation de la librairie que vous utilisez pour accéder aux données.

Cette pratique est assez courante, mais il reste des éléments qu’il n’est pas possible de passer en argument d’une requête préparée.

Par exemple, le nom de la colonne à utiliser pour le tri, le sens du tri, les noms des colonnes à retourner, le nom de la table à interroger. Pour ces éléments, vous devez les filtrer (et oui encore) avec une liste blanche.

Par exemple, la valeur du sens de tri est égale à ASC ? Alors utilisation de la valeur ASC, sinon utilisation de la valeur DESC. Le nom de la colonne fourni pour le tri est-il dans la liste des colonnes autorisées pour le tri ? Si oui, alors la colonne est utilisée, sinon, la valeur par défaut est utilisée.

Échapper l’affichage

Cette astuce est extrêmement simple à mettre en place. Toute donnée affichée dans les templates (pour une réponse HTTP) doit être échappée.

C’est-à-dire que si le texte à afficher contient le caractère < il sera remplacé part &lt;. L’affichage est correct et s’il contient une balise HTML utilisée pour réaliser une injection XSS le code sera affiché au lieu d’être exécuté par le navigateur.

L’échappement évitera également l’hébergement de site peu recommandable sur votre site grâce à l’injection de code HTML dans votre page.

Gérer correctement les fichiers téléversés

La première astuce est de limiter les types de fichiers autorisés et la taille. La vérification de ces informations doit être réalisée sur le serveur sans tenir compte des informations fournies par l’utilisateur (ou son navigateur).

La seconde est de ne pas stocker le fichier avec le nom original sur votre serveur. Calculer une somme de contrôle unique ou générer des données aléatoires pour être sûr du nom du fichier.

La gestion des fichiers uploadés par les utilisateurs de votre application est différente en fonction de ces deux cas :

  • les fichiers publics
  • les fichiers privés

Pour les fichiers publics, le stockage du fichier sera réalisé en général dans un dossier public. Ainsi le fichier sera plus simple à télécharger. L’exécution des fichiers présents dans le dossier de dépôt doit être désactivée !

Pour les fichiers privés, le stockage du fichier sera réalisé dans un dossier privé (inaccessible de l’extérieur via le serveur web). Le téléchargement passera forcément par votre application pour vérifier les accès (j’y reviendrai plus bas). Il est en général nécessaire de stocker certaines informations en base de données (tel que le nom original du fichier, le type, la taille, le nom réel utilisé sur l’espace de stockage).

Si vos fichiers privés sont hébergés sur un service de stockage distant (ObjectStorage, S3), bien vérifier les droits d’accès à l’espace de stockage.

Stocker correctement les mots de passe

Cette astuce consiste à rendre illisibles les mots de passe de vos utilisateurs stockés en base de données. Illisible, mais également difficile à retrouver. C’est pourquoi les méthodes de hachage tel que MD5, SHA1 et suivante sont à proscrire.

À la place, utilisez une méthode de hachage qui contient un paramètre de temps de calcul.

Imaginons que le calcul du hasch d’un mot de passe prend sur votre serveur 5ms. Lors de la vérification du mot de passe, l’utilisateur ne le sentira pas. Par contre, si vous avez 50 000 mots de passe dont il faut calculer le mot de passe, le temps n’est plus le même.

Un malveillant sera capable de mettre beaucoup d’argent et de temps pour casser les mots de passe s’il est sûr que cela lui rapportera gros.

Pour terminer cette astuce, lors de la vérification du mot de passe, utilisez la méthode de comparaison fournie par la librairie. Il existe des failles basées sur le temps lors de la vérification du hasch.

Protéger les actions dangereuses

Cette astuce consiste à sécuriser les actions réalisées par les utilisateurs de votre application au travers d’un formulaire.

Lorsque votre utilisateur ajoute, modifie ou supprime une donnée, vous devez vous assurer que l’action vient bien de lui. Pour cela, lors de la génération de la page du formulaire, ajouter un champ caché contenant un code unique aléatoire (token CSRF) que vous stockez également dans la session de l’utilisateur côté serveur.

Lors de la soumission du formulaire par l’utilisateur, la valeur du champ caché doit être égale à la valeur stockée en session.

Cette astuce est plutôt répandue (grâce aux frameworks qui l’intègre) pour les formulaires d’ajout et modification, mais pas pour les actions dangereuses qui ne passent pas par un formulaire telle la suppression.

Dans le cas de la suppression ou de toute autre action (activation/désactivation par exemple) il est possible de transformer le lien en formulaire et ainsi de positionner le token.

Mais il est également possible lors de la génération du lien de générer un token qui sera passé en paramètre GET de la requête.

Pourquoi tout cela ?

Il est très simple pour un malveillant de générer une page web contenant une image dont l’URL est celle d’une action dangereuse de votre application. Lors du chargement de la page malveillante, votre navigateur réalisera une requête vers votre application pour charger l’image. Si vous êtes connecté à l’application, l’action sera réalisée sans que vous le sachiez.

Voici à quoi peut ressembler une image piégée. Cela fonctionne également avec les courriels si votre client charge les images par défaut.

<img src="https://votre_application.fr/user/10/delete">

Le code ne fonctionnera pas si un token est nécessaire (ex.: https://votre_application.fr/user/10/delete?token=2528425faf41) car la valeur du token n’est pas devinable !

Contrôler les accès

Cette astuce consiste à vérifier si l’utilisateur actuel à le droit de voir la page demandée. Dans le cas ou l’utilisateur n’est pas connecter, une erreur HTTP 401 sera renvoyé et dans le cas ou l’utilisateur est connecté, mais qu’il n’a pas les accréditations une erreur HTTP 403 sera renvoyé.

Vérifier l’accès à une page est une chose, mais parfois un utilisateur à l’accréditation pour accéder à une page, mais pas pour toutes les données.

Prenons le cas de l’affichage des données d’un utilisateur. L’URL est sous la forme https://monapplication.fr/user/<id>.

L’utilisateur courant peu visualiser la page de profils uniquement des utilisateurs qui sont dans le même groupe que lui. Il a donc l’accréditation pour accéder à l’URL.

Lors du traitement de la requête, le serveur doit vérifier que l’utilisateur courant est dans le même groupe que l’utilisateur qu’il souhaite afficher. S’il n’est pas dans le même groupe, une erreur HTTP 403 sera renvoyée.

Cette vérification est également à inclure dans les tests automatisés de votre application.

Protéger vos cookies

Cette astuce consiste à bloquer l’utilisation du cookie de session ou reconnexion automatique (remember me) par JavaScript et lors d’un clic sur un lien (email ou un autre site).

Lors de l’envoi d’un cookie au navigateur, il y a des drapeaux (flags) qui peuvent être activés. Activer le drapeau HttpOnly pour que JavaScript n’y accède pas et ainsi éviter le vol des cookies de session.
Activer le drapeau Secure pour que le navigateur émettre le cookie que pour les requêtes HTTPS.
Activer le drapeau SameSite=Strict si votre application n’utilise pas de système d’authentification tiers (SSO). Cette option expérimentale bloquera des attaques CSRF (voir « Protéger les actions dangereuses ».

N’utilisez pas les index auto-incrément

Cette astuce consiste à ne pas utiliser d’identifiant auto incrémental. Pourquoi ? Lors de l’accès aux données, il est fréquent de mettre l’identifiant dans l’URL.
Si l’accès à la donnée est protégé, il est facile de tenter une autre URL pour essayer d’accéder à des données auquel il n’est normalement pas possible d’accéder.

Voici l’exemple de l’URL d’accès aux informations d’un client de ma base client héberger sur un service SaaS https://monservice.fr/client/10.

Maintenant, je sais que l’ID de mon premier client est 10 et le dernier est 20. Qu’y a-t-il avant et après ? Il est très facile de le découvrir.

Si vous avez appliqué l’astuce « Contrôler les accès » le problème ne se posera pas.

Il ne restera qu’une possible estimation de la quantité de données disponibles dans la table.

Oui, il est encore possible de déterminer le dernier index utilisé, car une fois dépassé, l’erreur HTTP sera 404 (introuvable) à la place de 403 (accès refusé).

C’est ici que les identifiants non incrémentaux générés de façon sécurisée sont efficaces, car non prévisibles.

Protéger vos API

Toutes ces astuces sont applicables aux API HTTP avec une nuance pour la protection des actions par token temporaire (CSRF). Vos API doivent être protégés par un token ajouté dans les en-têtes des requêtes HTTP.

Ce token est soit récupéré après l’authentification de l’utilisateur. Sois généré une fois et restera valide tant qu’il est connu du serveur.

Grâce à ces tokens, il est possible de limiter les accès à vos API (nombre de requêtes possibles dans un laps de temps).

Conclusion

La sécurité des données et de l’information en général est l’affaire de tous. Nous pouvons tous faire quelque chose à notre niveau.

En tant que développeur, nous sommes en amont des futures fuites de données et nous devons faire notre possible pour les éviter, informer nos collègues et nos responsables sur la nécessiter de sécuriser les données. Avec de petites astuces, il est possible de réduire les risques !

Je n’ai pas parlé de langages de programmation dans ces astuces… Dites-moi dans les commentaires si vous êtes intéressé par une explication des failles pour PHP, mais également le format que vous préférez.

Nouvelle erreur ValueError dans PHP 8 (partie 3)

Dans la continuité du précédent article sur PHP 8, voici une modification qui aura une incidence sur notre code et son bon fonctionnement sur cette nouvelle version majeure de PHP.

Fournir un objet en argument à la place d’un entier provoque une exception TypeError, mais que se passe-t-il si le type est correct, mais que la valeur ne l’est pas ?
À partir de PHP 8 un grand nombre de méthodes lèveront une exception ValueError.

Cette modification aura plus de conséquences sur le code devant être compatible avec PHP 7 et PHP 8 et dont la valeur des arguments est fournie par l’utilisateur ou le développeur.

La solution la plus simple est de valider la donnée avant son usage. Par validation, je recommande la vérification du type de donnée, mais également de son contenu (regex, liste exhaustive des valeurs acceptées, nettoyeur HTML par exemple). La vérification aura également un avantage en termes de sécurité, mais j’en reparlerais sûrement dans un autre bloc post.

Concrètement, à quoi faut’il s’attendre dans PHP 8 ?

Voici comment réagit la fonction password_hash avec un argument invalide dans PHP 8 :

Interactive shell

php > echo phpversion();
8.0.0-dev
php > 
php > $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);

Warning: Uncaught ValueError: Invalid number of threads in php shell code:1
Stack trace:
#0 php shell code(1): password_hash('test', 'argon2i', Array)
#1 {main}
  thrown in php shell code on line 1
php >

Ici, je passe un nombre incorrect de fils d’exécution à utiliser pour le calcul de la somme de contrôle du mot de passe. PHP lève une exception spécifique de type ValueError.

Dans votre code, si vous réalisez des tests similaires à if (is_string($return) === false) {}, il sera nécessaire de capturer l’exception par une structure de code try catch pour gérer les cas d’erreur en PHP 8.

<?php
try {
    $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);
} catch (ValueError $e) {
    var_dump($e);
}

Donne la sortie suivante :

object(ValueError)#3 (7) {
  ["message":protected]=>
  string(25) "Invalid number of threads"
  ["string":"Error":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(14) "php shell code"
  ["line":protected]=>
  int(1)
  ["trace":"Error":private]=>
  array(1) {
    [0]=>
    array(4) {
      ["file"]=>
      string(14) "php shell code"
      ["line"]=>
      int(1)
      ["function"]=>
      string(13) "password_hash"
      ["args"]=>
      array(3) {
        [0]=>
        string(4) "test"
        [1]=>
        string(7) "argon2i"
        [2]=>
        array(1) {
          ["threads"]=>
          int(0)
        }
      }
    }
  }
  ["previous":"Error":private]=>
  NULL
}

Maintenant, comment rendre notre code compatible avec PHP 7 et 8 ?

Voici un exemple de code :

<?php
try {
    $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);

    // Test du retour de la fonction
    if (is_string($return) === false) {
        throw new ValueError('Error with password_hash function, see the PHP logs to more details.');
    }
} catch (ValueError $e) {
    var_dump($e);
    // Traitement de l'erreur
}

Pour PHP 7, il est nécessaire de définir la classe ValueError, car elle n’existe pas. Cependant, plutôt que définir cette classe dans tous vos projets, utilisez la librairie polyfill pour PHP 8.0 réalisée par Symfony.

La mise à jour du code est donc possible dès maintenant pour nos projets et nos librairies.

Après avoir aperçu la modification du comportement, voici la liste des méthodes PHP qui peuvent émettre une exception ValueError :

array_chunk
array_combine
array_fill
array_multisort
array_pad
array_rand
assert_options
base_convert
bcadd
bccomp
bcdiv
bcmod
bcmul
bcpow
bcpowmod
bcscale
bcsqrt
bcsub
bzopen
bzread
cal_days_in_month
cal_from_jd
cal_info
cal_to_jd
count
count_chars
deflate_add
deflate_init
dirname
dns_check_record
easter_date
enchant_broker_free
explode
extract
fgetcsv
fgets
file
file_get_contents
flock
fputcsv
fread
fscanf
ftruncate
func_get_arg
gettype
gmp_pow
gzcompress
gzdeflate
gzencode
gzinflate
gzread

gzuncompress
hash
hash_file
inflate_add
inflate_init
jdtojewish
jdtounix
json_decode
log
max
mb_check_encoding
mb_chr
mb_convert_case
mb_convert_encoding
mb_convert_kana
mb_convert_variables
mb_decode_numericentity
mb_detect_encoding
mb_detect_order
mb_encode_numericentity
mb_encoding_aliases
mb_internal_encoding
mb_language
mb_ord
mb_preferred_mime_name
mb_regex_encoding
mb_strcut
mb_strimwidth
mb_stripos
mb_stristr
mb_strlen
mb_strpos
mb_strrchr
mb_strrichr
mb_strripos
mb_strrpos
mb_str_split
mb_strstr
mb_strtolower
mb_strtoupper
mb_strwidth
mb_substitute_character

mb_substr
mb_substr_count
min
mt_rand
parse_url
password_hash
pow
print
printf
proc_open
putenv
range
ReflectionParameter
scandir
settype
socket_connect
socket_create_pair
socket_send
socket_sendto
socket_set_option
spl_autoload_register
sprintf
sscanf
stream_context_get_default
stream_filter_register
stream_select
stream_set_chunk_size
stripos
strncasecmp
strncmp
str_pad
strpbrk
strpos
strripos
strrpos
str_split
str_word_count
substr_compare
substr_count
test
trigger_error
unixtojd
unserialize
version_compare
vfprintf
wordwrap
zip_close

Cette liste n’est pas exhaustive. Elle a été réalisée sur la base des tests de PHP.

Conclusion

Autant que possible, préparons dès maintenant la migration vers PHP 8 en modifiant notre code de façon à faciliter la migration.

Les tests sur PHP 8.0 ont été réalisés avec l’image docker php8-jit de Keinos. Merci pour le partage.

Restez à l’écoute, je vais réaliser d’autre article sur les modifications apportées par PHP 8 ayant une incidence sur notre code.

Double authentification et Symfony 5.1

Dans un article précédent, j’utilisais Guard pour réaliser un mécanisme de double authentification (si vous ne voyez pas de quoi je parle, lisez ce blog post « Double authentification avec Symfony »). J’ai souhaité le mettre à jour pour utiliser la nouvelle fonctionnalité introduite dans Symfony 5.1.

Avertissement

La fonctionnalité dont je vais parler est expérimentale ! Elle est susceptible d’être modifiée sans garantie de rétrocompatibilité dans les prochaines versions de Symfony 5.2 et suivante.

Cette fonctionnalité redessine la façon de fonctionner des classes d’authentification en se basant sur les évents. Ainsi les nouvelles classes d’authentification qui ressemblent un peu à celle de Guard génèrent un passeport contenant toutes les informations nécessaires pour la validation de l’authentification.

Pour ce blog post, j’ai préféré vous présenter cette migration sous forme de questions et réponses.

Comment fonctionne la classe d’authentification ?

Premier point, une classe d’authentification doit (comme pour Guard) dire si oui ou non il supporte la requête en cours. Si oui, la méthode authenticate est appelée et devra retourner un objet qui implémente l’interface PassportInterface.

La méthode authenticate doit :

  • Charger l’utilisateur depuis le fournisseur d’utilisateur.
  • Collecter les informations d’authentification (depuis la requête en général).
  • Générer le passeport avec la liste des badges nécessaires.

Comment sont vérifiées les informations d’authentification ?

Lors de la construction du passeport, le premier argument est l’objet utilisateur qui implémente l’interface UserInterface de Symfony. Le second est un « badge » spécifique qui implémente l’interface CredentialsInterface. Cet objet contiendra les informations d’authentification saisie par l’utilisateur qui tente de se connecter.

<?php
namespace Symfony\Component\Security\Http\Authenticator\Passport;
//...
class Passport implements UserPassportInterface
{
//...
    /**
     * @param CredentialsInterface $credentials the credentials to check for this authentication, use
     *                                          SelfValidatingPassport if no credentials should be checked.
     * @param BadgeInterface[]     $badges
     */
    public function __construct(UserInterface $user, CredentialsInterface $credentials, array $badges = [])
    {
       //...
        }
    }
//...
}

La vérification en elle-même dépend d’un écouteur (Listener/Subscriber) qui prend en charge la classe utilisée en second argument du constructeur de la classe passeport.

Qu’est-ce qu’un badge ?

Un badge est un objet qui est ajouté au passeport et qui permet de déclencher des vérifications supplémentaires lors de l’authentification. Par exemple, si l’utilisateur est actif, validation d’un code Yubikey…

Un badge doit implémenter l’interface BadgeInterface qui réclame que la méthode isResolved soit implémentée. Si cette méthode retourne la valeur « faux », l’authentification sera bloquée et l’utilisateur sera redirigé vers la page d’authentification.

Comment sont résolus les badges ?

Pour chaque badge il est nécessaire d’écrire un écouteur de l’événement SymfonyCheckPassportEvent. Ce dernier contient le passeport généré par la méthode authenticate de la classe d’authentification.

L’écouteur doit :

  1. Vérifier la présence dans le passeport du badge qu’il gère.
  2. Réaliser les traitements liés au badge.
  3. Si les conditions sont réunies, marquer le badge comme résolu (pour que la méthode isResolved retourne « vrai ») pour autoriser l’authentification.

Quelles sont les modifications marquantes ?

J’ai surtout dû ajouter beaucoup de classe puis déplacer certaines parties de code présentes dans les classes d’authentification vers les écouteurs.

Quelles sont les modifications apportées aux classes d’authentification ?

La première modification est l’héritage des classes AppLoginAuthenticator et AppTwoFactorAuthenticator qui est passé de SymfonyComponentSecurityGuardAuthenticatorAbstractFormLoginAuthenticator à SymfonyComponentSecurityHttpAuthenticatorAbstractLoginFormAuthenticator.

En observant le nom complet de la classe, il est possible de constater que je suis passé d’une classe d’authentification Guard à une classe d’authentification HTTP.

En suite les deux méthodes getUser et getCredentials on fusionné dans authenticate. La méthode checkCredentials pour la classe d’authentification AppLoginAuthenticator a été simplement supprimée, car c’est l’écouteur fourni par Symfony qui prend le relais pour vérifier le mot de passe de l’utilisateur.

Comment l’utilisateur est redirigé vers la page de saisie du second facteur ?

J’ai ajouté une classe de badge TwoFactorBadge avec son écouteur TwoFactorBadgeSubscriber. Lors de la construction du passeport dans AppLoginAuthenticator, ce badge est ajouté.

<?php

namespace AppSecurity;

final class AppLoginAuthenticator extends AbstractLoginFormAuthenticator
{
    public function authenticate(Request $request): PassportInterface
    {
        $email = $request->get('email');
        $user = $this->userRepository->findOneBy(['email' => $email]);
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $email
        );
        $request->getSession()->set(
            AppTwoFactorAuthenticator::USER_SESSION_KEY,
            $email
        );
        if (!$user) {
            throw new UsernameNotFoundException();
        }

        return new Passport($user, new PasswordCredentials($request->get('password')), [
            // and CSRF protection using a "csrf_token" field
            new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),

            // and add support for upgrading the password hash
            new PasswordUpgradeBadge($request->get('password'), $this->userRepository),
            new TwoFactorBadge(),
        ]);
    }
}

Voir la classe AppLoginAuthenticator complète.

Cet écouteur a une particularité, il doit être exécuté après la vérification du mot de passe. J’ai donc dû modifier la priorité de l’écouteur à -10, car par défaut, les écouteurs sont tous exécutés avant la vérification du mot de passe.

//...
    public static function getSubscribedEvents()
    {
        return [
            CheckPassportEvent::class => ['resolveBadge', -10]
        ];
    }
//...

Maintenant, si le mot de passe est correct, l’écouteur ajoutera la clé need_auth_two avec la valeur « vrai » dans la session sans résoudre le badge.

<?php
namespace AppSecurityListener;
final class TwoFactorBadgeSubscriber implements EventSubscriberInterface
{
    public function resolveBadge(CheckPassportEvent $event)
    {
        /** @var Passport $passport */
        $passport = $event->getPassport();

        // Here I can check if the user has the two factor authentication enabled
        if ($passport->hasBadge(TwoFactorBadge::class) && $passport->hasBadge(PasswordCredentials::class) && $passport->getBadge(PasswordCredentials::class)->isResolved()) {
            $this->session->set('need_auth_two', true);
        }
    }
}

Voir la classe TwoFactorBadgeSubscriber complète.

Après le passage du passeport dans tous les écouteurs, Symfony vérifiera si tous les badges sont résolus. Comme ce ne sera pas le cas, il redirigera l’utilisateur vers la page de login. C’est là que la méthode getLoginUrl de classe d’authentification AppLoginAuthenticator génèrera une URL pour la saisie du code de second facteur d’authentification.

La méthode getLoginUrl n’a pas été modifiée !

<?php

namespace AppSecurity;

final class AppLoginAuthenticator extends AbstractLoginFormAuthenticator
{
//...
    protected function getLoginUrl(Request $request): string
    {
        if ($this->session->get('need_auth_two', false) === true) {
            return $this->urlGenerator->generate(AppTwoFactorAuthenticator::LOGIN_ROUTE);
        }

        return $this->urlGenerator->generate(self::LOGIN_ROUTE);
    }
//...
}

Comment est vérifié le second facteur ?

Comme avec Guard la page de saisie du code de second facteur est différente de celle de la connexion avec le mot de passe. Il y a donc deux classes d’authentification. J’ai également modifié la classe d’authentification AppTwoFactorAuthenticator.

<?php

namespace AppSecurity;

final class AppTwoFactorAuthenticator extends AbstractLoginFormAuthenticator
{
//...
    public function authenticate(Request $request): PassportInterface
    {
        $email = $request->getSession()->get(self::USER_SESSION_KEY);
        $user = $this->userRepository->findOneBy(['email' => $email]);
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $email
        );
        $request->getSession()->set(
            AppTwoFactorAuthenticator::USER_SESSION_KEY,
            $email
        );
        if (!$user) {
            throw new UsernameNotFoundException();
        }

        return new Passport($user, new TwoFactorCredentials($request->get('password')), [
            // and CSRF protection using a "csrf_token" field
            new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
            new TwoFactorTimeoutBadge(),
            new TwoFactorMaxAttemptBadge(),

        ]);
    }
}

Cette classe d’authentification appliquait les règles suivantes en plus de la vérification du code :

  • L’utilisateur n’a que 3 essais.
  • L’utilisateur à un temps limité pour saisir le bon code.

Pour chacune de ces règles, j’ai ajouté un badge spécifique au passeport que génère la classe d’authentification. Bien sûr, j’ai également écrit l’écouteur qui correspond et qui marque le badge comme résolu si les conditions sont remplies (TwoFactorMaxAttemptBadgeSubscriber et TwoFactorTimeoutBadgeSubscriber).

Pour la partie vérification du code en lui-même, j’ai ajouté une classe TwoFactorCredentials qui permettra la vérification. Cette classe sera donc utilisée en second argument du constructeur du passeport et contiendra le code saisi par l’utilisateur.

J’ai ajouté une nouvelle classe, car ce n’est pas un mot de passe en tant que tel et je ne souhaite pas que l’écouteur fourni par Symfony se déclenche.

En suite, j’ai écrit l’écouteur pour réaliser la vérification du code.

<?php
namespace AppSecurityListener;
final class TwoFactorCredentialSubscriber implements EventSubscriberInterface
{
//...
    public function checkCredential(CheckPassportEvent $event)
    {
        /** @var Passport $passport */
        $passport = $event->getPassport();
        if ($passport->hasBadge(TwoFactorCredentials::class) === false) {
            return;
        }
        $badge = $passport->getBadge(TwoFactorCredentials::class);
        if ($badge->getPassword() === $this->session->get(AppTwoFactorAuthenticator::CODE_SESSION_KEY, null)) {
            $badge->markResolved();
            return;
        }

        $this->session->set(AppTwoFactorAuthenticator::COUNT_SESSION_KEY,
            $this->session->get(AppTwoFactorAuthenticator::COUNT_SESSION_KEY, 0) + 1);

    }
}

Fichier complet src/Security/Listener/TwoFactorCredentialSubscriber.php

Quelles sont les modifications apportées dans la configuration ?

Étant donné que les classes d’authentification ne sont plus liées à Guard, il a été nécessaire de modifier la configuration.
Ainsi, les ID de service enregistré pour le firewall main dans la clé guard.authenticators ont été déplacés dans la clé custom_authenticators. Étant donné que j’ai deux classes d’authentification, il est obligatoire de désigner le point d’entrée avec la clé entry_point à la place de guard.entry_point.

#...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            provider: db
            entry_point: AppSecurityAppLoginAuthenticator
            custom_authenticators:
                - AppSecurityAppLoginAuthenticator
                - AppSecurityAppTwoFactorAuthenticator
            logout:
                path: app_logout
#...

Voir le fichier config/packages/security.yaml complet.

En conclusion, quels sont les avantages d’un tel système ?

Le premier avantage que je constate est la possibilité de réutiliser un badge et son écouteur sur plusieurs firewalls de l’application.

Le second est le fort découpage des responsabilités, ainsi qu’une grande facilité d’extension du système.

Enfin le dernier avantage est de pouvoir proposer plus facilement des bundles Symfony permettant d’étendre et mutualiser les badges et leur écouteur.

Et vous,

Dites-moi dans les commentaires quels sont les avantages que vous voyez à cette modification du composant Security de Symfony.

Win32Service for PHP8

The version 8 of PHP is scheduled for December 2020. All active extension must be upgrading their code to need a run with the new version.

Actually in development, the version 1.0 of the extension Win32Service will be compliant only with PHP 8. The version 1.0.0-dev is already upgraded and usable for tests. You can download the snapshot build of PHP for Windows (on the bottom of the page) and the Win32Service extension DLL on the GitHub repository.

Currently, version 1.0 of the Win32Service extension is building only for the x64 architecture and for two PHP mode (NTS and TS).

Maintained version

For the future, Win32Service version 0.4 will be maintained for all PHP 7 maintained versions. And Win32Service version 1.0 will be maintained for all PHP 8 maintained versions.

Win32Service pour PHP 8

La version 8 de PHP est prévue pour décembre 2020. Toutes les extensions actives doivent mettre à jour leur code pour avoir besoin d’une exécution avec la nouvelle version.

La version 1.0 de l’extension Win32Service est en cours de développement et ne sera compatible qu’avec PHP 8. La version 1.0.0-dev est déjà mise à jour et utilisable pour les tests. Vous pouvez télécharger la version snapshot de PHP pour Windows (en bas de la page) et la DLL d’extension Win32Service sur le dépôt GitHub.

Actuellement, la version 1.0 de l’extension Win32Service est compilée uniquement pour l’architecture x64 et pour deux modes PHP (NTS et TS).

Versions maintenues

La version 0.4 sera maintenue pour les versions maintenues de PHP 7.
La version 1.0 sera maintenue pour les versions maintenues de PHP 8.

[Updated] DDD with Broadway and the Design Pattern State

Note : This article is the translate of this article. I use Google Translate for help me to write in english. Please, if you read wrong phrase send me the correct by one comment.

In a refactoring sprint, I found that my main aggregate class took much overweight.

I had exceeded 750 lines of code with, in many actions, a « switch » or a dozen « IF ». This did not please me very much because, if a change was required with this level, the amendment would be difficult.

Continuer la lecture de [Updated] DDD with Broadway and the Design Pattern State

[MàJ] DDD avec Broadway et le Design pattern State

[English version]

Au cours d’un petit sprint de refactorisation, j’ai constaté que la classe de mon principal agrégat prenait beaucoup d’embonpoint.

J’avais dépassé les 750 lignes de code avec, dans beaucoup d’actions, soit un “switch » soit une petite dizaine de “IF”. Cela ne me plaisait pas beaucoup car, si une modification était demandée à ce niveau-là, la modification serait délicate.

Continuer la lecture de [MàJ] DDD avec Broadway et le Design pattern State

[Updated] Symfony, Broadway and the replay event

Note : This article is the translate of this article. I use Google Translate for help me to write in english. Please, if you read wrong phrase send me the correct by one comment.

On of the main advantages of EventSourcing is the replay event for build a new view database or sync the an old view.

Do have you ever wondered how to go about not send emails on events issued by the aggregates ?

In my case, the dilemma is rather important because many things are based on events. If they are replayed, all treatments are rerun.

In my project, I use Broadway (by QandidateLabs) for implementing the CQRS/ES.

For use the default event bus, you must tag any services with `broadway.domain.event_listener` and extend this class `Broadway\Processor\Processor`.
All services will be injected into the event bus by a compiler pass. The latter takes 3 arguments: the event bus service identifier, the tag used for get all services to inject her, and the interface name that services must implement to be injected.

You can reuse the code to add a new event bus without write more code.

3 steps for separate into new bus all services necessary for refresh view.

1) Add a new event bus

# src/Vendor/Bundle/NameBundle/Ressources/config/services.yml
service:
     mon_bundle.event_handling.event_bus:
          class: Broadway\EventHandling\SimpleEventBus
          lazy: true

2) Add new tag for all services to update the view. By exemple `mon_bundle.domain.event_listener`.
3) Run new compiler pass with new parameters

// src/Vendor/Bundle/NameBundle/NameBundle.php
use Broadway\Bundle\BroadwayBundle\DependencyInjection\RegisterBusSubscribersCompilerPass;
$container->addCompilerPass(
     new RegisterBusSubscribersCompilerPass(
          'mon_bundle.event_handling.event_bus',
          'mon_bundle.domain.event_listener',
          'Broadway\EventHandling\EventListenerInterface'
     )
);

This is a SOLID code.

Your event listener must also be. One listener one responsibility.

Finally, during the retransmission of events, simply send them on the bus dedicated to replay events to perform the update only the view.
That’s done! You can retransmit all the events to update the view without having to change the configuration of the production application to prevent the resend of the emails.

Get the source code on github.

Now you can share your experiences in the comments.

[MàJ] Symfony, Broadway et le replay d’event

[English version]

L’un des principaux atouts qui reviennent souvent lorsque l’on parle de l’EventSourcing est la réémission des évènements afin de reconstruire la vue (par exemple). Cela peut être pour la construction d’une nouvelle base de vue ou le rafraichissement de la vue désynchronisée.

Ne vous est-il jamais arrivé de vous demander comment s’y prendre pour ne pas renvoyer les emails basés sur les évènements émis par les agrégats ?

Continuer la lecture de [MàJ] Symfony, Broadway et le replay d’event

[POC] Oauth

Vous avez tous vu, au moins une fois, sur un site Internet le bouton de “connexion Facebook” ou “connexion Google ».

Mais qu’y a-t-il derrière ? Dans la grande majorité des cas, il y a le protocole Oauth en version 1.0a ou en version 2.

Je ne vais pas faire d’explication de texte sur le protocole car d’autres s’en sont chargés pour moi et je les en remercie.

Par contre, je vais vous donner un lien vers deux dépôts GitHub qui contiennent les parties utiles pour la mise en oeuvre du protocole avec Symfony2.

La partie serveur Oauth (facebook/google/etc…) et serveur de ressource : https://github.com/macintoshplus/OauthServerApp

La partie cliente (le site Internet) : https://github.com/macintoshplus/OauthClientApp

Pour la partie cliente, il est possible de faire le même travail (connexion de l’utilisateur et récupération de ses informations) de deux manières différentes :

1) en obtenant du serveur de ressource les informations sur l’utilisateur et ne rien stocker en local. Pour cela j’utilise le package « hwi/oauth-bundle ». Il sera nécessaire d’interroger le serveur de ressource pour rafraîchir les données de l’utilisateur connecté.

2) en obtenant du serveur de ressource les informations sur l’utilisateur et les sauvegarder localement. Les informations peuvent donc être utilisées pour relier l’utilisateur à ses données sur votre site. Pour stocker localement les données j’ajoute à « hwi/oauth-bundle » le très populaire « friendsofsymfony/user-bundle ».

Quel est votre retour d’expérience Oauth ?