mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(oauth): 对齐 OpenAI 回调 state 解析与提交流程 (#306)
Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -40,22 +40,26 @@ pub fn parse_provider_oauth_callback_params(callback_url: &str) -> BTreeMap<Stri
|
|||||||
let Ok(url) = Url::parse(callback_url.trim()) else {
|
let Ok(url) = Url::parse(callback_url.trim()) else {
|
||||||
return merged;
|
return merged;
|
||||||
};
|
};
|
||||||
for (key, value) in url.query_pairs() {
|
for (key, value) in form_urlencoded::parse(url.query().unwrap_or_default().as_bytes()) {
|
||||||
merged.insert(key.into_owned(), value.into_owned());
|
merged.insert(key.into_owned(), value.into_owned());
|
||||||
}
|
}
|
||||||
if let Some(fragment) = url.fragment() {
|
if let Some(fragment) = url.fragment() {
|
||||||
for (key, value) in form_urlencoded::parse(fragment.as_bytes()) {
|
for (key, value) in form_urlencoded::parse(fragment.trim_start_matches('#').as_bytes()) {
|
||||||
merged
|
merged.insert(key.into_owned(), value.into_owned());
|
||||||
.entry(key.into_owned())
|
|
||||||
.or_insert_with(|| value.into_owned());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(code) = merged.get("code").cloned() {
|
if let Some(code) = merged.get("code").cloned() {
|
||||||
if let Some((code_part, state_part)) = code.split_once("#state=") {
|
if let Some((code_part, state_part)) = code.split_once('#') {
|
||||||
merged.insert("code".to_string(), code_part.to_string());
|
merged.insert("code".to_string(), code_part.to_string());
|
||||||
merged
|
if !merged.contains_key("state") && !state_part.is_empty() {
|
||||||
.entry("state".to_string())
|
let normalized_state = state_part
|
||||||
.or_insert_with(|| state_part.to_string());
|
.strip_prefix("state=")
|
||||||
|
.unwrap_or(state_part)
|
||||||
|
.trim();
|
||||||
|
if !normalized_state.is_empty() {
|
||||||
|
merged.insert("state".to_string(), normalized_state.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
merged
|
merged
|
||||||
@@ -307,3 +311,48 @@ pub fn build_kiro_device_key_name(email: Option<&str>, refresh_token: Option<&st
|
|||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
format!("kiro_{fallback} (idc)")
|
format!("kiro_{fallback} (idc)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_provider_oauth_callback_params;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_provider_oauth_callback_params_reads_openai_query_state() {
|
||||||
|
let params = parse_provider_oauth_callback_params(
|
||||||
|
"http://localhost:1455/auth/callback?code=ac_test123&scope=openid+email+profile+offline_access&state=4a138f8c65814df691b1a567fd425fb3d7010e86df9b4eb48dcadd94de233d93",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(params.get("code").map(String::as_str), Some("ac_test123"));
|
||||||
|
assert_eq!(
|
||||||
|
params.get("scope").map(String::as_str),
|
||||||
|
Some("openid email profile offline_access")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
params.get("state").map(String::as_str),
|
||||||
|
Some("4a138f8c65814df691b1a567fd425fb3d7010e86df9b4eb48dcadd94de233d93")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_provider_oauth_callback_params_prefers_fragment_values_like_python() {
|
||||||
|
let params = parse_provider_oauth_callback_params(
|
||||||
|
"http://localhost:1455/auth/callback?code=query-code&state=stale#code=fragment-code&state=fresh-state",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
params.get("code").map(String::as_str),
|
||||||
|
Some("fragment-code")
|
||||||
|
);
|
||||||
|
assert_eq!(params.get("state").map(String::as_str), Some("fresh-state"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_provider_oauth_callback_params_extracts_state_from_code_suffix() {
|
||||||
|
let params = parse_provider_oauth_callback_params(
|
||||||
|
"http://localhost:1455/auth/callback?code=code-value%23state%3Dnonce-value",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(params.get("code").map(String::as_str), Some("code-value"));
|
||||||
|
assert_eq!(params.get("state").map(String::as_str), Some("nonce-value"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -613,6 +613,8 @@ function createInitialOAuthState(): OAuthState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const oauth = ref<OAuthState>(createInitialOAuthState())
|
const oauth = ref<OAuthState>(createInitialOAuthState())
|
||||||
|
let oauthInitRequestId = 0
|
||||||
|
let oauthCompleteRequestId = 0
|
||||||
|
|
||||||
// 设备授权状态
|
// 设备授权状态
|
||||||
type DeviceAuthType = 'builder_id' | 'identity_center'
|
type DeviceAuthType = 'builder_id' | 'identity_center'
|
||||||
@@ -787,6 +789,8 @@ function resetDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
|
oauthInitRequestId += 1
|
||||||
|
oauthCompleteRequestId += 1
|
||||||
oauth.value = createInitialOAuthState()
|
oauth.value = createInitialOAuthState()
|
||||||
stopImportPolling()
|
stopImportPolling()
|
||||||
stopDevicePolling()
|
stopDevicePolling()
|
||||||
@@ -830,40 +834,51 @@ function openAuthorizationUrl() {
|
|||||||
async function initOAuth() {
|
async function initOAuth() {
|
||||||
if (!props.providerId) return
|
if (!props.providerId) return
|
||||||
if (isKiroProvider.value) return
|
if (isKiroProvider.value) return
|
||||||
|
if (oauth.value.starting) return
|
||||||
|
|
||||||
|
const requestId = ++oauthInitRequestId
|
||||||
oauth.value.starting = true
|
oauth.value.starting = true
|
||||||
try {
|
try {
|
||||||
const resp = await startProviderLevelOAuth(props.providerId)
|
const resp = await startProviderLevelOAuth(props.providerId)
|
||||||
|
if (requestId !== oauthInitRequestId) return
|
||||||
oauth.value.authorization_url = resp.authorization_url
|
oauth.value.authorization_url = resp.authorization_url
|
||||||
oauth.value.redirect_uri = resp.redirect_uri
|
oauth.value.redirect_uri = resp.redirect_uri
|
||||||
oauth.value.instructions = resp.instructions
|
oauth.value.instructions = resp.instructions
|
||||||
oauth.value.provider_type = resp.provider_type
|
oauth.value.provider_type = resp.provider_type
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
if (requestId !== oauthInitRequestId) return
|
||||||
const errorMessage = parseApiError(err, '初始化授权失败')
|
const errorMessage = parseApiError(err, '初始化授权失败')
|
||||||
showError(errorMessage, '错误')
|
showError(errorMessage, '错误')
|
||||||
mode.value = 'import'
|
mode.value = 'import'
|
||||||
} finally {
|
} finally {
|
||||||
oauth.value.starting = false
|
if (requestId === oauthInitRequestId) {
|
||||||
|
oauth.value.starting = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCompleteOAuth() {
|
async function handleCompleteOAuth() {
|
||||||
|
if (oauth.value.completing) return
|
||||||
if (!canCompleteOAuth.value || !props.providerId) return
|
if (!canCompleteOAuth.value || !props.providerId) return
|
||||||
|
const requestId = ++oauthCompleteRequestId
|
||||||
oauth.value.completing = true
|
oauth.value.completing = true
|
||||||
try {
|
try {
|
||||||
await completeProviderLevelOAuth(props.providerId, {
|
await completeProviderLevelOAuth(props.providerId, {
|
||||||
callback_url: oauth.value.callback_url.trim(),
|
callback_url: oauth.value.callback_url.trim(),
|
||||||
proxy_node_id: selectedProxyNodeId.value || undefined,
|
proxy_node_id: selectedProxyNodeId.value || undefined,
|
||||||
})
|
})
|
||||||
|
if (requestId !== oauthCompleteRequestId) return
|
||||||
success('授权成功,账号已添加')
|
success('授权成功,账号已添加')
|
||||||
emit('saved')
|
emit('saved')
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
if (requestId !== oauthCompleteRequestId) return
|
||||||
const errorMessage = parseApiError(err, '完成授权失败')
|
const errorMessage = parseApiError(err, '完成授权失败')
|
||||||
showError(errorMessage, '错误')
|
showError(errorMessage, '错误')
|
||||||
} finally {
|
} finally {
|
||||||
oauth.value.completing = false
|
if (requestId === oauthCompleteRequestId) {
|
||||||
|
oauth.value.completing = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user