Ettic Docs
MagicAuthDevelopers

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 );

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 after wp_set_auth_cookie on 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 through admin-post.php.

Filters

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_args
  • magicauth_disabled_notice_html
  • magicauth_disabled_notice_plaintext
  • magicauth_disabled_notice_subject
  • magicauth_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' );

On this page