systemprompt-api 0.2.0

HTTP API server and gateway for systemprompt.io OS
Documentation
use anyhow::Result;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{Form, Json};
use serde::{Deserialize, Serialize};
use systemprompt_oauth::repository::OAuthRepository;
use systemprompt_oauth::services::validate_jwt_token;
use systemprompt_oauth::services::validation::validate_client_credentials;

use crate::routes::oauth::extractors::OAuthRepo;

#[derive(Debug, Deserialize)]

pub struct IntrospectRequest {
    pub token: String,
    pub token_type_hint: Option<String>,
    pub client_id: Option<String>,
    pub client_secret: Option<String>,
}

#[derive(Debug, Serialize)]

pub struct IntrospectResponse {
    pub active: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exp: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iat: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sub: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub aud: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iss: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jti: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct IntrospectError {
    pub error: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_description: Option<String>,
}

pub async fn handle_introspect(
    OAuthRepo(repo): OAuthRepo,
    Form(request): Form<IntrospectRequest>,
) -> impl IntoResponse {
    if let Some(client_id) = &request.client_id {
        let client_id = systemprompt_identifiers::ClientId::new(client_id);
        if validate_client_credentials(&repo, &client_id, request.client_secret.as_deref())
            .await
            .is_err()
        {
            let error = IntrospectError {
                error: "invalid_client".to_string(),
                error_description: Some("Invalid client credentials".to_string()),
            };
            return (StatusCode::UNAUTHORIZED, Json(error)).into_response();
        }
    }

    match introspect_token(&repo, &request.token) {
        Ok(response) => (StatusCode::OK, Json(response)).into_response(),
        Err(error) => {
            let error = IntrospectError {
                error: "server_error".to_string(),
                error_description: Some(error.to_string()),
            };
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error)).into_response()
        },
    }
}

fn introspect_token(_repo: &OAuthRepository, token: &str) -> Result<IntrospectResponse> {
    let jwt_secret = systemprompt_models::SecretsBootstrap::jwt_secret()?;
    let config = systemprompt_models::Config::get()?;
    match validate_jwt_token(token, jwt_secret, &config.jwt_issuer, &config.jwt_audiences) {
        Ok(claims) => Ok(IntrospectResponse {
            active: true,
            scope: Some(systemprompt_models::auth::permissions_to_string(
                &claims.scope,
            )),
            client_id: claims.client_id.clone(),
            username: Some(claims.username),
            token_type: Some("Bearer".to_string()),
            exp: Some(claims.exp),
            iat: Some(claims.iat),
            sub: Some(claims.sub),
            aud: claims.aud.iter().map(ToString::to_string).collect(),
            iss: Some(claims.iss),
            jti: Some(claims.jti),
        }),
        Err(_) => Ok(IntrospectResponse {
            active: false,
            scope: None,
            client_id: None,
            username: None,
            token_type: None,
            exp: None,
            iat: None,
            sub: None,
            aud: Vec::new(),
            iss: None,
            jti: None,
        }),
    }
}