securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::auth::secure_string::SecureString;
use anyhow::{bail, Context, Result};
use serde::Deserialize;

/// GitHub OAuth client ID.
/// Override with SECUREGIT_GITHUB_CLIENT_ID env var for custom OAuth apps.
fn github_client_id() -> String {
    std::env::var("SECUREGIT_GITHUB_CLIENT_ID")
        .unwrap_or_else(|_| "securegit-placeholder".to_string())
}

/// GitLab OAuth client ID.
/// Override with SECUREGIT_GITLAB_CLIENT_ID env var.
fn gitlab_client_id() -> String {
    std::env::var("SECUREGIT_GITLAB_CLIENT_ID")
        .unwrap_or_else(|_| "securegit-placeholder".to_string())
}

#[derive(Deserialize)]
struct DeviceCodeResponse {
    device_code: String,
    user_code: String,
    verification_uri: String,
    expires_in: u64,
    interval: u64,
}

#[derive(Deserialize)]
struct TokenResponse {
    access_token: Option<String>,
    error: Option<String>,
    #[serde(default)]
    scope: Option<String>,
}

pub struct OAuthResult {
    pub token: SecureString,
    pub scope: String,
    pub user: String,
}

/// Run the GitHub Device Flow OAuth.
pub async fn github_device_flow(
    on_code: impl Fn(&str, &str),
    on_waiting: impl Fn(),
) -> Result<OAuthResult> {
    let client = reqwest::Client::new();
    let client_id = github_client_id();

    // Step 1: Request device code
    let resp = client
        .post("https://github.com/login/device/code")
        .header("Accept", "application/json")
        .json(&serde_json::json!({
            "client_id": client_id,
            "scope": "repo"
        }))
        .send()
        .await
        .context("Failed to request device code")?;

    if !resp.status().is_success() {
        let text = resp.text().await.unwrap_or_default();
        bail!("GitHub device code request failed: {}", text);
    }

    let device: DeviceCodeResponse = resp.json().await.context("Failed to parse device code")?;

    // Step 2: Show user code and open browser
    on_code(&device.user_code, &device.verification_uri);
    let _ = open::that(&device.verification_uri);

    // Step 3: Poll for token
    let mut interval = device.interval;
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(device.expires_in);

    loop {
        if std::time::Instant::now() > deadline {
            bail!("Device code expired. Please try again.");
        }

        tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
        on_waiting();

        let resp = client
            .post("https://github.com/login/oauth/access_token")
            .header("Accept", "application/json")
            .json(&serde_json::json!({
                "client_id": client_id,
                "device_code": device.device_code,
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
            }))
            .send()
            .await
            .context("Failed to poll for token")?;

        let token_resp: TokenResponse = resp.json().await?;

        if let Some(token) = token_resp.access_token {
            let secure_token = SecureString::from_string(token);
            let scope = token_resp.scope.unwrap_or_default();

            // Get username
            let user = get_github_user(&secure_token)
                .await
                .unwrap_or_else(|_| "unknown".to_string());

            return Ok(OAuthResult {
                token: secure_token,
                scope,
                user,
            });
        }

        match token_resp.error.as_deref() {
            Some("authorization_pending") => continue,
            Some("slow_down") => {
                interval += 5;
                continue;
            }
            Some("expired_token") => bail!("Device code expired. Please try again."),
            Some("access_denied") => bail!("Authorization was denied by the user."),
            Some(err) => bail!("OAuth error: {}", err),
            None => continue,
        }
    }
}

/// Run the GitLab Device Flow OAuth.
pub async fn gitlab_device_flow(
    on_code: impl Fn(&str, &str),
    on_waiting: impl Fn(),
) -> Result<OAuthResult> {
    let client = reqwest::Client::new();
    let client_id = gitlab_client_id();

    let resp = client
        .post("https://gitlab.com/oauth/authorize_device")
        .header("Accept", "application/json")
        .form(&[("client_id", client_id.as_str()), ("scope", "api")])
        .send()
        .await
        .context("Failed to request device code from GitLab")?;

    if !resp.status().is_success() {
        let text = resp.text().await.unwrap_or_default();
        bail!("GitLab device code request failed: {}", text);
    }

    let device: DeviceCodeResponse = resp.json().await.context("Failed to parse device code")?;

    on_code(&device.user_code, &device.verification_uri);
    let _ = open::that(&device.verification_uri);

    let mut interval = device.interval;
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(device.expires_in);

    loop {
        if std::time::Instant::now() > deadline {
            bail!("Device code expired. Please try again.");
        }

        tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
        on_waiting();

        let resp = client
            .post("https://gitlab.com/oauth/token")
            .header("Accept", "application/json")
            .form(&[
                ("client_id", client_id.as_str()),
                ("device_code", device.device_code.as_str()),
                ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
            ])
            .send()
            .await
            .context("Failed to poll for token")?;

        let token_resp: TokenResponse = resp.json().await?;

        if let Some(token) = token_resp.access_token {
            let secure_token = SecureString::from_string(token);
            let scope = token_resp.scope.unwrap_or_default();
            let user = get_gitlab_user(&secure_token)
                .await
                .unwrap_or_else(|_| "unknown".to_string());

            return Ok(OAuthResult {
                token: secure_token,
                scope,
                user,
            });
        }

        match token_resp.error.as_deref() {
            Some("authorization_pending") => continue,
            Some("slow_down") => {
                interval += 5;
                continue;
            }
            Some("expired_token") => bail!("Device code expired. Please try again."),
            Some("access_denied") => bail!("Authorization was denied by the user."),
            Some(err) => bail!("OAuth error: {}", err),
            None => continue,
        }
    }
}

async fn get_github_user(token: &SecureString) -> Result<String> {
    let client = reqwest::Client::new();
    let resp = client
        .get("https://api.github.com/user")
        .header("Authorization", format!("Bearer {}", token.as_str()))
        .header("User-Agent", "securegit")
        .send()
        .await?;
    let data: serde_json::Value = resp.json().await?;
    Ok(data["login"].as_str().unwrap_or("unknown").to_string())
}

async fn get_gitlab_user(token: &SecureString) -> Result<String> {
    let client = reqwest::Client::new();
    let resp = client
        .get("https://gitlab.com/api/v4/user")
        .header("PRIVATE-TOKEN", token.as_str())
        .send()
        .await?;
    let data: serde_json::Value = resp.json().await?;
    Ok(data["username"].as_str().unwrap_or("unknown").to_string())
}