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 authentication / credential verification.

use crate::error::OauthResult as Result;
use crate::repository::OAuthRepository;
use crate::services::verify_client_secret;
use systemprompt_identifiers::ClientId;

const TIMING_SAFE_DUMMY_HASH: &str = "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA/7E/fxXwK";

pub async fn validate_client_credentials(
    repo: &OAuthRepository,
    client_id: &ClientId,
    client_secret: Option<&str>,
) -> Result<()> {
    let client = repo
        .find_client_by_id(client_id)
        .await?
        .ok_or_else(|| crate::error::OauthError::Internal("Client not found".to_string()))?;

    verify_client_authentication(
        client.token_endpoint_auth_method.as_str(),
        client.client_secret_hash.as_deref(),
        client_secret,
    )
}

pub fn verify_client_authentication(
    auth_method: &str,
    secret_hash: Option<&str>,
    client_secret: Option<&str>,
) -> Result<()> {
    if auth_method == "none" {
        return Ok(());
    }

    let (hash_to_verify, secret_to_verify) = match (secret_hash, client_secret) {
        (Some(hash), Some(secret)) => (hash, secret),
        (Some(_hash), None) => {
            perform_timing_safe_dummy_verification();
            return Err(crate::error::OauthError::Internal(
                "Client secret required".to_string(),
            ));
        },
        (None, Some(_secret)) => {
            perform_timing_safe_dummy_verification();
            return Err(crate::error::OauthError::Internal(
                "Client has no secret hash configured".to_string(),
            ));
        },
        (None, None) => {
            perform_timing_safe_dummy_verification();
            return Err(crate::error::OauthError::Internal(
                "Client secret required".to_string(),
            ));
        },
    };

    if !verify_client_secret(secret_to_verify, hash_to_verify)? {
        return Err(crate::error::OauthError::Internal(
            "Invalid client secret".to_string(),
        ));
    }

    Ok(())
}

fn perform_timing_safe_dummy_verification() {
    if let Err(e) = verify_client_secret("dummy_secret", TIMING_SAFE_DUMMY_HASH) {
        tracing::debug!(error = %e, "Timing-safe dummy verification encountered error");
    }
}