Menu
Public repository / feb7ac86b5e60d55b429eacb7146cf0dc32c6628
AkurAI-Drive / backend/src/idp.rs
Latest change 6e3a0824f4ec4d2bea2524a20b4960c7ab9e2766 - backend: akurai-drive-api (OIDC RP + per-tenant S3 provisioning) by olibuijr
use base64::Engine;
use crate::config;
/// The subset of OIDC userinfo the Drive needs.
pub struct UserInfo {
pub sub: String,
pub email: String,
pub groups: Vec<String>,
pub tenant_id: Option<String>,
}
/// Back-channel: exchange the authorization `code` for an access token, then
/// fetch userinfo. Blocking (uses `ureq`) — call from `spawn_blocking`.
///
/// All traffic here is plain HTTP over localhost (`IDP_INTERNAL_URL`), so no TLS
/// is involved. The token endpoint uses HTTP Basic auth with the OIDC client
/// credentials; userinfo uses the resulting Bearer token.
pub fn exchange_code_for_userinfo(code: &str) -> Result<UserInfo, String> {
let cfg = config::get();
// 1. Token exchange (HTTP Basic client auth, form body).
let basic = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", cfg.oidc_client_id, cfg.oidc_client_secret));
let token_url = format!("{}/token", cfg.idp_internal_url);
let token_body = ureq::post(&token_url)
.set("Authorization", &format!("Basic {basic}"))
.send_form(&[
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", &cfg.oidc_redirect_uri),
])
.map_err(|e| format!("token request failed: {e}"))?
.into_string()
.map_err(|e| format!("token body read failed: {e}"))?;
let token_json: serde_json::Value =
serde_json::from_str(&token_body).map_err(|e| format!("token json parse failed: {e}"))?;
let access_token = token_json
.get("access_token")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| "no access_token in token response".to_string())?
.to_string();
// 2. Userinfo (Bearer auth).
let userinfo_url = format!("{}/userinfo", cfg.idp_internal_url);
let userinfo_body = ureq::get(&userinfo_url)
.set("Authorization", &format!("Bearer {access_token}"))
.call()
.map_err(|e| format!("userinfo request failed: {e}"))?
.into_string()
.map_err(|e| format!("userinfo body read failed: {e}"))?;
let info: serde_json::Value = serde_json::from_str(&userinfo_body)
.map_err(|e| format!("userinfo json parse failed: {e}"))?;
let sub = info
.get("sub")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| "no sub in userinfo".to_string())?
.to_string();
let email = info
.get("email")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let groups = info
.get("groups")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|g| g.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let tenant_id = info
.get("tenant_id")
.and_then(|v| v.as_str())
.map(String::from);
Ok(UserInfo {
sub,
email,
groups,
tenant_id,
})
}