systemprompt-oauth 0.21.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
//! RFC 7591 dynamic client registration types.
//!
//! Accessor defaults (grant/response types, auth method, application type)
//! follow the RFC 7591 §2 and OIDC Dynamic Client Registration §2 server
//! defaults for omitted fields.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use crate::error::{OauthError, OauthResult};
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 application_type: 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,
    pub application_type: 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) -> OauthResult<String> {
        self.client_name
            .as_ref()
            .filter(|n| !n.is_empty())
            .cloned()
            .ok_or_else(|| {
                OauthError::Validation("client_name is required for client registration".to_owned())
            })
    }

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

    pub fn get_grant_types(&self) -> Vec<String> {
        self.grant_types
            .as_ref()
            .filter(|types| !types.is_empty())
            .cloned()
            .unwrap_or_else(|| vec!["authorization_code".to_owned()])
    }

    pub fn get_response_types(&self) -> Vec<String> {
        self.response_types
            .as_ref()
            .filter(|types| !types.is_empty())
            .cloned()
            .unwrap_or_else(|| vec!["code".to_owned()])
    }

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

    pub fn get_token_endpoint_auth_method(&self) -> String {
        self.token_endpoint_auth_method
            .as_ref()
            .filter(|m| !m.is_empty())
            .cloned()
            .unwrap_or_else(|| "client_secret_basic".to_owned())
    }

    pub fn get_application_type(&self) -> OauthResult<String> {
        match self.application_type.as_deref().filter(|t| !t.is_empty()) {
            None => Ok("web".to_owned()),
            Some(t @ ("web" | "native")) => Ok(t.to_owned()),
            Some(other) => Err(OauthError::Validation(format!(
                "application_type must be \"web\" or \"native\", got {other:?}"
            ))),
        }
    }
}