2026-01-08 21:32:59 +00:00
|
|
|
//! Authentication and Authorization Module
|
|
|
|
|
//!
|
|
|
|
|
//! Comprehensive authentication system with JWT tokens, password hashing,
|
|
|
|
|
//! user management, and Cedar policy integration.
|
|
|
|
|
//!
|
|
|
|
|
//! # Architecture
|
|
|
|
|
//!
|
|
|
|
|
//! ```text
|
|
|
|
|
//! ┌─────────────────────────────────────────┐
|
|
|
|
|
//! │ Authentication Module │
|
|
|
|
|
//! ├─────────────────────────────────────────┤
|
|
|
|
|
//! │ │
|
|
|
|
|
//! │ ┌──────────────┐ ┌─────────────────┐ │
|
|
|
|
|
//! │ │ JWT Service │ │ Password Service│ │
|
|
|
|
|
//! │ │ │ │ │ │
|
|
|
|
|
//! │ │ • Generate │ │ • Hash (Argon2) │ │
|
|
|
|
|
//! │ │ • Validate │ │ • Verify │ │
|
|
|
|
|
//! │ │ • Rotate │ │ • Strength │ │
|
|
|
|
|
//! │ │ • Revoke │ └─────────────────┘ │
|
|
|
|
|
//! │ └──────────────┘ │
|
|
|
|
|
//! │ │ │
|
|
|
|
|
//! │ ▼ │
|
|
|
|
|
//! │ ┌──────────────┐ │
|
|
|
|
|
//! │ │ User Service │ │
|
|
|
|
|
//! │ │ │ │
|
|
|
|
|
//! │ │ • Create │ │
|
|
|
|
|
//! │ │ • Login │ │
|
|
|
|
|
//! │ │ • Logout │ │
|
|
|
|
|
//! │ │ • Update │ │
|
|
|
|
|
//! │ └──────────────┘ │
|
|
|
|
|
//! └─────────────────────────────────────────┘
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! # Token Flow
|
|
|
|
|
//!
|
|
|
|
|
//! ```text
|
|
|
|
|
//! 1. Login Request
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 2. Validate Credentials (Password Service)
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 3. Generate Token Pair (JWT Service)
|
|
|
|
|
//! ├─ Access Token (15 min)
|
|
|
|
|
//! └─ Refresh Token (7 days)
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 4. Return Token Pair
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 5. Client Uses Access Token
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 6. Access Token Expires
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 7. Rotate Using Refresh Token
|
|
|
|
|
//! ↓
|
|
|
|
|
//! 8. New Token Pair Generated
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! # Security Features
|
|
|
|
|
//!
|
|
|
|
|
//! - **RS256 Asymmetric Signing**: Enhanced security over symmetric HMAC
|
|
|
|
|
//! - **Token Rotation**: Automatic rotation before expiry
|
|
|
|
|
//! - **Token Revocation**: Blacklist-based revocation
|
|
|
|
|
//! - **Password Hashing**: Argon2id (memory-hard, side-channel resistant)
|
|
|
|
|
//! - **Cedar Integration**: Permissions hash for quick validation
|
|
|
|
|
//! - **Thread-Safe**: Arc + RwLock for concurrent access
|
|
|
|
|
//!
|
|
|
|
|
//! # Usage Example
|
|
|
|
|
//!
|
|
|
|
|
//! ```no_run
|
|
|
|
|
//! # use control_center::auth::{JwtService, PasswordService, User};
|
|
|
|
|
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
//! // Initialize services
|
|
|
|
|
//! let private_key = std::fs::read("keys/private.pem")?;
|
|
|
|
|
//! let public_key = std::fs::read("keys/public.pem")?;
|
|
|
|
|
//!
|
|
|
|
|
//! let jwt_service = JwtService::new(
|
|
|
|
|
//! &private_key,
|
|
|
|
|
//! &public_key,
|
|
|
|
|
//! "control-center",
|
|
|
|
|
//! vec!["orchestrator".to_string(), "cli".to_string()],
|
|
|
|
|
//! )?;
|
|
|
|
|
//!
|
|
|
|
|
//! let password_service = PasswordService::new();
|
|
|
|
|
//!
|
|
|
|
|
//! // Create user with hashed password
|
|
|
|
|
//! let password_hash = password_service.hash_password("secure_password")?;
|
|
|
|
|
//! let user = User {
|
|
|
|
|
//! id: "user123".to_string(),
|
|
|
|
|
//! username: "alice".to_string(),
|
|
|
|
|
//! password_hash,
|
|
|
|
|
//! // ... other fields
|
|
|
|
|
//! };
|
|
|
|
|
//!
|
|
|
|
|
//! // Login: verify password and generate tokens
|
|
|
|
|
//! if password_service.verify_password("secure_password", &user.password_hash)? {
|
|
|
|
|
//! let tokens = jwt_service.generate_token_pair(
|
|
|
|
|
//! &user.id,
|
|
|
|
|
//! "workspace1",
|
|
|
|
|
//! "permissions_hash",
|
|
|
|
|
//! None,
|
|
|
|
|
//! )?;
|
|
|
|
|
//!
|
|
|
|
|
//! println!("Access token: {}", tokens.access_token);
|
|
|
|
|
//! println!("Expires in: {} seconds", tokens.expires_in);
|
|
|
|
|
//! }
|
|
|
|
|
//!
|
|
|
|
|
//! // Validate token
|
|
|
|
|
//! let claims = jwt_service.validate_token(&tokens.access_token)?;
|
|
|
|
|
//! println!("User: {}, Workspace: {}", claims.sub, claims.workspace);
|
|
|
|
|
//!
|
|
|
|
|
//! // Rotate token before expiry
|
|
|
|
|
//! if claims.needs_rotation() {
|
|
|
|
|
//! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?;
|
|
|
|
|
//! println!("New access token: {}", new_tokens.access_token);
|
|
|
|
|
//! }
|
|
|
|
|
//!
|
|
|
|
|
//! // Revoke token (logout)
|
|
|
|
|
//! jwt_service.revoke_token(&claims.jti, claims.exp)?;
|
|
|
|
|
//! # Ok(())
|
|
|
|
|
//! # }
|
|
|
|
|
//! ```
|
|
|
|
|
|
|
|
|
|
pub mod jwt;
|
|
|
|
|
pub mod password;
|
|
|
|
|
pub mod user;
|
|
|
|
|
|
|
|
|
|
// Re-export commonly used types
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
pub use jwt::{BlacklistStats, JwtService, TokenClaims, TokenPair, TokenType};
|
|
|
|
|
pub use password::{PasswordService, PasswordStrength};
|
|
|
|
|
pub use user::{User, UserRole, UserService, UserStatus};
|
|
|
|
|
|
|
|
|
|
use crate::error::{auth, http, infrastructure, ControlCenterError, Result};
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
use crate::mfa::MfaService;
|
|
|
|
|
|
|
|
|
|
/// Unified authentication service combining JWT, password, and MFA management
|
|
|
|
|
#[allow(clippy::manual_non_exhaustive)]
|
|
|
|
|
pub struct AuthService {
|
|
|
|
|
/// JWT token service
|
|
|
|
|
pub jwt: Arc<JwtService>,
|
|
|
|
|
|
|
|
|
|
/// Password management service
|
|
|
|
|
pub password: Arc<PasswordService>,
|
|
|
|
|
|
|
|
|
|
/// User management service
|
|
|
|
|
pub user: Arc<UserService>,
|
|
|
|
|
|
|
|
|
|
/// MFA service (optional)
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
pub mfa: Option<Arc<MfaService>>,
|
|
|
|
|
|
|
|
|
|
/// MFA service placeholder when feature is disabled
|
|
|
|
|
#[cfg(not(feature = "mfa"))]
|
|
|
|
|
_mfa_placeholder: (),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AuthService {
|
|
|
|
|
/// Create new authentication service
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
/// * `jwt_service` - JWT token service
|
|
|
|
|
/// * `password_service` - Password management service
|
|
|
|
|
/// * `user_service` - User management service
|
|
|
|
|
pub fn new(
|
|
|
|
|
jwt_service: JwtService,
|
|
|
|
|
password_service: PasswordService,
|
|
|
|
|
user_service: UserService,
|
|
|
|
|
) -> Self {
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
{
|
|
|
|
|
Self {
|
|
|
|
|
jwt: Arc::new(jwt_service),
|
|
|
|
|
password: Arc::new(password_service),
|
|
|
|
|
user: Arc::new(user_service),
|
|
|
|
|
mfa: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(feature = "mfa"))]
|
|
|
|
|
{
|
|
|
|
|
Self {
|
|
|
|
|
jwt: Arc::new(jwt_service),
|
|
|
|
|
password: Arc::new(password_service),
|
|
|
|
|
user: Arc::new(user_service),
|
|
|
|
|
_mfa_placeholder: (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create new authentication service with MFA support
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
pub fn with_mfa(
|
|
|
|
|
jwt_service: JwtService,
|
|
|
|
|
password_service: PasswordService,
|
|
|
|
|
user_service: UserService,
|
|
|
|
|
mfa_service: MfaService,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
jwt: Arc::new(jwt_service),
|
|
|
|
|
password: Arc::new(password_service),
|
|
|
|
|
user: Arc::new(user_service),
|
|
|
|
|
mfa: Some(Arc::new(mfa_service)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Authenticate user and generate token pair
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
/// * `username` - Username or email
|
|
|
|
|
/// * `password` - Plain text password
|
|
|
|
|
/// * `workspace` - Workspace identifier
|
|
|
|
|
///
|
|
|
|
|
/// # Returns
|
|
|
|
|
/// Token pair on successful authentication (or partial token if MFA
|
|
|
|
|
/// required)
|
|
|
|
|
pub async fn login(
|
|
|
|
|
&self,
|
|
|
|
|
username: &str,
|
|
|
|
|
password: &str,
|
|
|
|
|
workspace: &str,
|
|
|
|
|
) -> Result<TokenPair> {
|
|
|
|
|
// Find user
|
|
|
|
|
let user = self.user.find_by_username(username).await?.ok_or_else(|| {
|
|
|
|
|
ControlCenterError::Auth(auth::AuthError::Authentication(
|
|
|
|
|
"Invalid credentials".to_string(),
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Check user status
|
|
|
|
|
if user.status != UserStatus::Active {
|
|
|
|
|
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
|
|
|
|
|
format!("User account is {}", user.status),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify password
|
|
|
|
|
if !self
|
|
|
|
|
.password
|
|
|
|
|
.verify_password(password, &user.password_hash)?
|
|
|
|
|
{
|
|
|
|
|
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
|
|
|
|
|
"Invalid credentials".to_string(),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if MFA is required
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
{
|
|
|
|
|
if let Some(ref mfa_service) = self.mfa {
|
|
|
|
|
if mfa_service.user_has_mfa(&user.id).await? {
|
|
|
|
|
// Return partial token - user must complete MFA
|
|
|
|
|
// The token has limited permissions and expires in 5 minutes
|
|
|
|
|
let mut metadata = HashMap::new();
|
|
|
|
|
metadata.insert("mfa_required".to_string(), "true".to_string());
|
|
|
|
|
metadata.insert("ttl".to_string(), "300".to_string());
|
|
|
|
|
return self.jwt.generate_token_pair(
|
|
|
|
|
&user.id,
|
|
|
|
|
workspace,
|
|
|
|
|
"mfa_pending",
|
|
|
|
|
Some(metadata),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update last login
|
|
|
|
|
self.user.update_last_login(&user.id).await?;
|
|
|
|
|
|
|
|
|
|
// Generate permissions hash from user roles
|
|
|
|
|
let permissions_hash = self.generate_permissions_hash(&user.roles);
|
|
|
|
|
|
|
|
|
|
// Generate token pair
|
|
|
|
|
self.jwt
|
|
|
|
|
.generate_token_pair(&user.id, workspace, &permissions_hash, None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Complete MFA verification and upgrade partial token to full access
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
/// * `partial_token` - Partial access token from initial login
|
|
|
|
|
/// * `mfa_code` - MFA verification code (TOTP or backup code)
|
|
|
|
|
///
|
|
|
|
|
/// # Returns
|
|
|
|
|
/// Full access token pair on successful MFA verification
|
|
|
|
|
#[cfg(feature = "mfa")]
|
|
|
|
|
pub async fn complete_mfa_login(
|
|
|
|
|
&self,
|
|
|
|
|
partial_token: &str,
|
|
|
|
|
mfa_code: &str,
|
|
|
|
|
) -> Result<TokenPair> {
|
|
|
|
|
// Validate partial token
|
|
|
|
|
let claims = self.jwt.validate_token(partial_token)?;
|
|
|
|
|
|
|
|
|
|
// Verify it's a pending MFA token
|
|
|
|
|
if claims.permissions_hash != "mfa_pending" {
|
|
|
|
|
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
|
|
|
|
|
"Invalid MFA token".to_string(),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get MFA service
|
|
|
|
|
let mfa_service = self.mfa.as_ref().ok_or_else(|| {
|
|
|
|
|
ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Internal(
|
|
|
|
|
"MFA not configured".to_string(),
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Get user for email
|
|
|
|
|
let user = self.user.find_by_id(&claims.sub).await?.ok_or_else(|| {
|
|
|
|
|
ControlCenterError::Http(http::HttpError::NotFound("User not found".to_string()))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Verify MFA code
|
|
|
|
|
let verification = mfa_service
|
|
|
|
|
.verify_any_mfa(&user.id, &user.email, mfa_code)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if !verification.verified {
|
|
|
|
|
return Err(ControlCenterError::Auth(auth::AuthError::Authentication(
|
|
|
|
|
"Invalid MFA code".to_string(),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Revoke partial token
|
|
|
|
|
self.jwt.revoke_token(&claims.jti, claims.exp)?;
|
|
|
|
|
|
|
|
|
|
// Update last login
|
|
|
|
|
self.user.update_last_login(&user.id).await?;
|
|
|
|
|
|
|
|
|
|
// Generate full permissions hash
|
|
|
|
|
let permissions_hash = self.generate_permissions_hash(&user.roles);
|
|
|
|
|
|
|
|
|
|
// Generate full access token pair
|
|
|
|
|
self.jwt
|
|
|
|
|
.generate_token_pair(&user.id, &claims.workspace, &permissions_hash, None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Logout user by revoking tokens
|
|
|
|
|
pub async fn logout(&self, access_token: &str) -> Result<()> {
|
|
|
|
|
let claims = self.jwt.validate_token(access_token)?;
|
|
|
|
|
self.jwt.revoke_token(&claims.jti, claims.exp)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate access token and return claims
|
|
|
|
|
pub fn validate(&self, token: &str) -> Result<TokenClaims> {
|
|
|
|
|
self.jwt.validate_token(token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rotate tokens using refresh token
|
|
|
|
|
pub fn refresh(&self, refresh_token: &str) -> Result<TokenPair> {
|
|
|
|
|
self.jwt.rotate_token(refresh_token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate permissions hash from user roles
|
|
|
|
|
///
|
|
|
|
|
/// This creates a stable hash of the user's Cedar permissions
|
|
|
|
|
/// for quick validation without policy evaluation
|
|
|
|
|
fn generate_permissions_hash(&self, roles: &[UserRole]) -> String {
|
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
|
|
|
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
for role in roles {
|
|
|
|
|
hasher.update(role.to_string().as_bytes());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
format!("{:x}", hasher.finalize())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn create_test_jwt_service() -> JwtService {
|
2026-01-12 04:53:31 +00:00
|
|
|
// Pre-generated RSA keys to avoid runtime key generation (avoids rand_core
|
|
|
|
|
// conflict)
|
2026-01-08 21:32:59 +00:00
|
|
|
let private_pem = b"-----BEGIN PRIVATE KEY-----
|
|
|
|
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k
|
|
|
|
|
DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5
|
|
|
|
|
K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI
|
|
|
|
|
YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M
|
|
|
|
|
H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj
|
|
|
|
|
G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK
|
|
|
|
|
R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv
|
|
|
|
|
1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu
|
|
|
|
|
E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp
|
|
|
|
|
R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW
|
|
|
|
|
B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW
|
|
|
|
|
2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu
|
|
|
|
|
BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX
|
|
|
|
|
nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP
|
|
|
|
|
b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x
|
|
|
|
|
H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2
|
|
|
|
|
Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt
|
|
|
|
|
1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3
|
|
|
|
|
fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k
|
|
|
|
|
wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl
|
|
|
|
|
qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo
|
|
|
|
|
0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm
|
|
|
|
|
4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO
|
|
|
|
|
9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq
|
|
|
|
|
JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu
|
|
|
|
|
LfEK1YMqL/VvAJdIXxqZqDXf
|
|
|
|
|
-----END PRIVATE KEY-----";
|
|
|
|
|
|
|
|
|
|
let public_pem = b"-----BEGIN PUBLIC KEY-----
|
|
|
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD
|
|
|
|
|
RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh
|
|
|
|
|
/DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu
|
|
|
|
|
ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E
|
|
|
|
|
PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF
|
|
|
|
|
WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx
|
|
|
|
|
IQIDAQAB
|
|
|
|
|
-----END PUBLIC KEY-----";
|
|
|
|
|
|
|
|
|
|
JwtService::new(
|
|
|
|
|
private_pem,
|
|
|
|
|
public_pem,
|
|
|
|
|
"test-issuer",
|
|
|
|
|
vec!["test-audience".to_string()],
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_auth_service_creation() {
|
|
|
|
|
let jwt_service = create_test_jwt_service();
|
|
|
|
|
let password_service = PasswordService::new();
|
|
|
|
|
let user_service = UserService::new();
|
|
|
|
|
|
|
|
|
|
let auth_service = AuthService::new(jwt_service, password_service, user_service);
|
|
|
|
|
|
|
|
|
|
assert!(Arc::strong_count(&auth_service.jwt) >= 1);
|
|
|
|
|
assert!(Arc::strong_count(&auth_service.password) >= 1);
|
|
|
|
|
assert!(Arc::strong_count(&auth_service.user) >= 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_permissions_hash_generation() {
|
|
|
|
|
let jwt_service = create_test_jwt_service();
|
|
|
|
|
let password_service = PasswordService::new();
|
|
|
|
|
let user_service = UserService::new();
|
|
|
|
|
|
|
|
|
|
let auth_service = AuthService::new(jwt_service, password_service, user_service);
|
|
|
|
|
|
|
|
|
|
let roles = vec![UserRole::Admin, UserRole::Developer];
|
|
|
|
|
let hash1 = auth_service.generate_permissions_hash(&roles);
|
|
|
|
|
let hash2 = auth_service.generate_permissions_hash(&roles);
|
|
|
|
|
|
|
|
|
|
// Hash should be deterministic
|
|
|
|
|
assert_eq!(hash1, hash2);
|
|
|
|
|
assert_eq!(hash1.len(), 64); // SHA256 hex = 64 chars
|
|
|
|
|
}
|
|
|
|
|
}
|