Skip to content

Commit f097c13

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 f097c13

1 file changed

Lines changed: 88 additions & 73 deletions

File tree

plugin/wp-cli-login-server.php

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

29+
/**
30+
* Complete the magic login handoff on wp-login.php.
31+
*
32+
* CDN/proxy layers (e.g. Cloudflare with "Cache Everything") may strip
33+
* Set-Cookie headers from responses on arbitrary URLs. By deferring the
34+
* cookie-setting to wp-login.php — which CDNs universally exclude from
35+
* caching — the auth cookies are guaranteed to reach the browser.
36+
*/
37+
function handle_login_handoff()
38+
{
39+
if (empty($_GET['wp_cli_login_token'])) {
40+
return;
41+
}
42+
43+
$token = sanitize_text_field($_GET['wp_cli_login_token']);
44+
$data = get_transient('wp_cli_login/handoff/' . $token);
45+
46+
if (! $data) {
47+
wp_die(
48+
'<strong>The magic login handoff has expired or already been used.</strong>'
49+
. sprintf('<p>Try again perhaps? or <a href="%s">Go Home &rarr;</a></p>', esc_url(home_url())),
50+
'Login Failed',
51+
['response' => 410]
52+
);
53+
}
54+
55+
delete_transient('wp_cli_login/handoff/' . $token);
56+
57+
$user = new \WP_User($data['user_id']);
58+
59+
if (! $user->exists()) {
60+
wp_die(
61+
'<strong>No user found or no longer exists.</strong>'
62+
. sprintf('<p><a href="%s">Go Home &rarr;</a></p>', esc_url(home_url())),
63+
'Login Failed',
64+
['response' => 410]
65+
);
66+
}
67+
68+
nocache_headers();
69+
70+
wp_set_auth_cookie($user->ID);
71+
72+
/** @see WP_CLI_Login_Server::loginUser() */
73+
do_action('wp_cli_login/login', $user->user_login, $user);
74+
do_action('wp_login', $user->user_login, $user);
75+
76+
$redirect_to = ! empty($data['redirect_url']) ? $data['redirect_url'] : admin_url();
77+
$redirect_to = apply_filters('login_redirect', $redirect_to, '', $user);
78+
$redirect_to = apply_filters('wp_cli_login/login_redirect', $redirect_to, '', $user);
79+
80+
if ((empty($redirect_to) || $redirect_to == 'wp-admin/' || $redirect_to == admin_url())) {
81+
if (is_multisite() && ! get_active_blog_for_user($user->ID) && ! is_super_admin($user->ID)) {
82+
$redirect_to = user_admin_url();
83+
} elseif (is_multisite() && ! $user->has_cap('read')) {
84+
$redirect_to = get_dashboard_url($user->ID);
85+
} elseif (! $user->has_cap('edit_posts')) {
86+
$redirect_to = $user->has_cap('read') ? admin_url('profile.php') : home_url();
87+
}
88+
89+
wp_redirect($redirect_to);
90+
exit;
91+
}
92+
93+
wp_safe_redirect($redirect_to);
94+
exit;
95+
}
96+
add_action('login_init', __NAMESPACE__ . '\\handle_login_handoff');
97+
2998
/**
3099
* @return bool
31100
*/
@@ -133,8 +202,8 @@ public function run()
133202
try {
134203
$magic = $this->loadMagic();
135204
$user = $this->validate($magic);
136-
$this->loginUser($user);
137-
$this->loginRedirect($user, $magic->redirect_url);
205+
$this->deleteMagic();
206+
$this->handoffRedirect($user, $magic->redirect_url);
138207
} catch (Exception $e) {
139208
$this->deleteMagic();
140209
$this->abort($e);
@@ -199,84 +268,30 @@ private function deleteMagic()
199268
}
200269

201270
/**
202-
* Login the given user and redirect them to wp-admin.
271+
* Create a short-lived handoff token and redirect to wp-login.php.
203272
*
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
273+
* CDN/proxy layers (e.g. Cloudflare with "Cache Everything") may strip
274+
* Set-Cookie headers from responses on arbitrary URLs. By redirecting to
275+
* wp-login.php — which CDNs universally exclude from caching — the auth
276+
* cookies are set on a response the browser is guaranteed to receive intact.
233277
*
234278
* @param WP_User $user
235279
* @param string $redirect_url
236280
*/
237-
private function loginRedirect(WP_User $user, $redirect_url)
281+
private function handoffRedirect(WP_User $user, $redirect_url)
238282
{
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-
}
283+
$token = bin2hex(random_bytes(16));
284+
285+
set_transient('wp_cli_login/handoff/' . $token, [
286+
'user_id' => $user->ID,
287+
'redirect_url' => $redirect_url,
288+
], 30);
275289

276-
/**
277-
* Redirect safely to the URL provided.
278-
*/
279-
wp_safe_redirect($redirect_to);
290+
nocache_headers();
291+
292+
wp_redirect(
293+
add_query_arg('wp_cli_login_token', $token, wp_login_url())
294+
);
280295
exit;
281296
}
282297

0 commit comments

Comments
 (0)