sunbeam-g2v 0.4.0

Sunbeam Service Framework - A ConnectRPC-based framework for building microservices
//! Session authentication primitives.

use async_trait::async_trait;
use serde_json::Value;

use super::error::AuthError;

/// Abstracts validation of a browser session or session token.
#[async_trait]
pub trait SessionClient: Send + Sync + 'static {
    /// Validate the session and return the raw session payload.
    async fn to_session(
        &self,
        cookie: Option<&str>,
        token: Option<&str>,
    ) -> Result<Value, AuthError>;
}

/// Resolved identity mapping for an upstream identity.
#[derive(Debug, Clone)]
pub struct IdentityMapping {
    /// Tenant that owns the identity.
    pub tenant_id: String,
    /// Local identity identifier (e.g. the sso-gateway public id).
    pub identity_id: String,
}

/// Maps an external identity to a local tenant + identity id.
#[async_trait]
pub trait IdentityMappingStore: Send + Sync + 'static {
    /// Resolve the mapping for the given upstream identity.
    ///
    /// `backend` identifies the identity provider (e.g. `"kratos"`).
    async fn get_identity_mapping(
        &self,
        backend: &str,
        identity_id: &str,
    ) -> Result<Option<IdentityMapping>, AuthError>;
}

/// Kratos-backed session client.
#[derive(Debug, Clone)]
pub struct KratosSessionClient {
    client: reqwest::Client,
    public_url: String,
}

impl KratosSessionClient {
    /// Create a client pointing at the Kratos public endpoint.
    pub fn new(public_url: impl Into<String>) -> Self {
        Self {
            client: reqwest::Client::new(),
            public_url: public_url.into(),
        }
    }
}

#[async_trait]
impl SessionClient for KratosSessionClient {
    async fn to_session(
        &self,
        cookie: Option<&str>,
        token: Option<&str>,
    ) -> Result<Value, AuthError> {
        let url = format!("{}/sessions/whoami", self.public_url.trim_end_matches('/'));
        let mut req = self.client.get(&url).header("accept", "application/json");

        if let Some(token) = token {
            req = req.header("X-Session-Token", token);
        }
        if let Some(cookie) = cookie {
            req = req.header("Cookie", cookie);
        }

        let response = req
            .send()
            .await
            .map_err(|e| AuthError::InvalidSession(e.to_string()))?;

        if response.status().is_success() {
            response
                .json()
                .await
                .map_err(|e| AuthError::InvalidSession(e.to_string()))
        } else {
            Err(AuthError::InvalidSession(format!(
                "session validation returned {}",
                response.status()
            )))
        }
    }
}