diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/domain/conversions.rs | 174 | ||||
| -rw-r--r-- | src/domain/mod.rs | 9 | ||||
| -rw-r--r-- | src/domain/models.rs | 223 | ||||
| -rw-r--r-- | src/domain/repositories.rs | 41 | ||||
| -rw-r--r-- | src/domain/services.rs | 63 | ||||
| -rw-r--r-- | src/lib.rs | 1 |
6 files changed, 511 insertions, 0 deletions
diff --git a/src/domain/conversions.rs b/src/domain/conversions.rs new file mode 100644 index 0000000..53e6062 --- /dev/null +++ b/src/domain/conversions.rs @@ -0,0 +1,174 @@ +use crate::database::{DbAccessToken, DbAuthCode, DbAuditLog, DbOAuthClient}; +use crate::domain::models::*; +use anyhow::Result; + +/// Trait for converting from database models to domain models +pub trait FromDb<T> { + fn from_db(db_model: T) -> Result<Self> + where + Self: Sized; +} + +/// Trait for converting from domain models to database models +pub trait ToDb<T> { + fn to_db(&self) -> Result<T>; +} + +// OAuth Client conversions +impl FromDb<DbOAuthClient> for OAuthClient { + fn from_db(db_client: DbOAuthClient) -> Result<Self> { + let redirect_uris: Vec<String> = serde_json::from_str(&db_client.redirect_uris)?; + let scopes: Vec<String> = db_client.scopes.split_whitespace().map(|s| s.to_string()).collect(); + let grant_types: Vec<String> = db_client.grant_types.split_whitespace().map(|s| s.to_string()).collect(); + let response_types: Vec<String> = db_client.response_types.split_whitespace().map(|s| s.to_string()).collect(); + + Ok(OAuthClient { + client_id: db_client.client_id, + client_name: db_client.client_name, + redirect_uris, + scopes, + grant_types, + response_types, + is_active: db_client.is_active, + created_at: db_client.created_at, + updated_at: db_client.updated_at, + }) + } +} + +impl ToDb<DbOAuthClient> for OAuthClient { + fn to_db(&self) -> Result<DbOAuthClient> { + Ok(DbOAuthClient { + id: 0, // Will be set by database + client_id: self.client_id.clone(), + client_secret_hash: String::new(), // Will be set separately + client_name: self.client_name.clone(), + redirect_uris: serde_json::to_string(&self.redirect_uris)?, + scopes: self.scopes.join(" "), + grant_types: self.grant_types.join(" "), + response_types: self.response_types.join(" "), + created_at: self.created_at, + updated_at: self.updated_at, + is_active: self.is_active, + }) + } +} + +// Authorization Code conversions +impl FromDb<DbAuthCode> for AuthorizationCode { + fn from_db(db_code: DbAuthCode) -> Result<Self> { + let scopes = db_code.scope + .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + .unwrap_or_default(); + + Ok(AuthorizationCode { + code: db_code.code, + client_id: db_code.client_id, + user_id: db_code.user_id, + redirect_uri: db_code.redirect_uri, + scopes, + expires_at: db_code.expires_at, + created_at: db_code.created_at, + is_used: db_code.is_used, + code_challenge: db_code.code_challenge, + code_challenge_method: db_code.code_challenge_method, + }) + } +} + +impl ToDb<DbAuthCode> for AuthorizationCode { + fn to_db(&self) -> Result<DbAuthCode> { + let scope = if self.scopes.is_empty() { + None + } else { + Some(self.scopes.join(" ")) + }; + + Ok(DbAuthCode { + id: 0, // Will be set by database + code: self.code.clone(), + client_id: self.client_id.clone(), + user_id: self.user_id.clone(), + redirect_uri: self.redirect_uri.clone(), + scope, + expires_at: self.expires_at, + created_at: self.created_at, + is_used: self.is_used, + code_challenge: self.code_challenge.clone(), + code_challenge_method: self.code_challenge_method.clone(), + }) + } +} + +// Access Token conversions +impl FromDb<DbAccessToken> for AccessToken { + fn from_db(db_token: DbAccessToken) -> Result<Self> { + let scopes = db_token.scope + .map(|s| s.split_whitespace().map(|scope| scope.to_string()).collect()) + .unwrap_or_default(); + + Ok(AccessToken { + token_id: db_token.token_id, + client_id: db_token.client_id, + user_id: db_token.user_id, + scopes, + expires_at: db_token.expires_at, + created_at: db_token.created_at, + is_revoked: db_token.is_revoked, + }) + } +} + +impl ToDb<DbAccessToken> for AccessToken { + fn to_db(&self) -> Result<DbAccessToken> { + let scope = if self.scopes.is_empty() { + None + } else { + Some(self.scopes.join(" ")) + }; + + Ok(DbAccessToken { + id: 0, // Will be set by database + token_id: self.token_id.clone(), + client_id: self.client_id.clone(), + user_id: self.user_id.clone(), + scope, + expires_at: self.expires_at, + created_at: self.created_at, + is_revoked: self.is_revoked, + token_hash: String::new(), // Will be set by service layer + }) + } +} + +// Audit Event conversions +impl FromDb<DbAuditLog> for AuditEvent { + fn from_db(db_log: DbAuditLog) -> Result<Self> { + Ok(AuditEvent { + event_type: db_log.event_type, + client_id: db_log.client_id, + user_id: db_log.user_id, + ip_address: db_log.ip_address, + user_agent: db_log.user_agent, + details: db_log.details, + success: db_log.success, + timestamp: db_log.created_at, + }) + } +} + +impl ToDb<DbAuditLog> for AuditEvent { + fn to_db(&self) -> Result<DbAuditLog> { + Ok(DbAuditLog { + id: 0, // Will be set by database + event_type: self.event_type.clone(), + client_id: self.client_id.clone(), + user_id: self.user_id.clone(), + ip_address: self.ip_address.clone(), + user_agent: self.user_agent.clone(), + details: self.details.clone(), + created_at: self.timestamp, + success: self.success, + }) + } +}
\ No newline at end of file diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..168ec01 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,9 @@ +pub mod conversions; +pub mod models; +pub mod repositories; +pub mod services; + +pub use conversions::*; +pub use models::*; +pub use repositories::*; +pub use services::*;
\ No newline at end of file diff --git a/src/domain/models.rs b/src/domain/models.rs new file mode 100644 index 0000000..85b554f --- /dev/null +++ b/src/domain/models.rs @@ -0,0 +1,223 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Core domain model for OAuth2 clients +#[derive(Debug, Clone, PartialEq)] +pub struct OAuthClient { + pub client_id: String, + pub client_name: String, + pub redirect_uris: Vec<String>, + pub scopes: Vec<String>, + pub grant_types: Vec<String>, + pub response_types: Vec<String>, + pub is_active: bool, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Core domain model for authorization codes +#[derive(Debug, Clone, PartialEq)] +pub struct AuthorizationCode { + pub code: String, + pub client_id: String, + pub user_id: String, + pub redirect_uri: String, + pub scopes: Vec<String>, + pub expires_at: DateTime<Utc>, + pub created_at: DateTime<Utc>, + pub is_used: bool, + // PKCE fields + pub code_challenge: Option<String>, + pub code_challenge_method: Option<String>, +} + +/// Core domain model for access tokens +#[derive(Debug, Clone, PartialEq)] +pub struct AccessToken { + pub token_id: String, + pub client_id: String, + pub user_id: String, + pub scopes: Vec<String>, + pub expires_at: DateTime<Utc>, + pub created_at: DateTime<Utc>, + pub is_revoked: bool, +} + +/// Core domain model for refresh tokens +#[derive(Debug, Clone, PartialEq)] +pub struct RefreshToken { + pub token_id: String, + pub access_token_id: String, + pub client_id: String, + pub user_id: String, + pub scopes: Vec<String>, + pub expires_at: DateTime<Utc>, + pub created_at: DateTime<Utc>, + pub is_revoked: bool, +} + +/// Domain model for audit events +#[derive(Debug, Clone, PartialEq)] +pub struct AuditEvent { + pub event_type: String, + pub client_id: Option<String>, + pub user_id: Option<String>, + pub ip_address: Option<String>, + pub user_agent: Option<String>, + pub details: Option<String>, + pub success: bool, + pub timestamp: DateTime<Utc>, +} + +/// Domain model for rate limiting +#[derive(Debug, Clone, PartialEq)] +pub struct RateLimit { + pub identifier: String, + pub endpoint: String, + pub count: u32, + pub window_start: DateTime<Utc>, + pub window_duration_minutes: u32, +} + +/// JWT token claims for domain use +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TokenClaims { + pub sub: String, // Subject (user ID) + pub iss: String, // Issuer + pub aud: String, // Audience (client ID) + pub exp: u64, // Expiration time + pub iat: u64, // Issued at + pub jti: String, // JWT ID (token ID) + pub scope: Option<String>, // Granted scopes +} + +/// OAuth2 authorization request +#[derive(Debug, Clone, PartialEq)] +pub struct AuthorizationRequest { + pub client_id: String, + pub redirect_uri: String, + pub response_type: String, + pub scope: Option<String>, + pub state: Option<String>, + // PKCE + pub code_challenge: Option<String>, + pub code_challenge_method: Option<String>, +} + +/// OAuth2 token request +#[derive(Debug, Clone, PartialEq)] +pub struct TokenRequest { + pub grant_type: String, + pub code: Option<String>, // For authorization_code grant + pub refresh_token: Option<String>, // For refresh_token grant + pub redirect_uri: Option<String>, // For authorization_code grant + pub client_id: String, + pub client_secret: Option<String>, + // PKCE + pub code_verifier: Option<String>, +} + +/// Result of successful authorization +#[derive(Debug, Clone, PartialEq)] +pub struct AuthorizationResult { + pub redirect_url: String, +} + +/// Result of successful token generation +#[derive(Debug, Clone, PartialEq)] +pub struct TokenResult { + pub access_token: String, + pub token_type: String, + pub expires_in: u64, + pub refresh_token: Option<String>, + pub scope: Option<String>, +} + +/// OAuth2 error with domain context +#[derive(Debug, Clone, PartialEq)] +pub struct OAuthError { + pub error_code: String, + pub description: Option<String>, + pub uri: Option<String>, +} + +impl OAuthError { + pub fn invalid_request(description: &str) -> Self { + Self { + error_code: "invalid_request".to_string(), + description: Some(description.to_string()), + uri: None, + } + } + + pub fn invalid_client(description: &str) -> Self { + Self { + error_code: "invalid_client".to_string(), + description: Some(description.to_string()), + uri: None, + } + } + + pub fn invalid_grant(description: &str) -> Self { + Self { + error_code: "invalid_grant".to_string(), + description: Some(description.to_string()), + uri: None, + } + } + + pub fn unsupported_grant_type(description: &str) -> Self { + Self { + error_code: "unsupported_grant_type".to_string(), + description: Some(description.to_string()), + uri: None, + } + } + + pub fn server_error(description: &str) -> Self { + Self { + error_code: "server_error".to_string(), + description: Some(description.to_string()), + uri: None, + } + } +} + +/// User representation for OAuth2 flows +#[derive(Debug, Clone, PartialEq)] +pub struct User { + pub id: String, + pub username: Option<String>, + pub email: Option<String>, + pub is_active: bool, +} + +/// Scope representation +#[derive(Debug, Clone, PartialEq)] +pub struct Scope { + pub name: String, + pub description: Option<String>, +} + +impl Scope { + pub fn openid() -> Self { + Self { + name: "openid".to_string(), + description: Some("OpenID Connect scope".to_string()), + } + } + + pub fn profile() -> Self { + Self { + name: "profile".to_string(), + description: Some("Access to user profile information".to_string()), + } + } + + pub fn email() -> Self { + Self { + name: "email".to_string(), + description: Some("Access to user email address".to_string()), + } + } +}
\ No newline at end of file diff --git a/src/domain/repositories.rs b/src/domain/repositories.rs new file mode 100644 index 0000000..1aa4f33 --- /dev/null +++ b/src/domain/repositories.rs @@ -0,0 +1,41 @@ +use crate::domain::models::*; +use anyhow::Result; + +/// Domain-focused repository for OAuth clients +pub trait DomainClientRepository: Send + Sync { + fn get_client(&self, client_id: &str) -> Result<Option<OAuthClient>>; + fn create_client(&self, client: &OAuthClient, client_secret_hash: &str) -> Result<()>; + fn update_client(&self, client: &OAuthClient) -> Result<()>; + fn delete_client(&self, client_id: &str) -> Result<()>; + fn list_clients(&self) -> Result<Vec<OAuthClient>>; +} + +/// Domain-focused repository for authorization codes +pub trait DomainAuthCodeRepository: Send + Sync { + fn create_auth_code(&self, code: &AuthorizationCode) -> Result<()>; + fn get_auth_code(&self, code: &str) -> Result<Option<AuthorizationCode>>; + fn mark_auth_code_used(&self, code: &str) -> Result<()>; + fn cleanup_expired_codes(&self) -> Result<()>; +} + +/// Domain-focused repository for access tokens +pub trait DomainTokenRepository: Send + Sync { + fn create_access_token(&self, token: &AccessToken, token_hash: &str) -> Result<()>; + fn get_access_token(&self, token_hash: &str) -> Result<Option<AccessToken>>; + fn revoke_access_token(&self, token_hash: &str) -> Result<()>; + fn cleanup_expired_tokens(&self) -> Result<()>; +} + +/// Domain-focused repository for audit events +pub trait DomainAuditRepository: Send + Sync { + fn create_audit_event(&self, event: &AuditEvent) -> Result<()>; + fn get_audit_events(&self, limit: Option<u32>) -> Result<Vec<AuditEvent>>; + fn cleanup_old_audit_events(&self, days: u32) -> Result<()>; +} + +/// Domain-focused repository for rate limiting +pub trait DomainRateRepository: Send + Sync { + fn check_rate_limit(&self, limit: &RateLimit) -> Result<u32>; // Returns current count + fn increment_rate_limit(&self, identifier: &str, endpoint: &str, window_minutes: u32) -> Result<u32>; + fn cleanup_old_rate_limits(&self) -> Result<()>; +}
\ No newline at end of file diff --git a/src/domain/services.rs b/src/domain/services.rs new file mode 100644 index 0000000..2c23cdc --- /dev/null +++ b/src/domain/services.rs @@ -0,0 +1,63 @@ +use crate::domain::models::*; +use anyhow::Result; + +/// Domain service for OAuth2 authorization flow +pub trait AuthorizationService: Send + Sync { + fn authorize(&self, request: &AuthorizationRequest, user: &User) -> Result<AuthorizationResult, OAuthError>; + fn validate_client(&self, client_id: &str) -> Result<OAuthClient, OAuthError>; + fn validate_redirect_uri(&self, client: &OAuthClient, redirect_uri: &str) -> Result<(), OAuthError>; + fn validate_scopes(&self, client: &OAuthClient, requested_scopes: &[String]) -> Result<Vec<String>, OAuthError>; +} + +/// Domain service for OAuth2 token operations +pub trait TokenService: Send + Sync { + fn exchange_code_for_tokens(&self, request: &TokenRequest) -> Result<TokenResult, OAuthError>; + fn refresh_tokens(&self, request: &TokenRequest) -> Result<TokenResult, OAuthError>; + fn introspect_token(&self, token: &str, client_id: &str) -> Result<TokenClaims, OAuthError>; + fn revoke_token(&self, token: &str, client_id: &str) -> Result<(), OAuthError>; +} + +/// Domain service for client management +pub trait ClientService: Send + Sync { + fn create_client(&self, client: &OAuthClient, client_secret: &str) -> Result<()>; + fn get_client(&self, client_id: &str) -> Result<Option<OAuthClient>>; + fn update_client(&self, client: &OAuthClient) -> Result<()>; + fn delete_client(&self, client_id: &str) -> Result<()>; + fn authenticate_client(&self, client_id: &str, client_secret: &str) -> Result<OAuthClient, OAuthError>; +} + +/// Domain service for user management +pub trait UserService: Send + Sync { + fn get_user(&self, user_id: &str) -> Result<Option<User>>; + fn authenticate_user(&self, username: &str, password: &str) -> Result<User, OAuthError>; + fn is_user_authorized(&self, user: &User, client: &OAuthClient, scopes: &[String]) -> Result<bool>; +} + +/// Domain service for audit logging +pub trait AuditService: Send + Sync { + fn log_authorization_attempt(&self, request: &AuthorizationRequest, user: Option<&User>, success: bool, ip_address: Option<&str>) -> Result<()>; + fn log_token_request(&self, request: &TokenRequest, success: bool, ip_address: Option<&str>) -> Result<()>; + fn log_token_introspection(&self, token_hash: &str, client_id: &str, success: bool) -> Result<()>; + fn log_token_revocation(&self, token_hash: &str, client_id: &str, success: bool) -> Result<()>; +} + +/// Domain service for rate limiting +pub trait RateLimitService: Send + Sync { + fn check_rate_limit(&self, identifier: &str, endpoint: &str) -> Result<(), OAuthError>; + fn is_rate_limited(&self, identifier: &str, endpoint: &str, max_requests: u32, window_minutes: u32) -> Result<bool>; +} + +/// Domain service for PKCE operations +pub trait PkceService: Send + Sync { + fn generate_code_verifier(&self) -> String; + fn generate_code_challenge(&self, verifier: &str, method: &str) -> Result<String>; + fn verify_code_challenge(&self, verifier: &str, challenge: &str, method: &str) -> Result<bool>; +} + +/// Domain service for JWT operations +pub trait JwtService: Send + Sync { + fn generate_access_token(&self, claims: &TokenClaims) -> Result<String>; + fn generate_refresh_token(&self, client_id: &str, user_id: &str, scopes: &[String]) -> Result<String>; + fn validate_token(&self, token: &str) -> Result<TokenClaims>; + fn get_jwks(&self) -> Result<String>; // JSON Web Key Set +}
\ No newline at end of file @@ -2,6 +2,7 @@ pub mod clients; pub mod config; pub mod container; pub mod database; +pub mod domain; pub mod http; pub mod keys; pub mod migrations; |
