# Passwordless Login With an Email Link

> Adds an "Email me a login link" option to the login screen; members enter their email or username and get a one-time link that logs them in for 15 minutes.

- Type: PHP
- Runs: Run everywhere
- Category: [Login](https://wpide.com/library/category/login/)
- Tags: login, passwordless, email
- Author: WPIDE Team
- Installs: 0
- Updated: 2026-10-28
- Page: https://wpide.com/library/magic-link-login/

## Code

```php
// Minutes a login link stays valid.
$wpide_magic_minutes = 15;
// Roles that may not use login links.
$wpide_magic_excluded_roles = array( 'administrator' );

add_action( 'login_form', function () {
    echo '<p style="margin:0 0 16px"><a href="' . esc_url( add_query_arg( 'action', 'wpide_magic', wp_login_url() ) ) . '">Email me a login link instead</a></p>';
} );

add_action( 'login_form_wpide_magic', function () use ( $wpide_magic_minutes, $wpide_magic_excluded_roles ) {
    if ( ! function_exists( 'login_header' ) ) {
        return;
    }

    $sent = false;

    if ( 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' ) && isset( $_POST['wpide_magic_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['wpide_magic_nonce'] ), 'wpide_magic' ) ) {
        $login = isset( $_POST['wpide_magic_login'] ) ? sanitize_text_field( wp_unslash( $_POST['wpide_magic_login'] ) ) : '';
        $user  = is_email( $login ) ? get_user_by( 'email', $login ) : get_user_by( 'login', $login );
        $sent  = true; // Same message either way, so nobody can test which accounts exist.

        $recent = $user ? (int) get_user_meta( $user->ID, 'wpide_magic_sent', true ) : 0;

        if ( $user && ! array_intersect( $wpide_magic_excluded_roles, (array) $user->roles ) && time() - $recent > MINUTE_IN_SECONDS ) {
            $token = wp_generate_password( 40, false );
            update_user_meta( $user->ID, 'wpide_magic_token', array( 'hash' => wp_hash( $token ), 'expires' => time() + $wpide_magic_minutes * MINUTE_IN_SECONDS ) );
            update_user_meta( $user->ID, 'wpide_magic_sent', time() );

            $link = add_query_arg( array( 'action' => 'wpide_magic_login', 'uid' => $user->ID, 'token' => $token ), wp_login_url() );

            wp_mail(
                $user->user_email,
                sprintf( '[%s] Your login link', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ),
                sprintf( "Hi %s,\n\nClick this link to log in. It works once and expires in %d minutes:\n%s\n\nIf you did not ask for it, you can ignore this email.", $user->display_name, $wpide_magic_minutes, $link )
            );
        }
    }

    login_header( 'Email me a login link' );

    if ( $sent ) {
        echo '<p class="message">If that account exists, a login link is on its way. Check your email.</p>';
    } else {
        ?>
        <form method="post" action="<?php echo esc_url( add_query_arg( 'action', 'wpide_magic', wp_login_url() ) ); ?>">
            <p><label for="wpide_magic_login">Username or email address</label>
            <input type="text" name="wpide_magic_login" id="wpide_magic_login" class="input" autocomplete="username" required></p>
            <?php wp_nonce_field( 'wpide_magic', 'wpide_magic_nonce' ); ?>
            <p class="submit"><button type="submit" class="button button-primary button-large">Send login link</button></p>
        </form>
        <?php
    }

    echo '<p id="nav"><a href="' . esc_url( wp_login_url() ) . '">Log in with a password</a></p>';
    login_footer();
    exit;
} );

add_action( 'login_form_wpide_magic_login', function () {
    $user_id = isset( $_GET['uid'] ) ? absint( $_GET['uid'] ) : 0;
    $token   = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
    $data    = $user_id ? get_user_meta( $user_id, 'wpide_magic_token', true ) : '';

    if ( ! $token || ! is_array( $data ) || time() > (int) $data['expires'] || ! hash_equals( $data['hash'], wp_hash( $token ) ) ) {
        wp_safe_redirect( add_query_arg( 'wpide_magic', 'invalid', wp_login_url() ) );
        exit;
    }

    delete_user_meta( $user_id, 'wpide_magic_token' );
    wp_set_current_user( $user_id );
    wp_set_auth_cookie( $user_id );
    do_action( 'wp_login', get_userdata( $user_id )->user_login, get_userdata( $user_id ) );

    wp_safe_redirect( apply_filters( 'login_redirect', admin_url(), '', get_userdata( $user_id ) ) );
    exit;
} );

add_filter( 'login_message', function ( $message ) {
    if ( isset( $_GET['wpide_magic'] ) ) {
        $message .= '<p class="message">That login link is invalid or has expired. Please ask for a new one.</p>';
    }

    return $message;
} );
```

## Notes

Links work once and expire after 15 minutes. Roles in $wpide_magic_excluded_roles (administrators by default) must still use their password.

## How to use it

Install the free WPIDE plugin (https://wordpress.org/plugins/wpide/), find "Passwordless Login With an Email Link" in its snippet library and add it in one click; or create a new PHP snippet in WPIDE (or any code snippets plugin) and paste the code above. More snippets: https://wpide.com/library/
