atproto-client 0.10.0

HTTP client for AT Protocol services with OAuth and identity integration
Documentation
//! AT Protocol server authentication operations.
//!
//! Client functions for com.atproto.server XRPC methods including
//! session creation and refresh with app password authentication.
//! - **`create_app_password()`**: Create a new app password for authenticated account
//!
//! ## Request/Response Types
//!
//! - **`CreateSessionRequest`**: Parameters for creating a new session
//! - **`AppPasswordSession`**: Response containing session data and tokens
//! - **`RefreshSessionResponse`**: Response from session refresh operation
//! - **`AppPasswordResponse`**: Response containing created app password details
//!
//! ## Authentication
//!
//! Session creation uses app password authentication, while session refresh requires
//! the refresh JWT token from a previous session. App password creation requires
//! an access JWT token from an authenticated session.

use anyhow::Result;
use serde::{Deserialize, Serialize};

use crate::{client::post_json, url::URLBuilder};

/// Request to create a new authentication session.
#[derive(Serialize, Clone)]
pub struct CreateSessionRequest {
    /// Handle or other identifier supported by the server for the authenticating user
    pub identifier: String,
    /// User password or app password
    pub password: String,
    /// Optional two-factor authentication token
    #[serde(skip_serializing_if = "Option::is_none", rename = "authFactorToken")]
    pub auth_factor_token: Option<String>,
}

impl std::fmt::Debug for CreateSessionRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CreateSessionRequest")
            .field("identifier", &self.identifier)
            .field("password", &"[REDACTED]")
            .field(
                "auth_factor_token",
                &self.auth_factor_token.as_ref().map(|_| "[REDACTED]"),
            )
            .finish()
    }
}

/// App password session data returned from successful authentication.
#[derive(Deserialize, Clone)]
pub struct AppPasswordSession {
    /// Distributed identifier for the authenticated account
    pub did: String,
    /// Handle for the authenticated account
    pub handle: String,
    /// Email address for the authenticated account
    pub email: String,
    /// JWT access token for authenticated requests
    #[serde(rename = "accessJwt")]
    pub access_jwt: String,
    /// JWT refresh token for obtaining new access tokens
    #[serde(rename = "refreshJwt")]
    pub refresh_jwt: String,
}

impl std::fmt::Debug for AppPasswordSession {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AppPasswordSession")
            .field("did", &self.did)
            .field("handle", &self.handle)
            .field("email", &self.email)
            .field("access_jwt", &"[REDACTED]")
            .field("refresh_jwt", &"[REDACTED]")
            .finish()
    }
}

/// Response from refreshing an authentication session.
#[derive(Deserialize, Clone)]
pub struct RefreshSessionResponse {
    /// Distributed identifier for the authenticated account
    pub did: String,
    /// Handle for the authenticated account
    pub handle: String,
    /// JWT access token for authenticated requests
    #[serde(rename = "accessJwt")]
    pub access_jwt: String,
    /// JWT refresh token for obtaining new access tokens
    #[serde(rename = "refreshJwt")]
    pub refresh_jwt: String,
    /// Whether the account is active
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active: Option<bool>,
    /// Account status (e.g., "takendown", "suspended", "deactivated")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

impl std::fmt::Debug for RefreshSessionResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RefreshSessionResponse")
            .field("did", &self.did)
            .field("handle", &self.handle)
            .field("access_jwt", &"[REDACTED]")
            .field("refresh_jwt", &"[REDACTED]")
            .field("active", &self.active)
            .field("status", &self.status)
            .finish()
    }
}

/// Response from creating a new app password.
#[derive(Deserialize, Clone)]
pub struct AppPasswordResponse {
    /// Name of the app password
    pub name: String,
    /// Generated app password string
    pub password: String,
    /// Creation timestamp in ISO 8601 format
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

impl std::fmt::Debug for AppPasswordResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AppPasswordResponse")
            .field("name", &self.name)
            .field("password", &"[REDACTED]")
            .field("created_at", &self.created_at)
            .finish()
    }
}

/// Creates a new authentication session using app password credentials.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `identifier` - Handle or other identifier for the user
/// * `password` - User password or app password
/// * `auth_factor_token` - Optional two-factor authentication token
///
/// # Returns
///
/// The created session data including access and refresh tokens
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn create_session(
    http_client: &reqwest::Client,
    base_url: &str,
    identifier: &str,
    password: &str,
    auth_factor_token: Option<&str>,
) -> Result<AppPasswordSession> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.createSession");
    let url = url_builder.build();

    let request = CreateSessionRequest {
        identifier: identifier.to_string(),
        password: password.to_string(),
        auth_factor_token: auth_factor_token.map(|s| s.to_string()),
    };

    let value = serde_json::to_value(request)?;

    post_json(http_client, &url, value)
        .await
        .and_then(|value| serde_json::from_value(value).map_err(|err| err.into()))
}

/// Refreshes an existing authentication session using a refresh token.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `refresh_token` - JWT refresh token from a previous session
///
/// # Returns
///
/// The refreshed session data with new access and refresh tokens
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn refresh_session(
    http_client: &reqwest::Client,
    base_url: &str,
    refresh_token: &str,
) -> Result<RefreshSessionResponse> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.refreshSession");
    let url = url_builder.build();

    // Create a new client with the refresh token in Authorization header
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("Bearer {}", refresh_token))?,
    );

    let response = http_client.post(&url).headers(headers).send().await?;

    let value = response.json::<serde_json::Value>().await?;

    serde_json::from_value(value).map_err(|err| err.into())
}

/// Creates a new app password for the authenticated account.
///
/// # Arguments
///
/// * `http_client` - HTTP client for making requests
/// * `base_url` - Base URL of the AT Protocol server
/// * `access_token` - JWT access token for authentication
/// * `name` - Name for the app password
///
/// # Returns
///
/// The created app password details including the generated password
///
/// # Errors
///
/// Returns errors for HTTP request failures, authentication failures,
/// or JSON parsing failures.
pub async fn create_app_password(
    http_client: &reqwest::Client,
    base_url: &str,
    access_token: &str,
    name: &str,
) -> Result<AppPasswordResponse> {
    let mut url_builder = URLBuilder::new(base_url);
    url_builder.path("/xrpc/com.atproto.server.createAppPassword");
    let url = url_builder.build();

    let request_body = serde_json::json!({
        "name": name
    });

    // Create a new client with the access token in Authorization header
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("Bearer {}", access_token))?,
    );

    let response = http_client
        .post(&url)
        .headers(headers)
        .json(&request_body)
        .send()
        .await?;

    let value = response.json::<serde_json::Value>().await?;

    serde_json::from_value(value).map_err(|err| err.into())
}