Hooks
Every action and filter MagicAuth fires.
MagicAuth fires actions for lifecycle events you'd want to react to (token issued, token consumed, just before the auth cookie is set) and filters for everywhere you'd want to override behaviour (every line of the email, the redirect target, IP detection, capability checks).
All hooks are prefixed magicauth_. Add-ons and integrations should use the same prefix to avoid collisions.
Actions
magicauth_booted
Fires after Plugin::boot() has wired every module. Use this as the canonical "MagicAuth is ready" signal for register-on-boot integrations.
add_action( 'magicauth_booted', function () {
// Register your add-on here.
} );magicauth_token_issued
Fires after a new token row is inserted into the database. Useful for logging, analytics, or syncing to a CRM.
/**
* @param int $user_id WordPress user ID.
* @param string $selector 16-char opaque selector for this token row.
*/
add_action( 'magicauth_token_issued', function ( $user_id, $selector ) {
error_log( "MagicAuth: token issued for user {$user_id}" );
}, 10, 2 );The plaintext verifier and code are not passed to this hook. They exist only in the email and verify URL.
magicauth_token_consumed
Fires once a link or code successfully authenticates. Same arguments as magicauth_token_issued.
add_action( 'magicauth_token_consumed', function ( $user_id, $selector ) {
// Bump a "last passwordless sign-in" timestamp, etc.
}, 10, 2 );magicauth_pre_set_auth_cookie
Fires immediately before wp_set_auth_cookie. Last chance to short-circuit or instrument a sign-in.
/**
* @param int $user_id WordPress user ID.
* @param string $context Where the sign-in originated. Currently `'shortcode'`.
*/
add_action( 'magicauth_pre_set_auth_cookie', function ( $user_id, $context ) {
// Pre-cookie work: sticky-session bump, audit row, etc.
}, 10, 2 );magicauth_throttle_keys_flushed
Fires after an admin clicks Reset throttle counters. Object-cache plugins can listen here to invalidate their own derived state.
/**
* @param int $count Number of throttle keys cleared.
* @param string[] $keys The transient key names that were cleared.
*/
add_action( 'magicauth_throttle_keys_flushed', function ( $count, $keys ) {
// ...
}, 10, 2 );magicauth_daily_cleanup
WP-Cron hook that fires once a day to sweep consumed and expired token rows. Don't normally bind to it. Bind to magicauth_token_consumed for per-row work.
Compatibility actions (re-fired)
wp_login. Fired afterwp_set_auth_cookieon every successful sign-in (magic link, code, password). Ensures security and analytics plugins observe MagicAuth sign-ins as normal logins.wp_login_failed. Fired after every failed password submission. Ensures brute-force-defence plugins (Wordfence, Limit Login Attempts) account for misses even though the request goes throughadmin-post.php.
Filters
Email pipeline (magic-link)
The full email-rendering pipeline runs on every sign-in email. Each filter takes the previous filter's output, so you can layer customisations.
magicauth_email_template_args
Mutate the canonical args array before any template renders. Most other email filters receive the post-filter args, so this is the cleanest place to inject context.
/**
* @param array{user:WP_User,link:string,code:string,code_display:string,
* expires_at:string,expiry_minutes:int,brand_color:string,
* brand_text:string,company_name:string,site_name:string,
* is_test:bool} $args
* @param WP_User $user
*/
add_filter( 'magicauth_email_template_args', function ( $args, $user ) {
$args['support_url'] = 'https://example.com/support';
return $args;
}, 10, 2 );magicauth_email_html
Replace or post-process the rendered HTML body.
add_filter( 'magicauth_email_html', function ( $html, $args ) {
return $html . '<p style="text-align:center;color:#888">Sent by Acme Inc.</p>';
}, 10, 2 );magicauth_email_plaintext
Replace or post-process the rendered plaintext body (used as AltBody in the multipart email).
add_filter( 'magicauth_email_plaintext', function ( $text, $args ) {
return $text . "\n\n--\nSent by Acme Inc.";
}, 10, 2 );magicauth_email_subject
Override the subject line. Default is {XXX-XXX} is your {Company}-code.
add_filter( 'magicauth_email_subject', function ( $subject, $user, $args ) {
return sprintf( 'Sign in to %s, code %s', $args['company_name'], $args['code_display'] );
}, 10, 3 );magicauth_email_from
Override the From header. The filter receives a [$display_name, $address] array.
add_filter( 'magicauth_email_from', function ( $from, $user ) {
return [ 'Acme Sign-in', 'no-reply@acme.com' ];
}, 10, 2 );magicauth_email_headers
Override the full wp_mail headers array. Less common; most use cases are better served by magicauth_email_from.
add_filter( 'magicauth_email_headers', function ( $headers, $user ) {
$headers[] = 'Reply-To: support@acme.com';
return $headers;
}, 10, 2 );magicauth_email_send
Short-circuit wp_mail entirely. Return any non-null value to skip the default send and use your own ESP. The return value is cast to bool and propagated as the success flag.
add_filter( 'magicauth_email_send', function ( $null, $user, $args ) {
return acme_postmark_send( $user->user_email, $args ) ? true : false;
}, 10, 3 );This is the integration point for Postmark, SendGrid, AWS SES, or any other transactional ESP. See Email customisation for a complete recipe.
Email pipeline (account-disabled notice)
The "your magic-link sign-in is disabled" email has a parallel filter pipeline. Same shape as the magic-link filters, different names:
magicauth_disabled_notice_template_argsmagicauth_disabled_notice_htmlmagicauth_disabled_notice_plaintextmagicauth_disabled_notice_subjectmagicauth_disabled_notice_send
The magicauth_email_from and magicauth_email_headers filters are shared with the magic-link path.
Templates
magicauth_template_path
Change the theme subdirectory MagicAuth looks in for overrides. Default is magicauth/.
add_filter( 'magicauth_template_path', fn () => 'auth/' );
// Now overrides live at your-theme/auth/login-form.php, etc.magicauth_locate_template
Full override of the template-resolution algorithm. Receives the resolved path, the requested filename, and the candidate path list MagicAuth tried.
add_filter( 'magicauth_locate_template', function ( $resolved, $filename, $candidates ) {
if ( 'email-magic-link.php' === $filename ) {
return WP_PLUGIN_DIR . '/my-mu/email-magic-link.php';
}
return $resolved;
}, 10, 3 );Sign-in flow
magicauth_remember_default
Whether wp_set_auth_cookie is called with $remember = true. Default true (sessions persist 14 days).
add_filter( 'magicauth_remember_default', '__return_false' );magicauth_redirect_to
Final post-sign-in redirect target.
/**
* @param string $target The URL MagicAuth was about to redirect to.
* @param WP_User $user
* @param string $context `'shortcode'` for shortcode/branded sign-ins.
*/
add_filter( 'magicauth_redirect_to', function ( $target, $user, $context ) {
if ( in_array( 'subscriber', $user->roles, true ) ) {
return home_url( '/account/' );
}
return $target;
}, 10, 3 );magicauth_login_heading
Override the heading shown on the branded sign-in card. Per-state.
/**
* @param string $default MagicAuth's default heading.
* @param string $state `'A'`, `'B'`, `'C'`, `'D'`, or `'E'`.
* @param string $brand Short brand label extracted from the Site Title.
* @param string $site_name Full Site Title.
*/
add_filter( 'magicauth_login_heading', function ( $default, $state, $brand, $site_name ) {
if ( 'A' === $state ) {
return 'Welcome back to ' . $brand;
}
return $default;
}, 10, 4 );Front-end assets
magicauth_force_frontend_assets
Force-load MagicAuth's CSS and JS on pages that don't contain the shortcode. Useful when a page builder injects the shortcode dynamically.
add_filter( 'magicauth_force_frontend_assets', function ( $force ) {
return is_page( 'sign-in' ) ? true : $force;
} );IP detection
magicauth_client_ip
Override the client IP. By default MagicAuth uses REMOTE_ADDR. Behind a reverse proxy or CDN you'll want to swap in a trusted header.
The filtered value is validated as an IP. Return false or an invalid string and MagicAuth falls back to REMOTE_ADDR.
add_filter( 'magicauth_client_ip', function ( $ip ) {
return $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $ip;
} );Only trust forwarded headers if your network actually terminates the request behind a proxy you control. Otherwise an attacker can spoof the header to bypass per-IP throttles.
Permissions
magicauth_current_user_can_control_user
Override the capability check that gates the per-user admin actions (issue, send, reset, disable). Default is manage_options plus a "users can always control themselves" rule.
/**
* @param bool $can MagicAuth's default decision.
* @param int $target_user_id The user the action would affect.
*/
add_filter( 'magicauth_current_user_can_control_user', function ( $can, $target_user_id ) {
if ( current_user_can( 'edit_users' ) ) {
return true;
}
return $can;
}, 10, 2 );Throttle tuning
magicauth_throttle_registry_max
Maximum number of throttle keys the per-request registry holds. Default 5000. Raise on very high-traffic sites.
add_filter( 'magicauth_throttle_registry_max', fn () => 20000 );magicauth_throttle_allow_reset_all
Safety gate for Throttle::reset_all(). Intended for unit tests.
Never enable this in production. Throttle::reset_all() clears every active rate-limit counter site-wide and will let an attacker bypass the throttle.
Debugging
magicauth_debug_log
Whether MagicAuth writes to the WP error log. Default follows WP_DEBUG_LOG.
add_filter( 'magicauth_debug_log', '__return_true' );