systemprompt-oauth 0.1.22

OAuth 2.0 authentication and authorization module for systemprompt.io OS
Documentation
use systemprompt_models::{AuthError, GrantType, ResponseType};

#[derive(Debug)]
pub struct CsrfToken(String);

impl CsrfToken {
    const MIN_STATE_LENGTH: usize = 16;

    pub fn new(state: impl Into<String>) -> Result<Self, AuthError> {
        let state = state.into();

        if state.is_empty() {
            return Err(AuthError::MissingState);
        }

        if state.len() < Self::MIN_STATE_LENGTH {
            return Err(AuthError::InvalidRequest {
                reason: format!(
                    "State must be at least {} characters for security",
                    Self::MIN_STATE_LENGTH
                ),
            });
        }

        if !state.chars().all(|c| c.is_ascii_graphic()) {
            return Err(AuthError::InvalidRequest {
                reason: "State must contain only printable ASCII characters".to_string(),
            });
        }

        Ok(Self(state))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn into_string(self) -> String {
        self.0
    }
}

#[derive(Debug)]
pub struct ValidatedClientRegistration {
    pub client_name: String,
    pub redirect_uris: Vec<String>,
    pub grant_types: Vec<GrantType>,
    pub response_types: Vec<ResponseType>,
}

pub fn required_param(value: Option<&str>, param_name: &str) -> Result<String, AuthError> {
    value
        .filter(|s| !s.is_empty())
        .ok_or_else(|| AuthError::InvalidRequest {
            reason: format!("{param_name} parameter is required"),
        })
        .map(ToString::to_string)
}

pub fn optional_param(value: Option<&str>) -> Option<String> {
    value.filter(|s| !s.is_empty()).map(ToString::to_string)
}

pub fn scope_param(value: Option<&str>) -> Result<Vec<String>, AuthError> {
    let scope_str = required_param(value, "scope")?;

    let scopes: Vec<String> = scope_str
        .split_whitespace()
        .map(ToString::to_string)
        .collect();

    if scopes.is_empty() {
        return Err(AuthError::InvalidScope { scope: scope_str });
    }

    Ok(scopes)
}

pub fn get_audit_user(user_id: Option<&str>) -> Result<String, AuthError> {
    user_id
        .filter(|id| !id.is_empty())
        .ok_or_else(|| AuthError::InvalidRequest {
            reason: "Authenticated user required for this operation".to_string(),
        })
        .map(ToString::to_string)
}