corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Authentication module with JWT support

use crate::{CorteqError, Result};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// JWT claims with tenant context
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
    /// Subject (user ID)
    pub sub: Uuid,

    /// Tenant ID - CRITICAL: Embedded in token for security
    pub tenant_id: Uuid,

    /// User roles
    pub roles: Vec<String>,

    /// Expiration time (Unix timestamp)
    pub exp: i64,

    /// Issued at (Unix timestamp)
    pub iat: i64,
}

/// JWT service for token creation and validation
#[derive(Clone)]
pub struct JwtService {
    encoding_key: EncodingKey,
    decoding_key: DecodingKey,
    validation: Validation,
}

impl std::fmt::Debug for JwtService {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("JwtService")
            .field("validation", &self.validation)
            .finish_non_exhaustive()
    }
}

impl JwtService {
    /// Create a new JWT service with the given secret
    pub fn new(secret: &[u8]) -> Self {
        Self {
            encoding_key: EncodingKey::from_secret(secret),
            decoding_key: DecodingKey::from_secret(secret),
            validation: Validation::default(),
        }
    }

    /// Encode claims into a JWT token
    pub fn encode(&self, claims: &Claims) -> Result<String> {
        jsonwebtoken::encode(&Header::default(), claims, &self.encoding_key)
            .map_err(CorteqError::from)
    }

    /// Decode and validate a JWT token
    pub fn decode(&self, token: &str) -> Result<Claims> {
        let token_data =
            jsonwebtoken::decode::<Claims>(token, &self.decoding_key, &self.validation)?;

        Ok(token_data.claims)
    }

    /// Validate that a token belongs to a specific tenant
    pub fn validate_tenant(&self, token: &str, expected_tenant_id: Uuid) -> Result<Claims> {
        let claims = self.decode(token)?;

        if claims.tenant_id != expected_tenant_id {
            return Err(CorteqError::AuthorizationFailed(
                "Token tenant mismatch".to_string(),
            ));
        }

        Ok(claims)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    #[test]
    fn test_jwt_encode_decode() {
        let service = JwtService::new(b"test-secret");

        let claims = Claims {
            sub: Uuid::new_v4(),
            tenant_id: Uuid::new_v4(),
            roles: vec!["user".to_string()],
            exp: (Utc::now() + chrono::Duration::hours(1)).timestamp(),
            iat: Utc::now().timestamp(),
        };

        let token = service.encode(&claims).unwrap();
        let decoded = service.decode(&token).unwrap();

        assert_eq!(claims.sub, decoded.sub);
        assert_eq!(claims.tenant_id, decoded.tenant_id);
    }

    #[test]
    fn test_tenant_validation() {
        let service = JwtService::new(b"test-secret");
        let tenant_id = Uuid::new_v4();

        let claims = Claims {
            sub: Uuid::new_v4(),
            tenant_id,
            roles: vec!["user".to_string()],
            exp: (Utc::now() + chrono::Duration::hours(1)).timestamp(),
            iat: Utc::now().timestamp(),
        };

        let token = service.encode(&claims).unwrap();

        // Should succeed with correct tenant
        assert!(service.validate_tenant(&token, tenant_id).is_ok());

        // Should fail with wrong tenant
        let wrong_tenant = Uuid::new_v4();
        assert!(service.validate_tenant(&token, wrong_tenant).is_err());
    }
}