systemprompt-oauth 0.10.2

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
//! OAuth client domain model and API DTOs.

pub mod api;

use crate::error::OauthResult as Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use systemprompt_identifiers::ClientId;

#[derive(Debug)]
pub struct ClientRelations {
    pub redirect_uris: Vec<String>,
    pub grant_types: Vec<String>,
    pub response_types: Vec<String>,
    pub scopes: Vec<String>,
    pub contacts: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct OAuthClientRow {
    pub client_id: ClientId,
    pub client_secret_hash: Option<String>,
    pub client_name: String,
    pub name: Option<String>,
    pub token_endpoint_auth_method: Option<String>,
    pub client_uri: Option<String>,
    pub logo_uri: Option<String>,
    pub is_active: Option<bool>,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuthClient {
    pub client_id: ClientId,
    pub client_secret_hash: Option<String>,
    pub client_name: String,
    pub name: Option<String>,
    pub redirect_uris: Vec<String>,
    pub grant_types: Vec<String>,
    pub response_types: Vec<String>,
    pub scopes: Vec<String>,
    pub token_endpoint_auth_method: String,
    pub client_uri: Option<String>,
    pub logo_uri: Option<String>,
    pub contacts: Option<Vec<String>>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl OAuthClient {
    pub fn from_row_with_relations(row: OAuthClientRow, relations: ClientRelations) -> Self {
        Self {
            client_id: row.client_id,
            client_secret_hash: row.client_secret_hash,
            client_name: row.client_name,
            name: row.name,
            redirect_uris: relations.redirect_uris,
            grant_types: relations.grant_types,
            response_types: relations.response_types,
            scopes: relations.scopes,
            token_endpoint_auth_method: row
                .token_endpoint_auth_method
                .unwrap_or_else(|| "client_secret_post".to_string()),
            client_uri: row.client_uri,
            logo_uri: row.logo_uri,
            contacts: relations.contacts,
            is_active: row.is_active.unwrap_or(true),
            created_at: row.created_at.unwrap_or_else(Utc::now),
            updated_at: row.updated_at.unwrap_or_else(Utc::now),
        }
    }

    pub fn validate(&self) -> Result<()> {
        if self.client_id.as_str().is_empty() {
            return Err(crate::error::OauthError::Internal(
                "client_id cannot be empty".to_string(),
            ));
        }
        if self.client_name.is_empty() {
            return Err(crate::error::OauthError::Internal(
                "client_name cannot be empty".to_string(),
            ));
        }
        if self.redirect_uris.is_empty() {
            return Err(crate::error::OauthError::Internal(
                "redirect_uris cannot be empty".to_string(),
            ));
        }
        if self.grant_types.is_empty() {
            return Err(crate::error::OauthError::Internal(
                "grant_types cannot be empty".to_string(),
            ));
        }
        if self.response_types.is_empty() {
            return Err(crate::error::OauthError::Internal(
                "response_types cannot be empty".to_string(),
            ));
        }
        if self.scopes.is_empty() {
            return Err(crate::error::OauthError::Internal(
                "scopes cannot be empty".to_string(),
            ));
        }
        Ok(())
    }
}