@@ -582,10 +582,288 @@ static function ($This, $scope, $args) {
582582 }
583583 );
584584
585+ // Track login by hooking the storage write operation
586+ // This captures the full user object after it's stored
587+ hook_method (
588+ 'Laminas\Authentication\Storage\Session ' ,
589+ 'write ' ,
590+ null ,
591+ static function ($ This , $ scope , $ args , $ returnValue ) {
592+ if (!function_exists ('\datadog\appsec\track_user_login_success_event_automated ' )) {
593+ return ;
594+ }
595+
596+ // The first argument to write() is the identity
597+ $ identity = isset ($ args [0 ]) ? $ args [0 ] : null ;
598+ if (!$ identity ) {
599+ return ;
600+ }
601+
602+ // Skip if identity is just a string (initial write from authenticate())
603+ // We only want to track when the full user object is written by the controller
604+ if (is_string ($ identity )) {
605+ return ;
606+ }
607+
608+ // Only track if this looks like a user object (has id property)
609+ $ userId = self ::getUserId ($ identity );
610+ if (!$ userId ) {
611+ return ;
612+ }
613+
614+ $ userLogin = self ::getUserLogin ($ identity );
615+ $ metadata = self ::getUserMetadata ($ identity );
616+
617+ \datadog \appsec \track_user_login_success_event_automated (
618+ $ userLogin ,
619+ $ userId ,
620+ $ metadata
621+ );
622+ }
623+ );
624+
625+ // Authentication tracking - Login failure
626+ install_hook (
627+ 'Laminas\Authentication\AuthenticationService::authenticate ' ,
628+ null ,
629+ static function (HookData $ hook ) {
630+ $ result = $ hook ->returned ;
631+
632+ if (!$ result instanceof \Laminas \Authentication \Result) {
633+ return ;
634+ }
635+
636+ $ code = $ result ->getCode ();
637+
638+ // Only track failures
639+ if ($ code === \Laminas \Authentication \Result::SUCCESS ) {
640+ return ;
641+ }
642+
643+ // Login failure
644+ if (!function_exists ('\datadog\appsec\track_user_login_failure_event_automated ' )) {
645+ return ;
646+ }
647+
648+ // Get the adapter from the hook arguments
649+ $ adapter = isset ($ hook ->args [0 ]) ? $ hook ->args [0 ] : null ;
650+ $ userLogin = null ;
651+
652+ // Try to get the login from the adapter if it has a getIdentity method
653+ if ($ adapter && method_exists ($ adapter , 'getIdentity ' )) {
654+ $ userLogin = $ adapter ->getIdentity ();
655+ }
656+
657+ $ userExists = ($ code === \Laminas \Authentication \Result::FAILURE_CREDENTIAL_INVALID );
658+
659+ \datadog \appsec \track_user_login_failure_event_automated (
660+ $ userLogin ,
661+ $ userLogin ,
662+ $ userExists ,
663+ []
664+ );
665+ }
666+ );
667+
668+ // Track authenticated user on each request
669+ hook_method (
670+ 'Laminas\Authentication\AuthenticationService ' ,
671+ 'hasIdentity ' ,
672+ null ,
673+ static function ($ This , $ scope , $ args , $ hasIdentity ) {
674+ if (!$ hasIdentity || !function_exists ('\datadog\appsec\track_authenticated_user_event_automated ' )) {
675+ return ;
676+ }
677+
678+ $ identity = $ This ->getIdentity ();
679+ if (!$ identity ) {
680+ return ;
681+ }
682+
683+ $ userId = self ::getUserId ($ identity );
684+ \datadog \appsec \track_authenticated_user_event_automated ($ userId );
685+ }
686+ );
585687
586688 return Integration::LOADED ;
587689 }
588690
691+ /**
692+ * Extract user ID from identity object
693+ *
694+ * @param mixed $identity
695+ * @return string
696+ */
697+ private static function getUserId ($ identity )
698+ {
699+ if (is_string ($ identity ) || is_int ($ identity )) {
700+ return (string )$ identity ;
701+ }
702+
703+ if (is_array ($ identity )) {
704+ if (isset ($ identity ['id ' ])) {
705+ return (string )$ identity ['id ' ];
706+ }
707+ if (isset ($ identity ['user_id ' ])) {
708+ return (string )$ identity ['user_id ' ];
709+ }
710+ if (isset ($ identity ['username ' ])) {
711+ return $ identity ['username ' ];
712+ }
713+ if (isset ($ identity ['email ' ])) {
714+ return $ identity ['email ' ];
715+ }
716+ }
717+
718+ if (is_object ($ identity )) {
719+ // Try common property names
720+ if (isset ($ identity ->id )) {
721+ return (string )$ identity ->id ;
722+ }
723+ if (isset ($ identity ->user_id )) {
724+ return (string )$ identity ->user_id ;
725+ }
726+ if (isset ($ identity ->userId )) {
727+ return (string )$ identity ->userId ;
728+ }
729+
730+ // Try common getter methods
731+ if (method_exists ($ identity , 'getId ' )) {
732+ return (string )$ identity ->getId ();
733+ }
734+ if (method_exists ($ identity , 'getUserId ' )) {
735+ return (string )$ identity ->getUserId ();
736+ }
737+ if (method_exists ($ identity , 'getUsername ' )) {
738+ return $ identity ->getUsername ();
739+ }
740+ if (method_exists ($ identity , 'getEmail ' )) {
741+ return $ identity ->getEmail ();
742+ }
743+
744+ // ArrayAccess support
745+ if ($ identity instanceof \ArrayAccess) {
746+ if (isset ($ identity ['id ' ])) {
747+ return (string )$ identity ['id ' ];
748+ }
749+ if (isset ($ identity ['user_id ' ])) {
750+ return (string )$ identity ['user_id ' ];
751+ }
752+ if (isset ($ identity ['username ' ])) {
753+ return $ identity ['username ' ];
754+ }
755+ if (isset ($ identity ['email ' ])) {
756+ return $ identity ['email ' ];
757+ }
758+ }
759+ }
760+
761+ return '' ;
762+ }
763+
764+ /**
765+ * Extract user login (username/email) from identity object
766+ *
767+ * @param mixed $identity
768+ * @return string|null
769+ */
770+ private static function getUserLogin ($ identity )
771+ {
772+ if (is_string ($ identity )) {
773+ return $ identity ;
774+ }
775+
776+ if (is_array ($ identity )) {
777+ if (isset ($ identity ['email ' ])) {
778+ return $ identity ['email ' ];
779+ }
780+ if (isset ($ identity ['username ' ])) {
781+ return $ identity ['username ' ];
782+ }
783+ }
784+
785+ if (is_object ($ identity )) {
786+ // Try properties
787+ if (isset ($ identity ->email )) {
788+ return $ identity ->email ;
789+ }
790+ if (isset ($ identity ->username )) {
791+ return $ identity ->username ;
792+ }
793+
794+ // Try getters
795+ if (method_exists ($ identity , 'getEmail ' )) {
796+ return $ identity ->getEmail ();
797+ }
798+ if (method_exists ($ identity , 'getUsername ' )) {
799+ return $ identity ->getUsername ();
800+ }
801+
802+ // ArrayAccess support
803+ if ($ identity instanceof \ArrayAccess) {
804+ if (isset ($ identity ['email ' ])) {
805+ return $ identity ['email ' ];
806+ }
807+ if (isset ($ identity ['username ' ])) {
808+ return $ identity ['username ' ];
809+ }
810+ }
811+ }
812+
813+ return null ;
814+ }
815+
816+ /**
817+ * Extract user metadata from identity object
818+ *
819+ * @param mixed $identity
820+ * @return array
821+ */
822+ private static function getUserMetadata ($ identity )
823+ {
824+ $ metadata = [];
825+
826+ if (is_array ($ identity )) {
827+ if (isset ($ identity ['name ' ])) {
828+ $ metadata ['name ' ] = $ identity ['name ' ];
829+ }
830+ if (isset ($ identity ['email ' ])) {
831+ $ metadata ['email ' ] = $ identity ['email ' ];
832+ }
833+ return $ metadata ;
834+ }
835+
836+ if (is_object ($ identity )) {
837+ // Try properties
838+ if (isset ($ identity ->name )) {
839+ $ metadata ['name ' ] = $ identity ->name ;
840+ }
841+ if (isset ($ identity ->email )) {
842+ $ metadata ['email ' ] = $ identity ->email ;
843+ }
844+
845+ // Try getters
846+ if (method_exists ($ identity , 'getName ' )) {
847+ $ metadata ['name ' ] = $ identity ->getName ();
848+ }
849+ if (method_exists ($ identity , 'getEmail ' ) && !isset ($ metadata ['email ' ])) {
850+ $ metadata ['email ' ] = $ identity ->getEmail ();
851+ }
852+
853+ // ArrayAccess support
854+ if ($ identity instanceof \ArrayAccess) {
855+ if (isset ($ identity ['name ' ]) && !isset ($ metadata ['name ' ])) {
856+ $ metadata ['name ' ] = $ identity ['name ' ];
857+ }
858+ if (isset ($ identity ['email ' ]) && !isset ($ metadata ['email ' ])) {
859+ $ metadata ['email ' ] = $ identity ['email ' ];
860+ }
861+ }
862+ }
863+
864+ return $ metadata ;
865+ }
866+
589867 public static function debugBacktraceToString (array $ backtrace )
590868 {
591869 // (methods) #<frame index> <file>(line): <class><type><function>()\n
0 commit comments