From 82bf6176f8423020c1a13c8f274034a465b7fb12 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 21 Jul 2026 10:32:47 -0600 Subject: [PATCH] fix(oauth): treat missing expires_in as non-expiring device_code token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub OAuth Apps issue tokens that never expire and omit expires_in from the response (they only send access_token, token_type, scope). RFC 6749 §5.1 allows this — expires_in is only REQUIRED for tokens that actually expire. When expires_in is missing, save the token with expires_at = i64::MAX so prepare_oauth_access_token never tries to refresh. If the token is ever revoked server-side, the eventual 401 on the API call is the user's cue to re-authenticate. No effect on providers that include expires_in (Moonshot etc. — unchanged). --- src/client/oauth.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/oauth.rs b/src/client/oauth.rs index 69dabc0..be25a5c 100644 --- a/src/client/oauth.rs +++ b/src/client/oauth.rs @@ -479,10 +479,10 @@ async fn run_device_code_flow(provider: &dyn OAuthProvider, client_name: &str) - if let Some(access_token) = token_response["access_token"].as_str() { let refresh_token = token_response["refresh_token"].as_str().map(str::to_string); - let expires_in_secs = token_response["expires_in"].as_i64().ok_or_else(|| { - anyhow!("Missing expires_in in device_code token response: {token_response}") - })?; - let expires_at = Utc::now().timestamp() + expires_in_secs; + let expires_at = match token_response["expires_in"].as_i64() { + Some(secs) => Utc::now().timestamp() + secs, + None => i64::MAX, + }; let account_id = provider.extract_account_id(&token_response); let tokens = OAuthTokens {