summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-06-11 17:19:20 -0600
committermo khan <mo@mokhan.ca>2025-06-11 17:19:20 -0600
commit0ff7c18f2e0e4f72cf6354530329c1c915c6294a (patch)
tree21fbee2f14ccde4c415b1047726c9e0f5e298ef8
parent9b8a098bfcfdd73bfdfcff0cb397ef2694a90367 (diff)
refactor: extract domain model
-rw-r--r--src/domain/conversions.rs174
-rw-r--r--src/domain/mod.rs9
-rw-r--r--src/domain/models.rs223
-rw-r--r--src/domain/repositories.rs41
-rw-r--r--src/domain/services.rs63
-rw-r--r--src/lib.rs1
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
diff --git a/src/lib.rs b/src/lib.rs
index 2b25e8a..e1c1b97 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;