@@ -479,60 +479,6 @@ public function revoke_all_refresh_tokens( int $user_id ): bool {
479479 return delete_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' );
480480 }
481481
482- /**
483- * Store refresh token JTI for tracking/revocation.
484- *
485- * @param int $user_id
486- * @param string $jti
487- * @param int $expires
488- */
489- private function store_refresh_token_jti ( int $ user_id , string $ jti , int $ expires ): void {
490- $ refresh_tokens = get_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , true );
491- if ( ! \is_array ( $ refresh_tokens ) ) {
492- $ refresh_tokens = array ();
493- }
494-
495- // Clean up expired tokens
496- $ refresh_tokens = array_filter ( $ refresh_tokens , function ( $ token ) {
497- return $ token ['expires ' ] > time ();
498- });
499-
500- // Capture session metadata
501- $ current_time = time ();
502- $ ip_address = $ this ->get_client_ip ();
503- $ user_agent = isset ( $ _SERVER ['HTTP_USER_AGENT ' ] ) ? sanitize_text_field ( wp_unslash ( $ _SERVER ['HTTP_USER_AGENT ' ] ) ) : '' ;
504- $ device_info = $ this ->parse_user_agent ( $ user_agent );
505-
506- // Add new token with metadata
507- $ refresh_tokens [ $ jti ] = array (
508- 'expires ' => $ expires ,
509- 'created ' => $ current_time ,
510- 'last_active ' => $ current_time ,
511- 'ip_address ' => $ ip_address ,
512- 'user_agent ' => $ user_agent ,
513- 'device_info ' => $ device_info ,
514- );
515-
516- update_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , $ refresh_tokens );
517- }
518-
519- /**
520- * Check if refresh token is still valid (not revoked).
521- *
522- * @param int $user_id
523- * @param string $jti
524- *
525- * @return bool
526- */
527- private function is_refresh_token_valid ( int $ user_id , string $ jti ): bool {
528- $ refresh_tokens = get_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , true );
529- if ( ! \is_array ( $ refresh_tokens ) ) {
530- return false ;
531- }
532-
533- return isset ( $ refresh_tokens [ $ jti ] ) && $ refresh_tokens [ $ jti ]['expires ' ] > time ();
534- }
535-
536482 /**
537483 * Get all active sessions for a user.
538484 *
@@ -557,11 +503,11 @@ public function get_user_sessions( int $user_id ): array {
557503
558504 $ sessions [] = array (
559505 'jti ' => $ jti ,
560- 'created ' => $ token_data ['created ' ] ?? $ current_time ,
506+ 'created ' => $ token_data ['created ' ] ?? $ current_time ,
561507 'last_active ' => $ token_data ['last_active ' ] ?? $ token_data ['created ' ] ?? $ current_time ,
562508 'expires ' => $ token_data ['expires ' ],
563- 'ip_address ' => $ token_data ['ip_address ' ] ?? '' ,
564- 'user_agent ' => $ token_data ['user_agent ' ] ?? '' ,
509+ 'ip_address ' => $ token_data ['ip_address ' ] ?? '' ,
510+ 'user_agent ' => $ token_data ['user_agent ' ] ?? '' ,
565511 'device_info ' => $ token_data ['device_info ' ] ?? array (),
566512 );
567513 }
@@ -655,6 +601,103 @@ public function can_manage_user_sessions( int $target_user_id ): bool {
655601 return false ;
656602 }
657603
604+ /**
605+ * Blacklist an access token by JTI (for instant revocation).
606+ *
607+ * @param string $jti Access token JTI.
608+ * @param int $ttl Time to live in seconds (until token expires).
609+ *
610+ * @return bool
611+ */
612+ public function blacklist_access_token ( string $ jti , int $ ttl ): bool {
613+ if ( empty ( $ jti ) ) {
614+ return false ;
615+ }
616+
617+ // Use transient with TTL matching token expiration
618+ return set_transient ( "wcpos_blacklist_ {$ jti }" , true , $ ttl );
619+ }
620+
621+ /**
622+ * Revoke session and blacklist current access token.
623+ *
624+ * @param int $user_id
625+ * @param string $refresh_jti Refresh token JTI.
626+ * @param string $access_jti Optional access token JTI to blacklist immediately.
627+ *
628+ * @return bool
629+ */
630+ public function revoke_session_with_blacklist ( int $ user_id , string $ refresh_jti , string $ access_jti = '' ): bool {
631+ // Revoke the refresh token (session)
632+ $ revoked = $ this ->revoke_session ( $ user_id , $ refresh_jti );
633+
634+ // Blacklist the current access token if provided
635+ if ( $ revoked && ! empty ( $ access_jti ) ) {
636+ // Calculate TTL - access tokens expire in 30 minutes by default
637+ $ issued_at = time ();
638+ $ expire = apply_filters ( 'woocommerce_pos_jwt_access_token_expire ' , $ issued_at + ( HOUR_IN_SECONDS / 2 ), $ issued_at );
639+ $ ttl = max ( 0 , $ expire - $ issued_at );
640+
641+ $ this ->blacklist_access_token ( $ access_jti , $ ttl );
642+ }
643+
644+ return $ revoked ;
645+ }
646+
647+ /**
648+ * Store refresh token JTI for tracking/revocation.
649+ *
650+ * @param int $user_id
651+ * @param string $jti
652+ * @param int $expires
653+ */
654+ private function store_refresh_token_jti ( int $ user_id , string $ jti , int $ expires ): void {
655+ $ refresh_tokens = get_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , true );
656+ if ( ! \is_array ( $ refresh_tokens ) ) {
657+ $ refresh_tokens = array ();
658+ }
659+
660+ // Clean up expired tokens
661+ $ refresh_tokens = array_filter ( $ refresh_tokens , function ( $ token ) {
662+ return $ token ['expires ' ] > time ();
663+ });
664+
665+ // Capture session metadata
666+ $ current_time = time ();
667+ $ ip_address = $ this ->get_client_ip ();
668+ $ user_agent = isset ( $ _SERVER ['HTTP_USER_AGENT ' ] ) ? sanitize_text_field ( wp_unslash ( $ _SERVER ['HTTP_USER_AGENT ' ] ) ) : '' ;
669+ $ device_info = $ this ->parse_user_agent ( $ user_agent );
670+
671+ // Add new token with metadata
672+ $ refresh_tokens [ $ jti ] = array (
673+ 'expires ' => $ expires ,
674+ 'created ' => $ current_time ,
675+ 'last_active ' => $ current_time ,
676+ 'ip_address ' => $ ip_address ,
677+ 'user_agent ' => $ user_agent ,
678+ 'device_info ' => $ device_info ,
679+ );
680+
681+ update_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , $ refresh_tokens );
682+ }
683+
684+ /**
685+ * Check if refresh token is still valid (not revoked).
686+ *
687+ * @param int $user_id
688+ * @param string $jti
689+ *
690+ * @return bool
691+ */
692+ private function is_refresh_token_valid ( int $ user_id , string $ jti ): bool {
693+ $ refresh_tokens = get_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , true );
694+ if ( ! \is_array ( $ refresh_tokens ) ) {
695+ return false ;
696+ }
697+
698+ return isset ( $ refresh_tokens [ $ jti ] ) && $ refresh_tokens [ $ jti ]['expires ' ] > time ();
699+ }
700+
658701 /**
659702 * Get client IP address.
660703 *
@@ -675,10 +718,11 @@ private function get_client_ip(): string {
675718 if ( ! empty ( $ _SERVER [ $ header ] ) ) {
676719 $ ip_address = sanitize_text_field ( wp_unslash ( $ _SERVER [ $ header ] ) );
677720 // Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs)
678- if ( strpos ( $ ip_address , ', ' ) !== false ) {
721+ if ( false !== strpos ( $ ip_address , ', ' ) ) {
679722 $ ip_parts = explode ( ', ' , $ ip_address );
680723 $ ip_address = trim ( $ ip_parts [0 ] );
681724 }
725+
682726 break ;
683727 }
684728 }
@@ -712,28 +756,35 @@ private function parse_user_agent( string $user_agent ): array {
712756 }
713757
714758 // Detect WooCommerce POS apps first (custom identifiers)
715- // Your apps should add identifiers like: "WCPOS-iOS/1.0.0" or "WooCommercePOS-iOS/1.0.0"
716- if ( preg_match ( '/WCPOS[-_]?iOS|WooCommercePOS[-_]?iOS/i ' , $ user_agent ) ) {
759+ // Check for Electron app (including just "WooCommercePOS" in user agent with Electron)
760+ if ( preg_match ( '/Electron/i ' , $ user_agent ) && preg_match ( '/WooCommercePOS|WCPOS/i ' , $ user_agent ) ) {
761+ $ device_info ['app_type ' ] = 'electron_app ' ;
762+ $ device_info ['browser ' ] = 'WooCommerce POS ' ;
763+ $ device_info ['device_type ' ] = 'desktop ' ;
764+ // Try to extract WooCommercePOS version
765+ if ( preg_match ( '/WooCommercePOS[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
766+ $ device_info ['browser_version ' ] = $ matches [1 ];
767+ } elseif ( preg_match ( '/WCPOS[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
768+ $ device_info ['browser_version ' ] = $ matches [1 ];
769+ }
770+ } elseif ( preg_match ( '/WCPOS[-_]?iOS|WooCommercePOS[-_]?iOS/i ' , $ user_agent ) ) {
717771 $ device_info ['app_type ' ] = 'ios_app ' ;
718772 $ device_info ['browser ' ] = 'WooCommerce POS ' ;
719- $ device_info ['device_type ' ] = preg_match ( '/ipad/i ' , $ user_agent ) ? 'tablet ' : 'mobile ' ;
773+ // Default to tablet unless explicitly detected as phone
774+ $ device_info ['device_type ' ] = preg_match ( '/iphone|ipod/i ' , $ user_agent ) ? 'mobile ' : 'tablet ' ;
720775 if ( preg_match ( '/WCPOS[-_]?iOS[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
721776 $ device_info ['browser_version ' ] = $ matches [1 ];
777+ } elseif ( preg_match ( '/WooCommercePOS[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
778+ $ device_info ['browser_version ' ] = $ matches [1 ];
722779 }
723780 } elseif ( preg_match ( '/WCPOS[-_]?Android|WooCommercePOS[-_]?Android/i ' , $ user_agent ) ) {
724781 $ device_info ['app_type ' ] = 'android_app ' ;
725782 $ device_info ['browser ' ] = 'WooCommerce POS ' ;
726- $ device_info ['device_type ' ] = preg_match ( '/tablet/i ' , $ user_agent ) ? 'tablet ' : 'mobile ' ;
783+ // Default to tablet unless explicitly detected as mobile
784+ $ device_info ['device_type ' ] = preg_match ( '/mobile/i ' , $ user_agent ) && ! preg_match ( '/tablet/i ' , $ user_agent ) ? 'mobile ' : 'tablet ' ;
727785 if ( preg_match ( '/WCPOS[-_]?Android[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
728786 $ device_info ['browser_version ' ] = $ matches [1 ];
729- }
730- } elseif ( preg_match ( '/WCPOS[-_]?Electron|WooCommercePOS[-_]?Electron|Electron.*WCPOS/i ' , $ user_agent ) ) {
731- $ device_info ['app_type ' ] = 'electron_app ' ;
732- $ device_info ['browser ' ] = 'WooCommerce POS ' ;
733- $ device_info ['device_type ' ] = 'desktop ' ;
734- if ( preg_match ( '/WCPOS[-_]?Electron[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
735- $ device_info ['browser_version ' ] = $ matches [1 ];
736- } elseif ( preg_match ( '/Electron\/([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
787+ } elseif ( preg_match ( '/WooCommercePOS[\/\s]([0-9.]+)/i ' , $ user_agent , $ matches ) ) {
737788 $ device_info ['browser_version ' ] = $ matches [1 ];
738789 }
739790 }
@@ -798,23 +849,6 @@ private function parse_user_agent( string $user_agent ): array {
798849 return $ device_info ;
799850 }
800851
801- /**
802- * Blacklist an access token by JTI (for instant revocation).
803- *
804- * @param string $jti Access token JTI.
805- * @param int $ttl Time to live in seconds (until token expires).
806- *
807- * @return bool
808- */
809- public function blacklist_access_token ( string $ jti , int $ ttl ): bool {
810- if ( empty ( $ jti ) ) {
811- return false ;
812- }
813-
814- // Use transient with TTL matching token expiration
815- return set_transient ( "wcpos_blacklist_ {$ jti }" , true , $ ttl );
816- }
817-
818852 /**
819853 * Check if an access token is blacklisted.
820854 *
@@ -830,30 +864,4 @@ private function is_access_token_blacklisted( string $jti ): bool {
830864 // Check transient
831865 return false !== get_transient ( "wcpos_blacklist_ {$ jti }" );
832866 }
833-
834- /**
835- * Revoke session and blacklist current access token.
836- *
837- * @param int $user_id
838- * @param string $refresh_jti Refresh token JTI.
839- * @param string $access_jti Optional access token JTI to blacklist immediately.
840- *
841- * @return bool
842- */
843- public function revoke_session_with_blacklist ( int $ user_id , string $ refresh_jti , string $ access_jti = '' ): bool {
844- // Revoke the refresh token (session)
845- $ revoked = $ this ->revoke_session ( $ user_id , $ refresh_jti );
846-
847- // Blacklist the current access token if provided
848- if ( $ revoked && ! empty ( $ access_jti ) ) {
849- // Calculate TTL - access tokens expire in 30 minutes by default
850- $ issued_at = time ();
851- $ expire = apply_filters ( 'woocommerce_pos_jwt_access_token_expire ' , $ issued_at + ( HOUR_IN_SECONDS / 2 ), $ issued_at );
852- $ ttl = max ( 0 , $ expire - $ issued_at );
853-
854- $ this ->blacklist_access_token ( $ access_jti , $ ttl );
855- }
856-
857- return $ revoked ;
858- }
859867}
0 commit comments