mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
fix(oauth): 兼容 Kiro device token 的 snake_case 字段并保留 error 响应体
- device poll 同时接受 accessToken/access_token、refreshToken/refresh_token、expiresIn/expires_in - post_kiro_device_oidc_json 在解析失败响应时保留完整 JSON 并附加 _error 标记, 避免丢失 authorization_pending 等详细信息 - 补充 authorization_pending 时 device session 保持 pending 状态的测试
This commit is contained in:
@@ -217,7 +217,11 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
|
|||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(access_token) = json_non_empty_string(token_result.get("accessToken")) else {
|
let Some(access_token) = json_non_empty_string(
|
||||||
|
token_result
|
||||||
|
.get("accessToken")
|
||||||
|
.or_else(|| token_result.get("access_token")),
|
||||||
|
) else {
|
||||||
return Ok(Json(json!({
|
return Ok(Json(json!({
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "token 响应缺少 accessToken 或 refreshToken",
|
"error": "token 响应缺少 accessToken 或 refreshToken",
|
||||||
@@ -225,7 +229,11 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
|
|||||||
}))
|
}))
|
||||||
.into_response());
|
.into_response());
|
||||||
};
|
};
|
||||||
let Some(refresh_token) = json_non_empty_string(token_result.get("refreshToken")) else {
|
let Some(refresh_token) = json_non_empty_string(
|
||||||
|
token_result
|
||||||
|
.get("refreshToken")
|
||||||
|
.or_else(|| token_result.get("refresh_token")),
|
||||||
|
) else {
|
||||||
return Ok(Json(json!({
|
return Ok(Json(json!({
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "token 响应缺少 accessToken 或 refreshToken",
|
"error": "token 响应缺少 accessToken 或 refreshToken",
|
||||||
@@ -233,9 +241,13 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
|
|||||||
}))
|
}))
|
||||||
.into_response());
|
.into_response());
|
||||||
};
|
};
|
||||||
let initial_expires_at = json_u64_value(token_result.get("expiresIn"))
|
let initial_expires_at = json_u64_value(
|
||||||
.map(|expires_in| current_unix_secs().saturating_add(expires_in))
|
token_result
|
||||||
.unwrap_or_else(|| current_unix_secs().saturating_add(3600));
|
.get("expiresIn")
|
||||||
|
.or_else(|| token_result.get("expires_in")),
|
||||||
|
)
|
||||||
|
.map(|expires_in| current_unix_secs().saturating_add(expires_in))
|
||||||
|
.unwrap_or_else(|| current_unix_secs().saturating_add(3600));
|
||||||
let social_refresh_base_url =
|
let social_refresh_base_url =
|
||||||
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_social_refresh");
|
admin_provider_oauth_kiro_refresh_base_url_override(state, "kiro_social_refresh");
|
||||||
let idc_refresh_base_url =
|
let idc_refresh_base_url =
|
||||||
|
|||||||
@@ -578,12 +578,25 @@ async fn post_kiro_device_oidc_json(
|
|||||||
let status = response.status;
|
let status = response.status;
|
||||||
let body_text = response.body_text;
|
let body_text = response.body_text;
|
||||||
Ok(
|
Ok(
|
||||||
serde_json::from_str::<serde_json::Value>(&body_text).unwrap_or_else(|_| {
|
match serde_json::from_str::<serde_json::Value>(&body_text) {
|
||||||
json!({
|
Ok(mut payload) => {
|
||||||
|
if !status.is_success() {
|
||||||
|
if let Some(object) = payload.as_object_mut() {
|
||||||
|
object.insert("_error".to_string(), json!(true));
|
||||||
|
} else {
|
||||||
|
payload = json!({
|
||||||
|
"_error": true,
|
||||||
|
"data": payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload
|
||||||
|
}
|
||||||
|
Err(_) => json!({
|
||||||
"_error": !status.is_success(),
|
"_error": !status.is_success(),
|
||||||
"error": body_text.trim(),
|
"error": body_text.trim(),
|
||||||
})
|
}),
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -444,6 +444,86 @@ async fn gateway_handles_admin_provider_oauth_device_poll_locally_with_trusted_a
|
|||||||
upstream_handle.abort();
|
upstream_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gateway_keeps_admin_provider_oauth_device_poll_pending_for_authorization_pending_error() {
|
||||||
|
let token_server = Router::new().route(
|
||||||
|
"/token",
|
||||||
|
post(move |_request: Request| async move {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(json!({
|
||||||
|
"error": "authorization_pending",
|
||||||
|
"error_description": "waiting for user confirmation",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut provider = sample_provider("provider-kiro", "kiro", 10);
|
||||||
|
provider.provider_type = "kiro".to_string();
|
||||||
|
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![provider],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
|
||||||
|
let (token_url, token_handle) = start_server(token_server).await;
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||||
|
provider_catalog_repository,
|
||||||
|
))
|
||||||
|
.with_provider_oauth_device_session_entry_for_tests(
|
||||||
|
"session-pending",
|
||||||
|
json!({
|
||||||
|
"provider_id": "provider-kiro",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"client_id": "kiro-device-client",
|
||||||
|
"client_secret": "kiro-device-secret",
|
||||||
|
"device_code": "device-code-123",
|
||||||
|
"interval": 5,
|
||||||
|
"expires_at_unix_secs": 4_102_444_800u64,
|
||||||
|
"status": "pending",
|
||||||
|
"proxy_node_id": null,
|
||||||
|
"created_at_unix_ms": 1_711_000_000u64,
|
||||||
|
"key_id": null,
|
||||||
|
"email": null,
|
||||||
|
"replaced": false,
|
||||||
|
"error_msg": null,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_provider_oauth_token_url_for_tests("kiro_device_poll", format!("{token_url}/token"));
|
||||||
|
|
||||||
|
let response = local_admin_provider_oauth_response(
|
||||||
|
&state,
|
||||||
|
http::Method::POST,
|
||||||
|
"/api/admin/provider-oauth/providers/provider-kiro/device-poll",
|
||||||
|
Some(json!({ "session_id": "session-pending" })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert!(
|
||||||
|
response.extensions().get::<AdminAuditEvent>().is_none(),
|
||||||
|
"pending state should not attach terminal audit"
|
||||||
|
);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.expect("body should read");
|
||||||
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||||
|
assert_eq!(payload["status"], "pending");
|
||||||
|
assert_eq!(payload["replaced"], false);
|
||||||
|
|
||||||
|
let stored = state
|
||||||
|
.load_provider_oauth_device_session_for_tests("device_auth_session:session-pending")
|
||||||
|
.expect("device session should persist");
|
||||||
|
let stored: serde_json::Value =
|
||||||
|
serde_json::from_str(&stored).expect("device session json should parse");
|
||||||
|
assert_eq!(stored["status"], "pending");
|
||||||
|
assert_eq!(stored["error_msg"], serde_json::Value::Null);
|
||||||
|
|
||||||
|
token_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_revalidates_kiro_device_poll_via_idc_refresh_and_backfills_email() {
|
async fn gateway_revalidates_kiro_device_poll_via_idc_refresh_and_backfills_email() {
|
||||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||||
|
|||||||
Reference in New Issue
Block a user