Authentication & Sessions
How WordPress logs a user in: wp_signon(), the auth and logged_in cookies, session tokens and application passwords.
wp_signon( array $credentials = array(), string|bool $secure_cookie = '' ): WP_User|WP_ErrorWordPress authentication is cookie-based and stateless on the server. There is no PHP session; every request re-identifies the user by validating a signed cookie during wp-settings.php.
Two cookies, not one
wp_set_auth_cookie( $user_id, $remember, $secure, $token ) sets both. The auth cookie (wordpress_, or wordpress_sec_ over HTTPS) is scoped to /wp-admin (ADMIN_COOKIE_PATH) and the plugins directory (PLUGINS_COOKIE_PATH), so it is not sent on an ordinary front-end request. The logged_in cookie (wordpress_logged_in_) is set site-wide and is the one the front end reads, which is why is_user_logged_in() works on a theme template while the admin cookie is not even sent. Both are signed with the salts in wp-config.php; changing AUTH_KEY and friends invalidates every login instantly.
Logging in
wp_signon( $credentials, $secure_cookie ) takes user_login, user_password and remember, and returns a WP_User or a WP_Error. It sends headers, so it must run before any output — hook it no later than init. It sets cookies but does not set the current user for the rest of that request.
Sessions and application passwords
Each login stores a token in the user's session_tokens meta, so a single device can be logged out without touching the others. For machine access, application passwords (WordPress 5.6) are per-application credentials sent as HTTP Basic Auth; they require HTTPS unless the environment type is local, and they authenticate the REST API and XML-RPC only — never wp-login.php.
Example
<?php
/**
* Handle a custom front-end login form.
* Runs on init because wp_signon() sends headers and must precede output.
*/
add_action( 'init', static function (): void {
if ( ! isset( $_POST['acme_login_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_key( $_POST['acme_login_nonce'] ), 'acme_login' ) ) {
return;
}
$user = wp_signon( array(
'user_login' => sanitize_user( wp_unslash( $_POST['log'] ?? '' ) ),
// Never sanitise or trim a password: it must reach WP byte for byte.
'user_password' => (string) ( $_POST['pwd'] ?? '' ),
'remember' => ! empty( $_POST['rememberme'] ),
), is_ssl() );
if ( is_wp_error( $user ) ) {
// Show a generic message; the error codes leak whether a login exists.
set_transient( 'acme_login_error', 1, 60 );
return;
}
// Cookies are set, but this request still thinks nobody is logged in.
wp_set_current_user( $user->ID );
wp_safe_redirect( home_url( '/account/' ) );
exit;
} );When to use it
- Build a front-end login form that does not redirect through wp-login.php.
- Authenticate a deploy script or integration against the REST API.
- Add a 'sign out everywhere' control to a member account page.
- Force every user to re-authenticate by rotating the salts in wp-config.php.
More examples
Calling the REST API with an application password
How an external script, a deploy job or another WordPress install authenticates against a site without storing the account password. The request must be over HTTPS — application passwords are unavailable otherwise unless the site's environment type is local — and the credential only works for REST and XML-RPC, so it cannot be used to sign in through the browser.
// Generated on the user's profile screen (WordPress 5.6+). The displayed
// value contains spaces; send it exactly as shown, spaces included.
$app_password = 'abcd EFGH ijkl MNOP qrst UVWX';
$response = wp_remote_get(
'https://example.com/wp-json/wp/v2/posts?status=draft',
array(
'headers' => array(
// HTTP Basic Auth: base64( username:application-password ).
'Authorization' => 'Basic ' . base64_encode( 'editor:' . $app_password ),
),
'timeout' => 15,
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$posts = json_decode( wp_remote_retrieve_body( $response ), true );Logging the user out of every other device
Powers the "log out everywhere else" button on the profile screen, and is what you should call after a password change or a suspected account compromise. Destroying tokens invalidates the matching cookies immediately, without waiting for expiry.
// Each login gets its own session token stored in the user's
// session_tokens meta, so they can be revoked individually.
$user_id = get_current_user_id();
$manager = WP_Session_Tokens::get_instance( $user_id );
// Keep this browser signed in, drop all the others.
$manager->destroy_others( wp_get_session_token() );
// Or end every session, including this one, after a password reset:
// $manager->destroy_all();
// wp_logout() ends only the current session and clears its cookies.Restricting where application passwords are allowed
A common hardening step: an application password on an administrator account is a full-privilege bearer credential sitting in someone's CI config. Restricting the feature to low-privilege service accounts limits the blast radius if one leaks.
/**
* Turn application passwords off for administrators, keeping them for
* service accounts. The filter runs per user (WordPress 5.6+).
*
* @param bool $available Whether the feature is available.
* @param WP_User $user The user being checked.
*/
add_filter( 'wp_is_application_passwords_available_for_user', static function ( $available, $user ) {
if ( user_can( $user, 'manage_options' ) ) {
return false;
}
return $available;
}, 10, 2 );
// Site-wide off switch, ignoring the per-user rules:
// add_filter( 'wp_is_application_passwords_available', '__return_false' );
Discussion