use crate::{
discovery::discover,
errors::{Error, Result},
http::HttpClient,
types::UserInfo,
};
use serde_json::json;
#[tracing::instrument(
name = "oidc_get_userinfo",
skip(access_token, http),
fields(issuer = %issuer)
)]
pub async fn get_userinfo(
issuer: &str,
access_token: &str,
http: &dyn HttpClient,
) -> Result<UserInfo> {
tracing::info!(
target: "xjp_oidc::userinfo",
"开始获取 UserInfo"
);
if issuer.is_empty() {
return Err(Error::InvalidParam("issuer cannot be empty"));
}
if access_token.is_empty() {
return Err(Error::InvalidParam("access_token cannot be empty"));
}
let cache = crate::cache::NoOpCache;
let metadata = discover(issuer, http, &cache).await?;
let userinfo_endpoint = metadata
.userinfo_endpoint
.ok_or_else(|| Error::Discovery("UserInfo endpoint not found".to_string()))?;
let auth_header = ("Authorization", format!("Bearer {}", access_token));
let empty_body = json!({});
let response = http
.post_json_value(&userinfo_endpoint, &empty_body, Some((auth_header.0, auth_header.1.as_str())))
.await
.map_err(|e| {
if let crate::http::HttpClientError::InvalidStatus { status, message: _ } = &e {
match *status {
401 => return Error::OAuth {
error: "unauthorized".to_string(),
description: Some("Invalid or expired access token".to_string()),
},
403 => return Error::OAuth {
error: "forbidden".to_string(),
description: Some("Access token does not have required scopes".to_string()),
},
405 => {
return Error::Network(
"UserInfo endpoint does not support POST method. \
This SDK currently requires POST support due to HttpClient trait limitations."
.to_string(),
);
}
_ => {}
}
}
Error::Network(format!("UserInfo request failed: {}", e))
})?;
if let Some(error) = response.get("error").and_then(|v| v.as_str()) {
let error_description = response
.get("error_description")
.and_then(|v| v.as_str())
.unwrap_or("No description");
return Err(Error::OAuth {
error: error.to_string(),
description: Some(error_description.to_string()),
});
}
let userinfo: UserInfo = serde_json::from_value(response)
.map_err(|e| Error::Verification(format!("Failed to parse UserInfo response: {}", e)))?;
if userinfo.sub.is_empty() {
return Err(Error::Verification("UserInfo response missing required 'sub' field".to_string()));
}
tracing::info!(
target: "xjp_oidc::userinfo",
"UserInfo 获取成功, sub: {}",
userinfo.sub
);
Ok(userinfo)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_userinfo_parsing() {
let json = serde_json::json!({
"sub": "123456",
"name": "Test User",
"given_name": "Test",
"family_name": "User",
"preferred_username": "testuser",
"email": "test@example.com",
"email_verified": true,
"picture": "https://example.com/photo.jpg",
"locale": "en-US",
"updated_at": 1234567890,
"xjp_admin": true,
"amr": ["pwd", "mfa"],
"auth_time": 1234567800
});
let userinfo: UserInfo = serde_json::from_value(json).unwrap();
assert_eq!(userinfo.sub, "123456");
assert_eq!(userinfo.name, Some("Test User".to_string()));
assert_eq!(userinfo.email, Some("test@example.com".to_string()));
assert_eq!(userinfo.email_verified, Some(true));
assert_eq!(userinfo.xjp_admin, Some(true));
assert_eq!(userinfo.amr, Some(vec!["pwd".to_string(), "mfa".to_string()]));
}
#[test]
fn test_minimal_userinfo() {
let json = serde_json::json!({
"sub": "user-123"
});
let userinfo: UserInfo = serde_json::from_value(json).unwrap();
assert_eq!(userinfo.sub, "user-123");
assert!(userinfo.name.is_none());
assert!(userinfo.email.is_none());
}
}