Authentication and Login API

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 8.4 and WP-CLI 2.12.0, and checked on 2026-10-28. WordPress 7.1 requires PHP 7.4 at minimum, and PHP 8.3 or newer is recommended. WordPress 7.2 is in beta and due in December 2026; nothing on this page is expected to change with it. Source excerpts are from the 7.1.3 wp-includes/pluggable.php, and every command and request below was run against a WordPress 7.1.3 site (official wordpress:7.1.3-php8.4-apache image, MariaDB 11.8, table prefix lp_, WP_ENVIRONMENT_TYPE set to local). Output blocks are real, with secrets replaced by placeholders. The standard test stack is described in Local Development Environments.

This is the developer’s view of logging in: which functions establish who the current user is, which hooks run on the way, and how passwords, cookies and sessions are stored. The administrator’s side (HTTPS, two-factor authentication, brute-force limits, pinning the bcrypt cost) is in Security Hardening; the user’s side (profiles, password resets, the Application Passwords screen) is in Users, Roles and Profiles; what an identified user may do is in Roles and Capabilities API.

The examples belong to Lumen Press, the fictitious publisher used throughout this sub-section; sam.reader is a Subscriber.

Two steps: identifying the user and checking a password

  • Identifying the current user, on every request. Nothing happens until code first asks (wp_get_current_user(), current_user_can(), is_user_logged_in()). Then the determine_current_user filter runs. Core registers wp_validate_auth_cookie() (priority 10, the admin cookie), wp_validate_logged_in_cookie() (20, the front-end cookie, skipped inside wp-admin) and wp_validate_application_password() (20, HTTP Basic credentials). The result is stored by wp_set_current_user(), which fires set_current_user.

  • Checking credentials, once, at login. wp_signon() calls wp_authenticate(), which runs the authenticate filter chain; on success it issues cookies with wp_set_auth_cookie() and fires wp_login.

Most of these functions live in wp-includes/pluggable.php and are pluggable: a plugin loaded earlier can replace them (wp_hash_password(), wp_check_password(), wp_set_auth_cookie(), wp_validate_auth_cookie(), wp_authenticate(), wp_logout()). Each now has filters, so replacing one is rarely needed; see Hooks: Actions and Filters. On the REST API, a request carrying only the login cookie without a valid wp_rest nonce is treated as anonymous; see REST API: Using It.

Auth cookies

A login sets three HttpOnly cookies. Their names end in COOKIEHASH, the MD5 of the siteurl option, so two sites on one host do not clash and changing the site URL logs everybody out:

Constant (default name) Path Purpose

AUTH_COOKIE (wordpress_<hash>)

/wp-admin and /wp-content/plugins

Admin authentication over HTTP, scheme auth

SECURE_AUTH_COOKIE (wordpress_sec_<hash>)

/wp-admin and /wp-content/plugins

The same over HTTPS (Secure flag), scheme secure_auth; replaces AUTH_COOKIE on HTTPS sites

LOGGED_IN_COOKIE (wordpress_logged_in_<hash>)

COOKIEPATH (usually /)

Identifies the user on the front end, scheme logged_in; Secure only when the home URL is HTTPS

TEST_COOKIE (wordpress_test_cookie)

/

Lets wp-login.php detect a browser that refuses cookies

The path restriction is a defense: the cookie that grants admin access is never sent with front-end requests. Names and paths can be redefined in wp-config.php Reference, which only renames them. A curl login as tom.author on the test site received exactly these: the test cookie, the admin cookie twice (once per path) and the logged_in cookie, all HttpOnly, and a redirect to /wp-admin/.

A value has four fields separated by | (%7C once URL-encoded): user login, expiration timestamp, a 43-character session token, and an HMAC-SHA256. wp_generate_auth_cookie() builds it, wp_parse_auth_cookie() splits it and wp_validate_auth_cookie() checks it. Simplified from the 7.1.3 source, the key is wp_hash( $login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ) and the last field is hash_hmac( 'sha256', $login . '|' . $expiration . '|' . $token, $key ), where $pass_frag is four characters of the stored password hash (characters 8 to 11 for phpass and plain bcrypt hashes, the last four for the $wp$ hashes of 6.8 and later). Three consequences follow:

  • wp_hash() is an HMAC keyed with wp_salt( $scheme ): AUTH_KEY and AUTH_SALT for auth, the SECURE_AUTH_* pair for secure_auth, the LOGGED_IN_* pair for logged_in. Rotating the salts (wp config shuffle-salts) invalidates every cookie.

  • Part of the password hash enters the key, so a password change invalidates the user’s cookies.

  • The token must also exist in the user’s session store (Sessions), so one session can be revoked alone.

A WP-CLI script (wp eval-file) shows the parts and the scheme check:

<?php
$user   = get_user_by( 'login', 'tom.author' );
$cookie = wp_generate_auth_cookie( $user->ID, time() + 2 * DAY_IN_SECONDS, 'logged_in' );

WP_CLI::line( LOGGED_IN_COOKIE . ' = md5( ' . get_option( 'siteurl' ) . ' ): ' . COOKIEHASH );
foreach ( wp_parse_auth_cookie( $cookie, 'logged_in' ) as $key => $value ) {
	WP_CLI::line( sprintf( '%-11s %s', $key, $value ) );
}
WP_CLI::line( 'Valid as logged_in: ' . var_export( wp_validate_auth_cookie( $cookie, 'logged_in' ), true ) );
WP_CLI::line( 'Valid as auth:      ' . var_export( wp_validate_auth_cookie( $cookie, 'auth' ), true ) );

wp_set_password( '<new-password>', $user->ID ); // Changes the hash fragment in the HMAC key.
WP_CLI::line( 'After a password change: ' . var_export( wp_validate_auth_cookie( $cookie, 'logged_in' ), true ) );
wordpress_logged_in_692cae3d5f4e20ddb2e4517f93172921 = md5( http://lumenpress.test ): 692cae3d5f4e20ddb2e4517f93172921
username    tom.author
expiration  1793387133
token       QHvCCrha7U7KMdW139g7Q3Gj4nqkrLpp6pbNo8CM384
hmac        bf3ef2b26692fbeec7b623906a90e09fd523ef3398390133d8c800d40ad5e1fa
scheme      logged_in
Valid as logged_in: 3
Valid as auth:      false
After a password change: false

wp_validate_auth_cookie() fires auth_cookie_malformed, auth_cookie_expired, auth_cookie_bad_username, auth_cookie_bad_hash, auth_cookie_bad_session_token or auth_cookie_valid, useful audit points. POST and AJAX requests get one hour of grace after expiry, so a form submitted just too late is not lost.

Lifetime. Without Remember Me the cookies last two days and are browser-session cookies; with it, 14 days. Both pass through auth_cookie_expiration (length, user ID, remember). secure_auth_cookie and secure_logged_in_cookie decide the Secure flag; set_auth_cookie, set_logged_in_cookie and clear_auth_cookie fire when cookies are written or removed.

Logging in from code

wp_signon()

wp_signon( $credentials, $secure_cookie ) is the whole login: it authenticates, sets the cookies, clears a pending password-reset key and fires wp_login, returning a WP_User or a WP_Error. The array takes user_login, user_password and remember; with no arguments it reads log, pwd and rememberme from $_POST, as wp-login.php does. It sends cookies, so it must run before any output, in a handler such as admin_post_nopriv_{$action} or template_redirect, never in a shortcode or template.

The no-argument form is also the safe way to pass the password. WordPress has always hashed passwords as received in the slashed $_POST, so passing wp_unslash( $_POST['pwd'] ) makes passwords containing a quote or a backslash fail.

Lumen Press lets readers sign in from a Sign in page (/sign-in/) whose form (fields log, pwd, rememberme, a hidden action of lumen_login and wp_nonce_field( 'lumen_login' )) posts to admin-post.php. The handler is a must-use plugin, wp-content/mu-plugins/lumen-front-login.php:

<?php
/**
 * Plugin Name: Lumen Front-end Login
 * Description: Signs readers in from the site's own sign-in form through admin-post.php.
 * Version:     0.1.0
 * License:     GPL-2.0-or-later
 */

defined( 'ABSPATH' ) || exit;

add_action(
	'admin_post_nopriv_lumen_login',
	static function () {
		check_admin_referer( 'lumen_login' );

		// With no arguments, wp_signon() reads log, pwd and rememberme from $_POST itself.
		$user = wp_signon();

		if ( is_wp_error( $user ) ) {
			wp_safe_redirect( add_query_arg( 'login', 'failed', home_url( '/sign-in/' ) ) );
			exit;
		}
		wp_safe_redirect( home_url( '/books/' ) );
		exit;
	}
);

The nonce guards against login CSRF (a hostile page signing the visitor into an attacker’s account), and the failure redirect does not say which usernames exist. On the test site a valid submission returned 302 to /books/ with the three auth cookies, a wrong password 302 to /sign-in/?login=failed, and a submission without the nonce 403. For a plain form, wp_login_form() prints the core form (posting to wp-login.php) with arguments such as redirect and remember and the login_form_defaults, login_form_top, login_form_middle and login_form_bottom filters; the Login/out block (core/loginout) does the same in the Site Editor.

wp_set_current_user( $id ) only changes who the current request runs as. It sets no cookie, creates no session and fires no wp_login: the tool for cron jobs, importers and tests that act as a user (WP-CLI’s --user does the same), not for logging someone in. On the test site, wp_set_current_user() with the ID of tom.author turned current_user_can( 'publish_posts' ) from false to true while wp_get_all_sessions() stayed empty.

To log a user in without a password (after an SSO callback or a verified magic link), code calls wp_set_current_user(), then wp_set_auth_cookie( $user_id, $remember ), then do_action( 'wp_login', $user->user_login, $user ) so audit and security plugins see the login. Such a path bypasses authentication completely if its own check is weak, so it needs expiring, single-use tokens and review.

Logging out. wp_logout() destroys the current session token, clears the cookies, sets the current user to 0 and fires wp_logout with the user ID (since 5.5). wp_logout_url( $redirect ) adds the log-out nonce that wp-login.php?action=logout checks; logout_redirect chooses the destination.

The authenticate filter

wp_authenticate( $username, $password ) sanitizes the username, trims the password and returns what the authenticate chain produces; an empty result becomes a generic authentication_failed error, and every error except an empty field fires wp_login_failed (username and WP_Error, the latter since 5.4). The default chain in 7.1.3 is wp_authenticate_username_password(), wp_authenticate_email_password() and wp_authenticate_application_password() (API requests only) at priority 20; wp_authenticate_cookie() at 30, added by wp_signon() only, which accepts an existing valid cookie when no credentials were posted; and wp_authenticate_spam_check() at 99, which refuses users marked as spam on Multisite.

Each callback receives the result so far (null, WP_User or WP_Error), the username and the password. A callback below 20 can replace password checking (an LDAP check returns a WP_User, which the core callbacks pass through); one above 20 sees the resolved user and can veto it. wp_authenticate_user is a narrower filter that runs only in the username and email paths, before the password is checked.

Suspending a reader

A user meta flag lp_suspended must block new logins, end existing sessions and stop the reader’s application passwords. The must-use plugin wp-content/mu-plugins/lumen-login.php also records each login (wp_login):

<?php
/**
 * Plugin Name: Lumen Login
 * Description: Suspended readers and last-login tracking for Lumen Press.
 * Version:     0.1.0
 * License:     GPL-2.0-or-later
 */

defined( 'ABSPATH' ) || exit;

/** Suspends a user: flags the account and ends every session it has. */
function lumen_suspend_user( int $user_id ): void {
	update_user_meta( $user_id, 'lp_suspended', 1 );
	WP_Session_Tokens::get_instance( $user_id )->destroy_all();
}

function lumen_is_suspended( WP_User $user ): bool {
	return (bool) get_user_meta( $user->ID, 'lp_suspended', true );
}

// Password logins: runs after the core handlers (priority 20) have resolved the user.
add_filter(
	'authenticate',
	static function ( $user ) {
		if ( $user instanceof WP_User && lumen_is_suspended( $user ) ) {
			return new WP_Error(
				'lp_suspended',
				__( '<strong>Error:</strong> This account is suspended. Contact the Lumen Press team.', 'lumen' )
			);
		}
		return $user;
	},
	30
);

// REST API requests with application passwords bypass the authenticate filter (XML-RPC logins do run it).
add_action(
	'wp_authenticate_application_password_errors',
	static function ( WP_Error $error, WP_User $user ) {
		if ( lumen_is_suspended( $user ) ) {
			$error->add( 'lp_suspended', __( 'This account is suspended.', 'lumen' ) );
		}
	},
	10,
	2
);

// Record the time of each successful login.
add_action(
	'wp_login',
	static function ( string $user_login, WP_User $user ) {
		update_user_meta( $user->ID, 'lp_last_login', time() );
	},
	10,
	2
);

The second hook matters: on API requests wp_validate_application_password() calls wp_authenticate_application_password() directly, so a check placed only on authenticate leaves application passwords working. Suspending the reader and retrying the reader’s application password:

wp user session list sam.reader --format=count
wp eval 'lumen_suspend_user( get_user_by( "login", "sam.reader" )->ID );'
wp user session list sam.reader --format=count
curl -s -u "sam.reader:$SAM_APP_PASSWORD" 'http://lumenpress.test/wp-json/wp/v2/users/me?_fields=id,name'
2
0
{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}

Before the suspension the request returned the reader’s ID and name; a password login on wp-login.php now shows the suspension message. The REST client gets the generic rest_not_logged_in, not lp_suspended: on the test site the current user was resolved lazily, after the REST server had checked authentication, so the failed application password just left the request anonymous. Clients must not rely on the specific code; what holds is that the request does not run as the suspended user.

Login and logout hooks

  • wp_authenticate (action): start of wp_signon(), with username and password by reference.

  • wp_login, wp_login_failed, wp_logout (actions): after a successful login (login name, WP_User), a failed one (username, WP_Error) or a logout (user ID).

  • login_redirect, logout_redirect, registration_redirect, lostpassword_redirect (filters): where to go next.

  • login_init and login_form_{$action} (actions): top of wp-login.php, and once per action (login, logout, lostpassword, retrievepassword, rp, resetpass, register, checkemail, postpass, confirmaction, confirm_admin_email, or a custom one).

  • login_enqueue_scripts, login_head, login_footer (actions): assets for the login screen, where wp_enqueue_scripts does not run.

  • login_headerurl, login_headertext, login_title, login_body_class (filters): logo link and text, title, classes.

  • login_message, login_errors, wp_login_errors, shake_error_codes (filters): messages, error output, and which error codes shake the form.

  • login_form, register_form, lostpassword_form, resetpass_form (actions): extra fields, such as a second-factor code. A field added there must be validated in an authenticate callback; printing it protects nothing.

  • login_display_language_dropdown (filter, 5.9): hides the language switcher.

On the test site, with lumen-login.php active, a login as sam.reader stored lp_last_login. A login_redirect callback returning home_url( '/books/' ) for users without edit_posts when no redirect_to was requested sent sam.reader to the catalogue instead of the profile, and add_filter( 'login_headerurl', static fn() => home_url( '/' ) ) pointed the logo at the home page.

Password hashing

Since 6.8, wp_hash_password() uses bcrypt through PHP’s password_hash(). Because bcrypt ignores everything after 72 bytes, WordPress first takes an HMAC-SHA384 of the password (key wp-sha384), base64-encodes it, hashes that and prefixes $wp: stored hashes start with $wp$2y$ and the cost ($wp$2y$12$ under PHP 8.4, whose default cost is 12). Passwords longer than 4096 characters are refused. wp_check_password( $password, $hash, $user_id ) accepts every format WordPress has stored:

Stored value Origin Checked by

$wp$2y$...

6.8 and later

password_verify() on the HMAC-SHA384 of the password

$P$...

phpass, 2.5 to 6.7

The bundled PasswordHash class

32 hexadecimal characters

MD5, before 2.5, or written by hand into the database

md5() comparison

$2y$..., $argon2id$...

Plain password_hash() (plugins, or the wp_hash_password_algorithm filter)

password_verify()

$generic$...

Application passwords and other random keys, 6.8 and later

wp_verify_fast_hash() (BLAKE2b), not wp_check_password()

wp_password_needs_rehash() (6.8) reports whether a hash is weaker than the current algorithm and options; wp_authenticate_username_password() then calls wp_set_password(), so old hashes are upgraded one login at a time:

<?php
require_once ABSPATH . WPINC . '/class-phpass.php';

$password = 'correct horse battery staple';
$hashes   = array(
	'bcrypt (6.8+)' => wp_hash_password( $password ),
	'phpass'        => ( new PasswordHash( 8, true ) )->HashPassword( $password ),
	'MD5'           => md5( $password ),
);
foreach ( $hashes as $label => $hash ) {
	WP_CLI::line( sprintf( '%-14s %-12s check=%-5s rehash=%s', $label, substr( $hash, 0, 10 ),
		var_export( wp_check_password( $password, $hash ), true ),
		var_export( wp_password_needs_rehash( $hash ), true ) ) );
}
WP_CLI::line( 'Wrong password: ' . var_export( wp_check_password( 'Correct horse battery staple', $hashes['bcrypt (6.8+)'] ), true ) );
bcrypt (6.8+)  $wp$2y$12$   check=true  rehash=false
phpass         $P$BZP0R.9   check=true  rehash=true
MD5            9cc2ae8a1b   check=true  rehash=true
Wrong password: false

The classic lock-out remedy, writing MD5('<temporary-password>') into user_pass, was tried on maria.editor: the login succeeded and the column then started with $wp$2y$12$. It works, but leaves a fast, unsalted hash until the next login; wp user update maria.editor --user_pass=<new-password> stores bcrypt directly. Extension points:

  • wp_hash_password_algorithm and wp_hash_password_options (6.8) choose algorithm and options; Security Hardening pins the cost and shows Argon2id.

  • check_password filters every check result, password_needs_rehash the rehash decision, and the wp_set_password action (6.2) fires after a password is stored.

  • A plugin that sets the global $wp_hasher (the pre-6.8 way to replace phpass) still wins over bcrypt, and wp_password_needs_rehash() then always returns false; such plugins are worth removing.

Application passwords from code

Application passwords (5.6) are per-user, revocable credentials for HTTP Basic authentication on the REST API and XML-RPC; they never work on wp-login.php. They are stored in the _application_passwords user meta as items with uuid, app_id, name, password, created, last_used and last_ip; since 6.8 password holds a BLAKE2b hash from wp_fast_hash(), marked $generic$. Availability rules are in Users, Roles and Profiles. Provisioning code uses WP_Application_Passwords:

<?php
$user   = get_user_by( 'login', 'tom.author' );
$result = WP_Application_Passwords::create_new_application_password(
	$user->ID,
	array(
		'name'   => 'Lumen reader app',
		'app_id' => '9a1d0c55-3f6e-4b7a-8d2e-6c0f4b1e2a77',
	)
);
if ( is_wp_error( $result ) ) {
	WP_CLI::error( $result );
}
list( $password, $item ) = $result;

WP_CLI::line( 'Password: ' . WP_Application_Passwords::chunk_password( $password ) );
WP_CLI::line( 'UUID:     ' . $item['uuid'] );
WP_CLI::line( 'Stored:   ' . substr( $item['password'], 0, 9 ) . '...' );
Password: <24-character application password>
UUID:     75673a15-dca5-489a-b0bc-dc2e38fe216c
Stored:   $generic$...

The plain password is returned only once, as the first array element; a missing name gives a WP_Error with status 400. On the test site, curl -u "tom.author:$TOM_APP_PASSWORD" against /wp-json/wp/v2/users/me?context=edit returned tom.author with the author role; see REST API: Using It for the request side. Posted to wp-login.php, the same password was rejected as incorrect. The other static methods are get_user_application_passwords(), get_user_application_password(), update_application_password(), record_application_password_usage() (writes at most once a day), delete_application_password() and delete_all_application_passwords(); they fire wp_create_application_password, wp_update_application_password and wp_delete_application_password. Authentication fires application_password_did_authenticate or application_password_failed_authentication, and application_password_is_api_request extends the API-only rule. WP-CLI wraps the class as wp user application-password.

An external app should not ask users to paste a password: it sends them to /wp-admin/authorize-application.php?app_name=...&app_id=<uuid>&success_url=... (advertised in the REST index under authentication), and after approval WordPress redirects to success_url with site_url, user_login and password. A plain-HTTP success_url is refused unless the host is a loopback address or the environment type is local; custom app schemes are allowed, javascript: and data: are not.

Sessions

Every login creates a session in a WP_Session_Tokens store. The default WP_User_Meta_Session_Tokens keeps the session_tokens user meta, keyed by the SHA-256 of each token, with the session’s expiration, ip, ua and login time; expired sessions are filtered out whenever the store is read. attach_session_information adds fields, and session_token_manager swaps in another class (for example one backed by Redis). The cookie carries the raw token, the database only its hash:

<?php
$manager = WP_Session_Tokens::get_instance( get_user_by( 'login', 'tom.author' )->ID );

$laptop = $manager->create( time() + 2 * DAY_IN_SECONDS );  // Returns the raw token.
$phone  = $manager->create( time() + 14 * DAY_IN_SECONDS );

WP_CLI::line( get_class( $manager ) . ', sessions: ' . count( $manager->get_all() ) );
WP_CLI::line( 'Stored keys: ' . implode( ', ', array_keys( $manager->get( $laptop ) ) ) );

$manager->destroy_others( $laptop ); // "Log Out Everywhere Else" from the laptop.
WP_CLI::line( 'After destroy_others: ' . count( $manager->get_all() ) . ', phone valid: ' . var_export( $manager->verify( $phone ), true ) );

$manager->destroy_all();
WP_CLI::line( 'After destroy_all: ' . count( $manager->get_all() ) );
WP_User_Meta_Session_Tokens, sessions: 4
Stored keys: expiration, ip, ua, login
After destroy_others: 1, phone valid: false
After destroy_all: 0

The count is four because tom.author already had two sessions (the curl login and the cookie example). For the current user, wp_get_session_token(), wp_get_all_sessions(), wp_destroy_current_session(), wp_destroy_other_sessions() and wp_destroy_all_sessions() wrap the same methods, and WP_Session_Tokens::destroy_all_for_all_users() logs everybody out. A password change leaves stored sessions in place, but their cookies stop validating. From the shell: wp user session destroy <user> --all.

Single sign-on integration points

Core knows only passwords, cookies and application passwords. OpenID Connect, OAuth 2.0 or SAML sign-on comes from plugins, such as OpenID Connect Generic Client on WordPress.org, or from commercial identity suites, which hook into the places described above:

Integration point What an SSO plugin does there

login_form, login_init

Adds a "Sign in with …​" button, or redirects wp-login.php to the identity provider

A callback (admin_post_nopriv_{$action}, a REST route or a rewrite rule)

Receives the authorization code, exchanges it and validates the ID token

wp_insert_user() and user meta

Creates or links the account on first login and maps claims to roles

wp_set_current_user(), wp_set_auth_cookie(), wp_login

Starts a normal WordPress session, so nothing else changes

authenticate below priority 20

Checks the normal form’s credentials against an external directory (LDAP)

determine_current_user

Accepts a bearer token (OAuth access token or JWT) on REST requests instead of a cookie

wp_logout, logout_redirect

Ends the provider session too (RP-initiated logout)

WordPress cookies outlive the provider’s session unless the plugin shortens auth_cookie_expiration, and local passwords and application passwords keep working beside SSO unless the plugin disables them. The protocols are explained in OpenID Connect, Social Login and Federation and Logout and Session Management.

Errors and pitfalls

Symptom Cause and fix

"Cookies are blocked or not supported by your browser"

No wordpress_test_cookie, or the cookie domain or path does not match the URL used (COOKIE_DOMAIN, siteurl)

Logged out again on the next page

HTTPS terminated at a proxy without HTTPS set, so the cookie scheme is wrong; see Web Servers: Apache and nginx

A login check is bypassed by API clients

It is only on authenticate or wp_authenticate_user; add wp_authenticate_application_password_errors

One user’s page served to another

The page cache ignores wordpress_logged_in_* cookies; see Performance and Caching

Where older books are outdated

Older claim Current behaviour (WordPress 7.1)

Passwords are stored as phpass (MD5-based) hashes

bcrypt since 6.8 ($wp$2y$ after an HMAC-SHA384 pre-hash); old hashes are upgraded at the next login

Reset a lost password by writing an MD5 hash with phpMyAdmin

Accepted once and rehashed, but wp user update --user_pass is the clean route

WP_ALLOW_REPAIR helps recover a lost password

It only repairs tables, and exposes the repair page without login

Core offers two-factor authentication

It does not; it comes from a plugin such as Two Factor, or from SSO

Log in with wp_login() or wp_setcookie(); read the user with get_currentuserinfo()

Deprecated since 2.5 and 4.5; use wp_signon(), wp_set_auth_cookie() and wp_get_current_user()

Remote apps need the user’s real password or a Basic Auth plugin

Application passwords are built in since 5.6, hashed with BLAKE2b since 6.8

More corrections are collected in Where Older Books and Tutorials Are Outdated.