feat(oauth): enable browser-paste PKCE flow for OpenAI-compatible providers
Two coordinated changes that make openai-compatible OAuth providers usable with a non-localhost redirect_uri (browser shows the callback URL, user copies it back into the terminal — the same UX Claude uses). Fix: OpenAICompatibleOAuthProvider::fixed_redirect_uri() previously returned Some(uri) for any redirect_uri including public HTTPS URLs, which trapped run_pkce_flow into trying to bind a TCP listener on a public URL. It now returns Some only for loopback URIs (127.0.0.1, localhost, ::1). Non-loopback URIs return None, routing run_pkce_flow to the paste branch. New tri-format paste parser (parse_paste_input): - Full callback URL (starts with http:// or https://): parse code + state from the query string. This is what most modern OAuth providers redirect to and what a naive user copies from the browser bar. - Anthropic-style code#state fragment: preserved for Claude compatibility. - Bare code: accepted with a warning that CSRF state validation is skipped. For providers whose callback page shows only the code with no state. State validation moved from mandatory to conditional — if a paste didn't carry state (bare-code path), we warn and skip the check instead of hard- failing. The listener path (localhost + LAN redirects) still requires state because the server sends it in the query. Adds 9 unit tests covering both changes.
This commit is contained in:
@@ -5,6 +5,13 @@ pub struct OpenAICompatibleOAuthProvider {
|
||||
pub client_name: String,
|
||||
}
|
||||
|
||||
fn is_loopback_uri(uri: &str) -> bool {
|
||||
url::Url::parse(uri)
|
||||
.ok()
|
||||
.and_then(|u| u.host_str().map(str::to_string))
|
||||
.is_some_and(|host| matches!(host.as_str(), "127.0.0.1" | "localhost" | "[::1]" | "::1"))
|
||||
}
|
||||
|
||||
impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
||||
fn provider_name(&self) -> &str {
|
||||
&self.client_name
|
||||
@@ -70,7 +77,11 @@ impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
||||
|
||||
fn fixed_redirect_uri(&self) -> Option<String> {
|
||||
if let Some(uri) = &self.config.redirect_uri {
|
||||
return Some(uri.clone());
|
||||
return if is_loopback_uri(uri) {
|
||||
Some(uri.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if let Some(port) = self.config.redirect_port {
|
||||
return Some(format!("http://127.0.0.1:{port}/callback"));
|
||||
|
||||
Reference in New Issue
Block a user