mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
fix: preserve provider auth metadata
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use super::super::helpers::admin_provider_oauth_key_name_from_auth_config;
|
||||
use super::super::token_import::{
|
||||
build_provider_access_token_import_auth_config, provider_type_supports_access_token_import,
|
||||
build_provider_access_token_import_auth_config, decode_access_token_expires_at,
|
||||
provider_oauth_import_authorization_bearer_token, provider_type_supports_access_token_import,
|
||||
};
|
||||
use super::kiro_import::execute_admin_provider_oauth_kiro_batch_import;
|
||||
use super::parse::{
|
||||
@@ -394,9 +395,22 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
|
||||
let AdminProviderOAuthResolvedBatchImport {
|
||||
access_token,
|
||||
mut auth_config,
|
||||
expires_at,
|
||||
mut expires_at,
|
||||
} = resolved_import;
|
||||
apply_admin_provider_oauth_batch_import_hints(provider_type, entry, &mut auth_config);
|
||||
if let Some(header_access_token) =
|
||||
provider_oauth_import_authorization_bearer_token(entry.request_headers.as_ref())
|
||||
{
|
||||
if let Some(header_expires_at) =
|
||||
decode_access_token_expires_at(&header_access_token).or(entry.expires_at)
|
||||
{
|
||||
auth_config.insert("expires_at".to_string(), json!(header_expires_at));
|
||||
expires_at = Some(header_expires_at);
|
||||
} else {
|
||||
auth_config.remove("expires_at");
|
||||
expires_at = None;
|
||||
}
|
||||
}
|
||||
|
||||
let duplicate =
|
||||
match find_duplicate_provider_oauth_key(state, provider_id, &auth_config, None).await {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use super::super::token_import::{import_tokens_from_raw_token, normalize_provider_import_tokens};
|
||||
use super::super::token_import::{
|
||||
import_tokens_from_raw_token, normalize_provider_import_tokens,
|
||||
normalize_provider_oauth_import_headers_from_object,
|
||||
provider_oauth_import_authorization_bearer_token,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
|
||||
use crate::handlers::admin::provider::oauth::state::{current_unix_secs, json_u64_value};
|
||||
use axum::{
|
||||
@@ -9,6 +13,7 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -22,6 +27,7 @@ pub(super) struct AdminProviderOAuthBatchImportEntry {
|
||||
pub parse_error: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
pub access_token: Option<String>,
|
||||
pub export_access_token: Option<String>,
|
||||
pub raw_credentials: Option<serde_json::Value>,
|
||||
pub expires_at: Option<u64>,
|
||||
pub account_id: Option<String>,
|
||||
@@ -37,6 +43,7 @@ pub(super) struct AdminProviderOAuthBatchImportEntry {
|
||||
pub sso_rw_token: Option<String>,
|
||||
pub cf_cookies: Option<String>,
|
||||
pub cf_clearance: Option<String>,
|
||||
pub request_headers: Option<BTreeMap<String, String>>,
|
||||
pub user_agent: Option<String>,
|
||||
pub browser_profile: Option<String>,
|
||||
}
|
||||
@@ -183,6 +190,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
|
||||
parse_error: None,
|
||||
refresh_token,
|
||||
access_token,
|
||||
export_access_token: None,
|
||||
raw_credentials: None,
|
||||
expires_at: None,
|
||||
account_id: None,
|
||||
@@ -198,6 +206,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
|
||||
sso_rw_token: grok_cookie_value(raw_token, "sso-rw"),
|
||||
cf_cookies: grok_cookie_profile(raw_token),
|
||||
cf_clearance: grok_cookie_value(raw_token, "cf_clearance"),
|
||||
request_headers: None,
|
||||
user_agent: None,
|
||||
browser_profile: None,
|
||||
})
|
||||
@@ -237,10 +246,24 @@ fn extract_admin_provider_oauth_batch_import_entry(
|
||||
.as_deref()
|
||||
.and_then(|cookie| grok_cookie_value(cookie, "sso"))
|
||||
});
|
||||
let request_headers = normalize_provider_oauth_import_headers_from_object(object);
|
||||
let header_bearer_token =
|
||||
provider_oauth_import_authorization_bearer_token(request_headers.as_ref());
|
||||
let export_access_token = if refresh_token.is_none() {
|
||||
access_token
|
||||
.as_ref()
|
||||
.filter(|value| header_bearer_token.as_deref() != Some(value.as_str()))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (refresh_token, access_token) = normalize_provider_import_tokens(
|
||||
provider_type,
|
||||
refresh_token.as_deref(),
|
||||
access_token.as_deref().or(session_token.as_deref()),
|
||||
access_token
|
||||
.as_deref()
|
||||
.or(session_token.as_deref())
|
||||
.or(header_bearer_token.as_deref()),
|
||||
);
|
||||
let windsurf_api_key = is_windsurf
|
||||
.then(|| {
|
||||
@@ -386,6 +409,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
|
||||
parse_error: None,
|
||||
refresh_token,
|
||||
access_token,
|
||||
export_access_token,
|
||||
raw_credentials,
|
||||
expires_at,
|
||||
account_id,
|
||||
@@ -401,6 +425,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
|
||||
sso_rw_token,
|
||||
cf_cookies,
|
||||
cf_clearance,
|
||||
request_headers,
|
||||
user_agent,
|
||||
browser_profile,
|
||||
})
|
||||
@@ -479,6 +504,7 @@ fn parse_error_entry(error: String) -> AdminProviderOAuthBatchImportEntry {
|
||||
parse_error: Some(error),
|
||||
refresh_token: None,
|
||||
access_token: None,
|
||||
export_access_token: None,
|
||||
raw_credentials: None,
|
||||
expires_at: None,
|
||||
account_id: None,
|
||||
@@ -494,6 +520,7 @@ fn parse_error_entry(error: String) -> AdminProviderOAuthBatchImportEntry {
|
||||
sso_rw_token: None,
|
||||
cf_cookies: None,
|
||||
cf_clearance: None,
|
||||
request_headers: None,
|
||||
user_agent: None,
|
||||
browser_profile: None,
|
||||
}
|
||||
@@ -569,6 +596,11 @@ pub(super) fn apply_admin_provider_oauth_batch_import_hints(
|
||||
.entry("pool_tier".to_string())
|
||||
.or_insert_with(|| json!(pool_tier));
|
||||
}
|
||||
if let Some(access_token) = entry.export_access_token.as_ref() {
|
||||
auth_config
|
||||
.entry("access_token".to_string())
|
||||
.or_insert_with(|| json!(access_token));
|
||||
}
|
||||
if let Some(user_id) = entry.user_id.as_ref() {
|
||||
auth_config
|
||||
.entry("user_id".to_string())
|
||||
@@ -599,6 +631,11 @@ pub(super) fn apply_admin_provider_oauth_batch_import_hints(
|
||||
.entry("cf_clearance".to_string())
|
||||
.or_insert_with(|| json!(cf_clearance));
|
||||
}
|
||||
if let Some(request_headers) = entry.request_headers.as_ref() {
|
||||
auth_config
|
||||
.entry("headers".to_string())
|
||||
.or_insert_with(|| json!(request_headers));
|
||||
}
|
||||
if let Some(user_agent) = entry.user_agent.as_ref() {
|
||||
auth_config
|
||||
.entry("user_agent".to_string())
|
||||
@@ -721,11 +758,110 @@ mod tests {
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].refresh_token, None);
|
||||
assert_eq!(entries[0].access_token.as_deref(), Some("at_1"));
|
||||
assert_eq!(entries[0].export_access_token.as_deref(), Some("at_1"));
|
||||
assert_eq!(entries[0].expires_at, Some(2_100_000_000));
|
||||
assert_eq!(entries[0].account_id.as_deref(), Some("acc-1"));
|
||||
assert_eq!(entries[0].email.as_deref(), Some("[email protected]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_codex_import_header_overrides() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"[{"access_token":"jwt_access","headers":{"authorization":"Bearer session-1","chatgpt-account-id":"acc-1","host":"blocked.example"}}]"#,
|
||||
);
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
let headers = entries[0]
|
||||
.request_headers
|
||||
.as_ref()
|
||||
.expect("headers should parse");
|
||||
assert_eq!(
|
||||
headers.get("authorization"),
|
||||
Some(&"Bearer session-1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("chatgpt-account-id"),
|
||||
Some(&"acc-1".to_string())
|
||||
);
|
||||
assert!(!headers.contains_key("host"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treats_import_authorization_header_as_session_access_token() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"[{"access_token":"jwt_access","headers":{"authorization":"Bearer session-1"}}]"#,
|
||||
);
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].access_token.as_deref(), Some("jwt_access"));
|
||||
assert_eq!(
|
||||
entries[0].export_access_token.as_deref(),
|
||||
Some("jwt_access")
|
||||
);
|
||||
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"[{"headers":{"authorization":"Bearer session-1"}}]"#,
|
||||
);
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].access_token.as_deref(), Some("session-1"));
|
||||
assert!(entries[0].export_access_token.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_codex_import_header_overrides_to_auth_config() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"{"accessToken":"at_1","headers":{"Authorization":"Bearer session-1"}}"#,
|
||||
);
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_admin_provider_oauth_batch_import_hints("codex", &entries[0], &mut auth_config);
|
||||
|
||||
assert_eq!(
|
||||
auth_config.get("headers"),
|
||||
Some(&json!({"authorization":"Bearer session-1"}))
|
||||
);
|
||||
assert_eq!(auth_config.get("access_token"), Some(&json!("at_1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_export_access_token_when_it_is_the_header_bearer() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"{"accessToken":"session-1","headers":{"Authorization":"Bearer session-1"}}"#,
|
||||
);
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_admin_provider_oauth_batch_import_hints("codex", &entries[0], &mut auth_config);
|
||||
|
||||
assert!(entries[0].export_access_token.is_none());
|
||||
assert!(auth_config.get("access_token").is_none());
|
||||
assert_eq!(
|
||||
auth_config.get("headers"),
|
||||
Some(&json!({"authorization":"Bearer session-1"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_export_access_token_from_refresh_token_payload() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
"codex",
|
||||
r#"{"refreshToken":"refresh-1","accessToken":"refreshed-access"}"#,
|
||||
);
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_admin_provider_oauth_batch_import_hints("codex", &entries[0], &mut auth_config);
|
||||
|
||||
assert_eq!(entries[0].refresh_token.as_deref(), Some("refresh-1"));
|
||||
assert_eq!(entries[0].access_token.as_deref(), Some("refreshed-access"));
|
||||
assert!(entries[0].export_access_token.is_none());
|
||||
assert!(auth_config.get("access_token").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_common_chatgpt_web_json_aliases() {
|
||||
let entries = parse_admin_provider_oauth_batch_import_entries(
|
||||
|
||||
@@ -16,7 +16,9 @@ use super::super::state::{
|
||||
};
|
||||
use super::helpers::admin_provider_oauth_key_name_from_auth_config;
|
||||
use super::token_import::{
|
||||
build_provider_access_token_import_auth_config, normalize_provider_import_tokens,
|
||||
build_provider_access_token_import_auth_config, decode_access_token_expires_at,
|
||||
normalize_provider_import_tokens, normalize_provider_oauth_import_headers_from_object,
|
||||
provider_oauth_import_authorization_bearer_token_from_object,
|
||||
provider_type_supports_access_token_import,
|
||||
};
|
||||
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_import_provider_id;
|
||||
@@ -151,6 +153,20 @@ fn import_payload_u64_any(
|
||||
})
|
||||
}
|
||||
|
||||
fn import_payload_export_access_token(
|
||||
payload: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> Option<String> {
|
||||
if import_payload_string(payload, "refresh_token", "refreshToken").is_some() {
|
||||
return None;
|
||||
}
|
||||
let access_token = import_payload_string(payload, "access_token", "accessToken")?;
|
||||
let header_bearer = provider_oauth_import_authorization_bearer_token_from_object(payload);
|
||||
if header_bearer.as_deref() == Some(access_token.as_str()) {
|
||||
return None;
|
||||
}
|
||||
Some(access_token)
|
||||
}
|
||||
|
||||
fn apply_single_import_hints(
|
||||
provider_type: &str,
|
||||
payload: &serde_json::Map<String, serde_json::Value>,
|
||||
@@ -267,6 +283,18 @@ fn apply_single_import_hints(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(access_token) = import_payload_export_access_token(payload) {
|
||||
auth_config
|
||||
.entry("access_token".to_string())
|
||||
.or_insert_with(|| json!(access_token));
|
||||
}
|
||||
|
||||
if let Some(request_headers) = normalize_provider_oauth_import_headers_from_object(payload) {
|
||||
auth_config
|
||||
.entry("headers".to_string())
|
||||
.or_insert_with(|| json!(request_headers));
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_admin_provider_oauth_single_import_tokens(
|
||||
@@ -497,7 +525,8 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
|
||||
"session_token",
|
||||
"sessionToken",
|
||||
],
|
||||
);
|
||||
)
|
||||
.or_else(|| provider_oauth_import_authorization_bearer_token_from_object(&raw_payload));
|
||||
let imported_expires_at =
|
||||
import_payload_u64_any(&raw_payload, &["expires_at", "expiresAt", "expired"]);
|
||||
let name = raw_payload
|
||||
@@ -614,9 +643,22 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
|
||||
let AdminProviderOAuthSingleImportTokens {
|
||||
access_token,
|
||||
mut auth_config,
|
||||
expires_at,
|
||||
mut expires_at,
|
||||
} = resolved_import;
|
||||
apply_single_import_hints(&provider_type, &raw_payload, &mut auth_config);
|
||||
if let Some(header_access_token) =
|
||||
provider_oauth_import_authorization_bearer_token_from_object(&raw_payload)
|
||||
{
|
||||
if let Some(header_expires_at) =
|
||||
decode_access_token_expires_at(&header_access_token).or(imported_expires_at)
|
||||
{
|
||||
auth_config.insert("expires_at".to_string(), json!(header_expires_at));
|
||||
expires_at = Some(header_expires_at);
|
||||
} else {
|
||||
auth_config.remove("expires_at");
|
||||
expires_at = None;
|
||||
}
|
||||
}
|
||||
let has_refresh_token = auth_config
|
||||
.get("refresh_token")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
@@ -776,6 +818,69 @@ mod tests {
|
||||
assert_eq!(auth_config.get("user_agent"), Some(&json!("antigravity")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_import_preserves_distinct_top_level_access_token_for_export() {
|
||||
let payload = json!({
|
||||
"access_token": "jwt-access-token",
|
||||
"headers": {
|
||||
"authorization": "Bearer imported-session-token"
|
||||
},
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload should be an object");
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_single_import_hints("codex", &payload, &mut auth_config);
|
||||
|
||||
assert_eq!(
|
||||
auth_config.get("access_token"),
|
||||
Some(&json!("jwt-access-token"))
|
||||
);
|
||||
assert_eq!(
|
||||
auth_config.get("headers"),
|
||||
Some(&json!({"authorization":"Bearer imported-session-token"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_import_does_not_preserve_header_bearer_as_export_access_token() {
|
||||
let payload = json!({
|
||||
"access_token": "imported-session-token",
|
||||
"headers": {
|
||||
"authorization": "Bearer imported-session-token"
|
||||
},
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload should be an object");
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_single_import_hints("codex", &payload, &mut auth_config);
|
||||
|
||||
assert!(auth_config.get("access_token").is_none());
|
||||
assert_eq!(
|
||||
auth_config.get("headers"),
|
||||
Some(&json!({"authorization":"Bearer imported-session-token"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_import_does_not_preserve_access_token_from_refresh_token_payload_for_export() {
|
||||
let payload = json!({
|
||||
"refresh_token": "refresh-token",
|
||||
"access_token": "refreshed-access-token",
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload should be an object");
|
||||
let mut auth_config = serde_json::Map::new();
|
||||
|
||||
apply_single_import_hints("codex", &payload, &mut auth_config);
|
||||
|
||||
assert!(auth_config.get("access_token").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windsurf_import_error_redacts_http_body() {
|
||||
let error = OAuthError::HttpStatus {
|
||||
|
||||
@@ -5,6 +5,7 @@ use base64::{
|
||||
Engine as _,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn decode_base64_url_part(value: &str) -> Option<Vec<u8>> {
|
||||
URL_SAFE_NO_PAD
|
||||
@@ -117,6 +118,95 @@ pub(super) fn decode_access_token_expires_at(access_token: &str) -> Option<u64>
|
||||
json_u64_value(claims.get("exp"))
|
||||
}
|
||||
|
||||
fn normalize_import_header_name(raw: &str) -> Option<String> {
|
||||
let value = raw.trim().to_ascii_lowercase();
|
||||
if value.is_empty()
|
||||
|| matches!(
|
||||
value.as_str(),
|
||||
"host"
|
||||
| "content-length"
|
||||
| "connection"
|
||||
| "keep-alive"
|
||||
| "proxy-authenticate"
|
||||
| "proxy-authorization"
|
||||
| "proxy-connection"
|
||||
| "te"
|
||||
| "trailer"
|
||||
| "transfer-encoding"
|
||||
| "upgrade"
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
http::header::HeaderName::from_bytes(value.as_bytes())
|
||||
.ok()
|
||||
.map(|name| name.as_str().to_string())
|
||||
}
|
||||
|
||||
fn normalize_import_header_value(value: &Value) -> Option<String> {
|
||||
let value = match value {
|
||||
Value::String(raw) => raw.trim().to_string(),
|
||||
Value::Number(raw) => raw.to_string(),
|
||||
Value::Bool(raw) => raw.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
if value.is_empty() || http::header::HeaderValue::from_str(&value).is_err() {
|
||||
return None;
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_provider_oauth_import_headers(
|
||||
value: Option<&Value>,
|
||||
) -> Option<BTreeMap<String, String>> {
|
||||
let object = value?.as_object()?;
|
||||
let mut headers = BTreeMap::new();
|
||||
for (raw_key, raw_value) in object {
|
||||
let Some(key) = normalize_import_header_name(raw_key) else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = normalize_import_header_value(raw_value) else {
|
||||
continue;
|
||||
};
|
||||
headers.insert(key, value);
|
||||
}
|
||||
(!headers.is_empty()).then_some(headers)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_provider_oauth_import_headers_from_object(
|
||||
object: &Map<String, Value>,
|
||||
) -> Option<BTreeMap<String, String>> {
|
||||
normalize_provider_oauth_import_headers(
|
||||
object
|
||||
.get("headers")
|
||||
.or_else(|| object.get("request_headers"))
|
||||
.or_else(|| object.get("requestHeaders"))
|
||||
.or_else(|| object.get("header_overrides"))
|
||||
.or_else(|| object.get("headerOverrides"))
|
||||
.or_else(|| object.get("extra_headers"))
|
||||
.or_else(|| object.get("extraHeaders")),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn provider_oauth_import_authorization_bearer_token(
|
||||
headers: Option<&BTreeMap<String, String>>,
|
||||
) -> Option<String> {
|
||||
let value = headers?.get("authorization")?.trim();
|
||||
let (scheme, token) = value.split_once(' ')?;
|
||||
if !scheme.eq_ignore_ascii_case("bearer") {
|
||||
return None;
|
||||
}
|
||||
let token = token.trim();
|
||||
(!token.is_empty()).then(|| token.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn provider_oauth_import_authorization_bearer_token_from_object(
|
||||
object: &Map<String, Value>,
|
||||
) -> Option<String> {
|
||||
let headers = normalize_provider_oauth_import_headers_from_object(object)?;
|
||||
provider_oauth_import_authorization_bearer_token(Some(&headers))
|
||||
}
|
||||
|
||||
pub(super) fn provider_type_supports_access_token_import(provider_type: &str) -> bool {
|
||||
matches!(
|
||||
provider_type.trim().to_ascii_lowercase().as_str(),
|
||||
@@ -176,7 +266,9 @@ pub(super) fn build_provider_access_token_import_auth_config(
|
||||
mod tests {
|
||||
use super::{
|
||||
build_provider_access_token_import_auth_config, decode_access_token_expires_at,
|
||||
looks_like_access_token, normalize_provider_import_tokens, normalize_single_import_tokens,
|
||||
looks_like_access_token, normalize_provider_import_tokens,
|
||||
normalize_provider_oauth_import_headers, normalize_single_import_tokens,
|
||||
provider_oauth_import_authorization_bearer_token,
|
||||
};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use serde_json::json;
|
||||
@@ -247,6 +339,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_import_header_overrides_for_auth_headers() {
|
||||
let headers = normalize_provider_oauth_import_headers(Some(&json!({
|
||||
"Authorization": " Bearer session-token ",
|
||||
"X-Feature": true,
|
||||
"Host": "evil.example",
|
||||
"X-Bad": "line\nbreak",
|
||||
})))
|
||||
.expect("headers should normalize");
|
||||
|
||||
assert_eq!(
|
||||
headers.get("authorization"),
|
||||
Some(&"Bearer session-token".to_string())
|
||||
);
|
||||
assert_eq!(headers.get("x-feature"), Some(&"true".to_string()));
|
||||
assert!(!headers.contains_key("host"));
|
||||
assert!(!headers.contains_key("x-bad"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_import_authorization_header_bearer_token() {
|
||||
let headers = normalize_provider_oauth_import_headers(Some(&json!({
|
||||
"Authorization": "Bearer at-session-token",
|
||||
})))
|
||||
.expect("headers should normalize");
|
||||
|
||||
assert_eq!(
|
||||
provider_oauth_import_authorization_bearer_token(Some(&headers)).as_deref(),
|
||||
Some("at-session-token")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_chatgpt_web_temporary_auth_config_from_access_token() {
|
||||
let token = unsigned_jwt(json!({
|
||||
|
||||
Reference in New Issue
Block a user