Authentication & Sessions

How WordPress logs a user in: wp_signon(), the auth and logged_in cookies, session tokens and application passwords.

Syntaxwp_signon( array $credentials = array(), string|bool $secure_cookie = '' ): WP_User|WP_Error

WordPress 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

Example · php
<?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.

Example · php
// 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.

Example · php
// 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.

Example · php
/**
 * 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

  • Be the first to comment on this lesson.