@@ -115,6 +115,17 @@ public function validate_token( $token = '', $token_type = 'access' ) {
115115 );
116116 }
117117
118+ // Check if access token is blacklisted (for instant revocation)
119+ if ( 'access ' === $ token_type && isset ( $ decoded_token ->jti ) ) {
120+ if ( $ this ->is_access_token_blacklisted ( $ decoded_token ->jti ) ) {
121+ return new WP_Error (
122+ 'woocommerce_pos_auth_token_revoked ' ,
123+ 'Access token has been revoked ' ,
124+ array ( 'status ' => 403 )
125+ );
126+ }
127+ }
128+
118129 // Everything looks good return the decoded token
119130 return $ decoded_token ;
120131 } catch ( Exception $ e ) {
@@ -133,10 +144,11 @@ public function validate_token( $token = '', $token_type = 'access' ) {
133144 * Generate an access token for the provided user (short-lived).
134145 *
135146 * @param WP_User $user
147+ * @param string $refresh_jti Optional refresh token JTI to link access token to session.
136148 *
137149 * @return string|WP_Error
138150 */
139- public function generate_access_token ( WP_User $ user ) {
151+ public function generate_access_token ( WP_User $ user, string $ refresh_jti = '' ) {
140152 // First thing, check the secret key if not exist return a error
141153 if ( ! $ this ->get_secret_key () ) {
142154 return new WP_Error (
@@ -166,10 +178,14 @@ public function generate_access_token( WP_User $user ) {
166178 */
167179 $ expire = apply_filters ( 'woocommerce_pos_jwt_access_token_expire ' , $ issued_at + ( HOUR_IN_SECONDS / 2 ), $ issued_at );
168180
181+ // Generate unique JTI for access token
182+ $ jti = wp_generate_uuid4 ();
183+
169184 $ token = array (
170185 'iss ' => get_bloginfo ( 'url ' ),
171186 'iat ' => $ issued_at ,
172187 'exp ' => $ expire ,
188+ 'jti ' => $ jti ,
173189 'type ' => 'access ' ,
174190 'data ' => array (
175191 'user ' => array (
@@ -178,6 +194,11 @@ public function generate_access_token( WP_User $user ) {
178194 ),
179195 );
180196
197+ // Link to refresh token if provided
198+ if ( ! empty ( $ refresh_jti ) ) {
199+ $ token ['refresh_jti ' ] = $ refresh_jti ;
200+ }
201+
181202 /*
182203 * Let the user modify the access token data before the sign.
183204 *
@@ -274,16 +295,24 @@ public function generate_refresh_token( WP_User $user ) {
274295 * @return array|WP_Error
275296 */
276297 public function generate_token_pair ( WP_User $ user ) {
277- $ access_token = $ this ->generate_access_token ( $ user );
278- if ( is_wp_error ( $ access_token ) ) {
279- return $ access_token ;
280- }
281-
298+ // Generate refresh token first to get its JTI
282299 $ refresh_token = $ this ->generate_refresh_token ( $ user );
283300 if ( is_wp_error ( $ refresh_token ) ) {
284301 return $ refresh_token ;
285302 }
286303
304+ // Decode to get the JTI
305+ $ decoded_refresh = $ this ->validate_token ( $ refresh_token , 'refresh ' );
306+ if ( is_wp_error ( $ decoded_refresh ) ) {
307+ return $ decoded_refresh ;
308+ }
309+
310+ // Generate access token with link to refresh token
311+ $ access_token = $ this ->generate_access_token ( $ user , $ decoded_refresh ->jti ?? '' );
312+ if ( is_wp_error ( $ access_token ) ) {
313+ return $ access_token ;
314+ }
315+
287316 $ issued_at = time ();
288317 $ expire = apply_filters ( 'woocommerce_pos_jwt_access_token_expire ' , $ issued_at + ( HOUR_IN_SECONDS / 2 ), $ issued_at );
289318
@@ -399,8 +428,8 @@ public function refresh_access_token( string $refresh_token ) {
399428 // Update last_active timestamp for this session
400429 $ this ->update_session_activity ( $ decoded ->data ->user ->id , $ decoded ->jti ?? '' );
401430
402- // Generate new access token (refresh token stays the same)
403- $ new_access_token = $ this ->generate_access_token ( $ user );
431+ // Generate new access token with link to refresh token (refresh token stays the same)
432+ $ new_access_token = $ this ->generate_access_token ( $ user, $ decoded -> jti ?? '' );
404433 if ( is_wp_error ( $ new_access_token ) ) {
405434 return $ new_access_token ;
406435 }
@@ -587,7 +616,7 @@ public function revoke_all_sessions_except( int $user_id, string $current_jti ):
587616 *
588617 * @return bool
589618 */
590- private function update_session_activity ( int $ user_id , string $ jti ): bool {
619+ public function update_session_activity ( int $ user_id , string $ jti ): bool {
591620 $ refresh_tokens = get_user_meta ( $ user_id , '_woocommerce_pos_refresh_tokens ' , true );
592621 if ( ! \is_array ( $ refresh_tokens ) || ! isset ( $ refresh_tokens [ $ jti ] ) ) {
593622 return false ;
@@ -768,4 +797,63 @@ private function parse_user_agent( string $user_agent ): array {
768797
769798 return $ device_info ;
770799 }
800+
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+
818+ /**
819+ * Check if an access token is blacklisted.
820+ *
821+ * @param string $jti Access token JTI.
822+ *
823+ * @return bool
824+ */
825+ private function is_access_token_blacklisted ( string $ jti ): bool {
826+ if ( empty ( $ jti ) ) {
827+ return false ;
828+ }
829+
830+ // Check transient
831+ return false !== get_transient ( "wcpos_blacklist_ {$ jti }" );
832+ }
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+ }
771859}
0 commit comments