Authentication & Authorization
EventCore is authentication-agnostic but provides hooks for integrating your auth system.
Authentication Integration
Capturing User Identity
EventCore’s metadata system captures user identity for audit trails:
#![allow(unused)] fn main() { use eventcore::{CommandExecutor, UserId}; // Execute command with authenticated user let user_id = UserId::try_new("user@example.com")?; let result = executor .execute_as_user(command, user_id) .await?; }
Middleware Pattern
Implement authentication as middleware:
#![allow(unused)] fn main() { use axum::{ extract::State, http::StatusCode, middleware::Next, response::Response, }; async fn auth_middleware( State(auth): State<AuthService>, headers: HeaderMap, mut req: Request, next: Next, ) -> Result<Response, StatusCode> { // Extract and verify token let token = headers .get("Authorization") .and_then(|h| h.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?; let user = auth .verify_token(token) .await .map_err(|_| StatusCode::UNAUTHORIZED)?; // Add user to request extensions req.extensions_mut().insert(user); Ok(next.run(req).await) } }
Authorization Patterns
Stream-Level Authorization
Implement fine-grained access control:
#![allow(unused)] fn main() { #[async_trait] trait StreamAuthorization { async fn can_read(&self, user: &User, stream_id: &StreamId) -> bool; async fn can_write(&self, user: &User, stream_id: &StreamId) -> bool; } struct CommandAuthorizationLayer<A: StreamAuthorization> { auth: A, } impl<A: StreamAuthorization> CommandAuthorizationLayer<A> { async fn authorize_command( &self, command: &impl Command, user: &User, ) -> Result<(), AuthError> { // Check read permissions for stream_id in command.read_streams() { if !self.auth.can_read(user, &stream_id).await { return Err(AuthError::Forbidden(stream_id)); } } // Check write permissions for stream_id in command.write_streams() { if !self.auth.can_write(user, &stream_id).await { return Err(AuthError::Forbidden(stream_id)); } } Ok(()) } } }
Role-Based Access Control (RBAC)
#![allow(unused)] fn main() { #[derive(Debug, Clone)] enum Role { Admin, User, ReadOnly, } #[derive(Debug, Clone)] struct User { id: UserId, roles: Vec<Role>, } impl User { fn has_role(&self, role: &Role) -> bool { self.roles.contains(role) } fn can_execute_command(&self, command_type: &str) -> bool { match command_type { "CreateAccount" => self.has_role(&Role::Admin), "UpdateAccount" => { self.has_role(&Role::Admin) || self.has_role(&Role::User) } "ViewAccount" => true, // All authenticated users _ => false, } } } }
Attribute-Based Access Control (ABAC)
#![allow(unused)] fn main() { #[derive(Debug)] struct AccessContext { user: User, resource: Resource, action: Action, environment: Environment, } #[async_trait] trait AccessPolicy { async fn evaluate(&self, context: &AccessContext) -> Decision; } struct AbacAuthorizer { policies: Vec<Box<dyn AccessPolicy>>, } impl AbacAuthorizer { async fn authorize(&self, context: AccessContext) -> Result<(), AuthError> { for policy in &self.policies { match policy.evaluate(&context).await { Decision::Deny(reason) => { return Err(AuthError::PolicyDenied(reason)); } Decision::Allow => continue, } } Ok(()) } } }
Projection Security
Row-Level Security
Filter projections based on user permissions:
#![allow(unused)] fn main() { #[async_trait] impl ReadModelStore for SecureAccountStore { async fn get_account( &self, account_id: &AccountId, user: &User, ) -> Result<Option<AccountReadModel>> { let account = self.inner.get_account(account_id).await?; // Apply row-level security match account { Some(acc) if self.user_can_view(&acc, user) => Ok(Some(acc)), _ => Ok(None), } } async fn list_accounts( &self, user: &User, filter: AccountFilter, ) -> Result<Vec<AccountReadModel>> { let accounts = self.inner.list_accounts(filter).await?; // Filter based on permissions Ok(accounts .into_iter() .filter(|acc| self.user_can_view(acc, user)) .collect()) } } }
Field-Level Security
Redact sensitive fields:
#![allow(unused)] fn main() { impl AccountReadModel { fn redact_for_user(&self, user: &User) -> Self { let mut redacted = self.clone(); if !user.has_role(&Role::Admin) { redacted.ssn = None; redacted.tax_id = None; } if !user.has_role(&Role::Financial) { redacted.balance = None; redacted.credit_limit = None; } redacted } } }
Best Practices
- Fail Secure: Default to denying access
- Audit Everything: Log all authorization decisions
- Minimize Privileges: Grant only necessary permissions
- Separate Concerns: Keep auth logic separate from business logic
- Token Expiry: Implement short-lived tokens with refresh
- Rate Limiting: Prevent brute force attacks
Common Pitfalls
- Not checking permissions on read models
- Forgetting to validate token expiry
- Exposing internal IDs that enable enumeration
- Not rate limiting authentication attempts
- Storing permissions in events (they change over time)