Skip to main content

benecaster_invalid_token_recorded

Action Free

Fires from Security\InvalidTokenMonitor each time a feed request presents a token that matches no row in benecaster_tokens. $current_count is the number of such failures for that show in the last $context['window_minutes'] minutes (default 60), this one included — a true rolling window of per-minute buckets, not a counter that only resets after an hour of quiet.

Revoked tokens never fire this hook, by design. A cancelled subscriber whose podcast app keeps polling produces a revoked lookup every poll — that is a re-engagement signal, not an attack, and counting it would make any show with churn look under attack. An active token presented to the wrong show also classifies as revoked and does not fire it. For every failed poll regardless of type, use benecaster_token_invalid or benecaster_feed_request_dispatched instead.

Per-show counting only: there is no per-IP counter in core (an attacker rotating IPs would otherwise create unbounded storage). Build a per-source breakdown from $context['ip_hash'] in your own listener. Counting is approximate under concurrent requests. The window is per show — benecaster_invalid_token_alert_threshold can change it — and when that filter switches monitoring off for a show, this action does not fire for it.

Break a failed-token spike down by source

Free Intermediate

Core counts failed lookups per show only — deliberately, since a per-IP counter would let an attacker rotating IPs create unbounded storage. The IP hash travels in the hook’s $context so you can keep your own per-source tally where your storage can take it. Only tokens matching no subscriber row fire this; a churned subscriber’s app polling a revoked token never does.

ip_hash is salted with benecaster_ip_hash_salt — it identifies a repeat source but cannot be reversed to an address. Never log $_SERVER['REMOTE_ADDR'] alongside it; the plugin’s security checklist forbids storing raw IPs.

<?php
add_action(
    'benecaster_invalid_token_recorded',
    function ( int $show_id, int $current_count, array $context ): void {
        $source = $context['ip_hash'] ?? 'unknown';
        $key    = 'my_bad_token_' . $show_id . '_' . substr( (string) $source, 0, 16 );

        // Your own short-lived per-source tally. One transient per
        // source: on a site without a persistent object cache these are
        // wp_options rows, so keep the TTL short.
        $hits = (int) get_transient( $key ) + 1;
        set_transient( $key, $hits, HOUR_IN_SECONDS );

        // One source responsible for most of the show's failures in the
        // window looks like a scraper or a guessing run, not a typo.
        if ( $hits >= 20 && $hits * 2 >= $current_count ) {
            my_ops_alert( sprintf(
                'Show #%d: %d failed token attempts in %d min, %d from one source (%s)',
                $show_id,
                $current_count,
                $context['window_minutes'],
                $hits,
                $context['country_code'] ?? '??'
            ) );
        }
    },
    10,
    3
);

View on GitHub →

Parameters

Name Type Default Description
$show_id int ID of the show the failed lookup was for.
$current_count int Failures for this show in the last `$context['window_minutes']` minutes, this one included.
$context array Keys: `ip_hash` (string — salted SHA-256, same salt as the feed request log, never a raw IP), `token_prefix` (string — first 8 characters of the presented token), `user_agent` (?string), `country_code` (?string), `region_code` (?string), `window_minutes` (int).

Examples

Log every counted failure to your own observability platform

add_action( 'benecaster_invalid_token_recorded', function ( int $show_id, int $current_count, array $context ): void {
    error_log( sprintf( 'Show #%d: %d failed token attempts in the last %d min', $show_id, $current_count, $context['window_minutes'] ) );
}, 10, 3 );

Notes

Distinct from benecaster_token_invalid, which fires for every failed lookup (revoked included), before revoked and not-found are told apart, and carries only ( $token_prefix, $show_id ). See the recipe above for a per-source breakdown.

Need this built rather than just documented? See our services →