Skip to content

Commit f4be744

Browse files
bourafaiclaude
andcommitted
Fix magic login behind CDNs that strip Set-Cookie headers
CDNs configured to cache aggressively (e.g. Cloudflare with "Cache Everything") strip Set-Cookie headers from the magic login response, preventing users from being authenticated. Use a two-step handoff: the magic URL now creates a short-lived transient and redirects to wp-login.php, which CDNs universally exclude from caching. The auth cookies are set there instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6aabb74 commit f4be744

File tree

1 file changed

+42
-74
lines changed

1 file changed

+42
-74
lines changed

plugin/wp-cli-login-server.php

Lines changed: 42 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,36 @@ function init_server_from_request()
2626
add_action('plugins_loaded', __NAMESPACE__ . '\\init_server_from_request');
2727
}
2828

29+
/**
30+
* Handle the second leg of the magic login on wp-login.php.
31+
*
32+
* CDN/proxy layers (e.g. Cloudflare with "Cache Everything") strip Set-Cookie
33+
* headers from responses on arbitrary URLs. wp-login.php is universally excluded
34+
* from CDN caching, so setting cookies here guarantees they reach the browser.
35+
*/
36+
function handle_login_handoff()
37+
{
38+
if (empty($_GET['wp_cli_login_token'])) {
39+
return;
40+
}
41+
42+
$token = sanitize_text_field($_GET['wp_cli_login_token']);
43+
$data = get_transient('wp_cli_login/handoff/' . $token);
44+
delete_transient('wp_cli_login/handoff/' . $token);
45+
46+
if (! $data || ! ($user = new \WP_User($data['user_id'])) || ! $user->exists()) {
47+
wp_die('The magic login handoff has expired or is invalid.', '', ['response' => 410]);
48+
}
49+
50+
wp_set_auth_cookie($user->ID);
51+
do_action('wp_cli_login/login', $user->user_login, $user);
52+
do_action('wp_login', $user->user_login, $user);
53+
54+
wp_safe_redirect($data['redirect_url'] ?: admin_url());
55+
exit;
56+
}
57+
add_action('login_init', __NAMESPACE__ . '\\handle_login_handoff');
58+
2959
/**
3060
* @return bool
3161
*/
@@ -133,8 +163,8 @@ public function run()
133163
try {
134164
$magic = $this->loadMagic();
135165
$user = $this->validate($magic);
136-
$this->loginUser($user);
137-
$this->loginRedirect($user, $magic->redirect_url);
166+
$this->deleteMagic();
167+
$this->handoffToLogin($user, $magic->redirect_url);
138168
} catch (Exception $e) {
139169
$this->deleteMagic();
140170
$this->abort($e);
@@ -199,84 +229,22 @@ private function deleteMagic()
199229
}
200230

201231
/**
202-
* Login the given user and redirect them to wp-admin.
203-
*
204-
* @param WP_User $user
205-
*/
206-
private function loginUser(WP_User $user)
207-
{
208-
$this->deleteMagic();
209-
210-
wp_set_auth_cookie($user->ID);
211-
212-
/**
213-
* Fires after the user has successfully logged in via the WP-CLI Login Server.
214-
*
215-
* @param string $user_login Username.
216-
* @param WP_User $user WP_User object of the logged-in user.
217-
*/
218-
do_action('wp_cli_login/login', $user->user_login, $user);
219-
220-
/**
221-
* Fires after the user has successfully logged in.
222-
*
223-
* @param string $user_login Username.
224-
* @param WP_User $user WP_User object of the logged-in user.
225-
*/
226-
do_action('wp_login', $user->user_login, $user);
227-
}
228-
229-
/**
230-
* Redirect the user after logging in.
231-
*
232-
* Mostly copied from wp-login.php
232+
* Hand off to wp-login.php via a short-lived token so that auth cookies
233+
* are set on a response that CDNs won't strip Set-Cookie headers from.
233234
*
234235
* @param WP_User $user
235236
* @param string $redirect_url
236237
*/
237-
private function loginRedirect(WP_User $user, $redirect_url)
238+
private function handoffToLogin(WP_User $user, $redirect_url)
238239
{
239-
$redirect_to = $redirect_url ?: admin_url();
240-
241-
/**
242-
* Filters the login redirect URL.
243-
*
244-
* @param string $redirect_to The redirect destination URL.
245-
* @param string $requested_redirect_to The requested redirect destination URL passed as a parameter.
246-
* @param WP_User $user WP_User object.
247-
*/
248-
$redirect_to = apply_filters('login_redirect', $redirect_to, '', $user);
249-
250-
/**
251-
* Filters the login redirect URL for WP-CLI Login Server requests.
252-
*
253-
* @param string $redirect_to The redirect destination URL.
254-
* @param string $requested_redirect_to The requested redirect destination URL passed as a parameter.
255-
* @param WP_User $user WP_User object.
256-
*/
257-
$redirect_to = apply_filters('wp_cli_login/login_redirect', $redirect_to, '', $user);
258-
259-
/**
260-
* Figure out where to redirect the user for the default wp-admin URL based on the user's capabilities.
261-
*/
262-
if ((empty($redirect_to) || $redirect_to == 'wp-admin/' || $redirect_to == admin_url())) {
263-
// If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile.
264-
if (is_multisite() && ! get_active_blog_for_user($user->ID) && ! is_super_admin($user->ID)) {
265-
$redirect_to = user_admin_url();
266-
} elseif (is_multisite() && ! $user->has_cap('read')) {
267-
$redirect_to = get_dashboard_url($user->ID);
268-
} elseif (! $user->has_cap('edit_posts')) {
269-
$redirect_to = $user->has_cap('read') ? admin_url('profile.php') : home_url();
270-
}
271-
272-
wp_redirect($redirect_to);
273-
exit;
274-
}
240+
$token = bin2hex(random_bytes(16));
241+
242+
set_transient('wp_cli_login/handoff/' . $token, [
243+
'user_id' => $user->ID,
244+
'redirect_url' => $redirect_url,
245+
], 30);
275246

276-
/**
277-
* Redirect safely to the URL provided.
278-
*/
279-
wp_safe_redirect($redirect_to);
247+
wp_redirect(add_query_arg('wp_cli_login_token', $token, wp_login_url()));
280248
exit;
281249
}
282250

0 commit comments

Comments
 (0)