fix(oauth): treat missing expires_in as non-expiring device_code token

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).
This commit is contained in:
2026-07-21 10:32:47 -06:00
parent d407eb5a6a
commit 82bf6176f8
+4 -4
View File
@@ -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() { 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 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(|| { let expires_at = match token_response["expires_in"].as_i64() {
anyhow!("Missing expires_in in device_code token response: {token_response}") Some(secs) => Utc::now().timestamp() + secs,
})?; None => i64::MAX,
let expires_at = Utc::now().timestamp() + expires_in_secs; };
let account_id = provider.extract_account_id(&token_response); let account_id = provider.extract_account_id(&token_response);
let tokens = OAuthTokens { let tokens = OAuthTokens {