Archives de catégorie : Web Application

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 ?