Skip to content

Commit 620e8f3

Browse files
moirahuangoz-agent
andauthored
TUI: Restore authentication after /logout (#14568)
## Description Restores a complete authentication path after `/logout` in the headless TUI. - Shows an explicit signed-out welcome state and starts device authorization only after user input. - Starts fresh device authorization after local logout, then opens a validated `/logout?continue=…` browser URL so the prior web session is cleared first. - Keeps the exact browser URL visible as a manual fallback and returns to the authenticated TUI without requiring a restart. - Validates the device continuation as same-origin, exact-path, and CLI-sourced before passing it to the browser. This is the client half of the auth-return handshake; the browser implementation is in warpdotdev/warp-server#13620. ## Linked Issue None. - [ ] The linked issue is labeled `ready-to-spec` or `ready-to-implement`. - [x] Manual end-to-end verification is documented below. ## Testing https://www.loom.com/share/2c6d09cb0d0c41658e5a97514f8f347b - [x] `./script/format --check` - [x] `./script/check_no_inline_test_modules` - [x] Presubmit workspace Clippy, default `warp` Clippy, and `warp_completer` Clippy with warnings denied - [x] `cargo nextest run -p warp_tui` (906/906 tests passed) - [x] Full terminal verification: signed-out welcome, unrelated input ignored, Enter starts device auth, wrapped logout continuation opens, browser completion returns to the authenticated TUI - [ ] I have manually tested my changes locally with `./script/run` The changed surface is the headless TUI, so manual verification used the real TUI in a full terminal rather than the GUI `./script/run` path. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode Co-Authored-By: Oz [oz-agent@warp.dev](mailto:oz-agent@warp.dev) --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent d9ed472 commit 620e8f3

8 files changed

Lines changed: 1179 additions & 191 deletions

File tree

app/src/auth/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub use auth_manager::AuthManager;
2020
pub use auth_state::AuthStateProvider;
2121
use itertools::Itertools;
2222
pub use login_failure_notification::LoginFailureReason;
23+
#[cfg(feature = "tui")]
24+
use url::Url;
2325
pub use user_uid::UserUid;
2426
use warp_core::channel::ChannelState;
2527
use warp_core::user_preferences::GetUserPreferences as _;
@@ -76,6 +78,37 @@ pub fn web_logout_url() -> String {
7678
)
7779
}
7880

81+
/// Returns the configured Warp web logout URL with a validated browser continuation.
82+
///
83+
/// TUI logout only continues to the same Warp web origin's device page. This
84+
/// keeps the logout endpoint from becoming an open redirect if an unexpected
85+
/// device-authorization response reaches the client.
86+
#[cfg(feature = "tui")]
87+
pub fn web_logout_url_with_continue(continue_url: &str) -> Option<String> {
88+
let mut logout_url =
89+
Url::parse(&web_logout_url()).expect("configured Warp web logout URL must be valid");
90+
let continue_url = Url::parse(continue_url).ok()?;
91+
let has_required_query = continue_url
92+
.query_pairs()
93+
.any(|(key, value)| key == "user_code" && !value.is_empty())
94+
&& continue_url
95+
.query_pairs()
96+
.any(|(key, value)| key == "source" && value == "warp-agent-cli");
97+
if continue_url.origin() != logout_url.origin()
98+
|| continue_url.path() != "/device"
99+
|| !continue_url.username().is_empty()
100+
|| continue_url.password().is_some()
101+
|| continue_url.fragment().is_some()
102+
|| !has_required_query
103+
{
104+
return None;
105+
}
106+
logout_url
107+
.query_pairs_mut()
108+
.append_pair("continue", continue_url.as_str());
109+
Some(logout_url.into())
110+
}
111+
79112
/// If the app has running processes or dirty objects, we'll show a confirmation modal before logging out.
80113
/// If the user aborts, the user will not be logged out.
81114
pub fn maybe_log_out(app: &mut AppContext) {

app/src/auth/mod_tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use warp_core::channel::ChannelState;
22

33
use super::web_logout_url;
4+
#[cfg(feature = "tui")]
5+
use super::web_logout_url_with_continue;
46

57
#[test]
68
fn web_logout_url_uses_configured_server_root() {
@@ -10,3 +12,46 @@ fn web_logout_url_uses_configured_server_root() {
1012
format!("{}/logout", server_root_url.trim_end_matches('/'))
1113
);
1214
}
15+
16+
#[test]
17+
#[cfg(feature = "tui")]
18+
fn web_logout_url_rejects_unsafe_device_auth_continuations() {
19+
let server_root_url = ChannelState::server_root_url();
20+
for continue_url in [
21+
"https://example.com/device?user_code=ABCD-EFGH&source=warp-agent-cli".to_owned(),
22+
format!(
23+
"{}/login?user_code=ABCD-EFGH&source=warp-agent-cli",
24+
server_root_url.trim_end_matches('/')
25+
),
26+
format!(
27+
"{}/device?source=warp-agent-cli",
28+
server_root_url.trim_end_matches('/')
29+
),
30+
format!(
31+
"{}/device?user_code=ABCD-EFGH",
32+
server_root_url.trim_end_matches('/')
33+
),
34+
] {
35+
assert_eq!(web_logout_url_with_continue(&continue_url), None);
36+
}
37+
}
38+
39+
#[test]
40+
#[cfg(feature = "tui")]
41+
fn web_logout_url_encodes_device_auth_continuation() {
42+
let continue_url = format!(
43+
"{}/device?user_code=ABCD-EFGH&source=warp-agent-cli",
44+
ChannelState::server_root_url().trim_end_matches('/')
45+
);
46+
let logout_url =
47+
url::Url::parse(&web_logout_url_with_continue(&continue_url).unwrap()).unwrap();
48+
49+
assert_eq!(logout_url.path(), "/logout");
50+
assert_eq!(
51+
logout_url
52+
.query_pairs()
53+
.find(|(key, _)| key == "continue")
54+
.map(|(_, value)| value.into_owned()),
55+
Some(continue_url)
56+
);
57+
}

0 commit comments

Comments
 (0)