AkurAI Build
Menu

Public repository / 01b0c3e49ec2c526bbda745999ffed8ebfa8b8ee

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,
    })
}