systemprompt-oauth 0.2.1

OAuth 2.0 / OIDC with PKCE, token introspection, and audience/issuer validation for systemprompt.io AI governance infrastructure. WebAuthn and JWT auth for the MCP governance pipeline.
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::ClientId;

#[derive(Debug, Deserialize)]
pub struct DynamicRegistrationRequest {
    pub client_name: Option<String>,
    pub redirect_uris: Option<Vec<String>>,
    pub grant_types: Option<Vec<String>>,
    pub response_types: Option<Vec<String>>,
    pub scope: Option<String>,
    pub token_endpoint_auth_method: Option<String>,
    pub client_uri: Option<String>,
    pub logo_uri: Option<String>,
    pub contacts: Option<Vec<String>>,

    pub software_statement: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct DynamicRegistrationResponse {
    pub client_id: ClientId,
    pub client_secret: String,
    pub client_name: String,
    pub redirect_uris: Vec<String>,
    pub grant_types: Vec<String>,
    pub response_types: Vec<String>,
    pub scope: String,
    pub token_endpoint_auth_method: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_uri: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub logo_uri: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub contacts: Option<Vec<String>>,

    pub client_secret_expires_at: u64,

    #[serde(with = "chrono::serde::ts_seconds")]
    pub client_id_issued_at: DateTime<Utc>,

    pub registration_access_token: String,
    pub registration_client_uri: String,
}

impl DynamicRegistrationRequest {
    pub fn get_client_name(&self) -> Result<String, String> {
        self.client_name
            .as_ref()
            .filter(|n| !n.is_empty())
            .ok_or_else(|| "client_name is required for client registration".to_string())
            .cloned()
    }

    pub fn get_redirect_uris(&self) -> Result<Vec<String>, String> {
        self.redirect_uris
            .as_ref()
            .filter(|uris| !uris.is_empty())
            .ok_or_else(|| "redirect_uris are required for client registration".to_string())
            .cloned()
    }

    pub fn get_grant_types(&self) -> Result<Vec<String>, String> {
        self.grant_types
            .as_ref()
            .filter(|types| !types.is_empty())
            .ok_or_else(|| "grant_types are required for client registration".to_string())
            .cloned()
    }

    pub fn get_response_types(&self) -> Result<Vec<String>, String> {
        self.response_types
            .as_ref()
            .filter(|types| !types.is_empty())
            .ok_or_else(|| "response_types are required for client registration".to_string())
            .cloned()
    }

    pub fn get_scopes(&self) -> Vec<String> {
        self.scope.as_ref().map_or_else(Vec::new, |s| {
            s.split_whitespace().map(ToString::to_string).collect()
        })
    }

    pub fn get_token_endpoint_auth_method(&self) -> Result<String, String> {
        self.token_endpoint_auth_method
            .as_ref()
            .filter(|m| !m.is_empty())
            .ok_or_else(|| {
                "token_endpoint_auth_method is required for client registration".to_string()
            })
            .cloned()
    }
}