Domain Layer
Context
The hdc-auth-proxy project is an authentication/authorization proxy for the Humanitarian Data Cube (HDC) at WFP. It protects S3 resources, HTTP endpoints and applications. It supports OAuth2/OIDC, API keys and JWT. Actors are human users (browser, session-based) and M2M services (stateless). Identity Providers are AWS Cognito and federated CIAM for partners.
This document defines the domain layer — pure domain models with no external dependencies.
Bounded Contexts (4)
| Context |
Purpose |
Key question |
| Identity |
WHO is making the request |
Human user or M2M service? From which IdP? |
| Authentication |
HOW identity was proven |
Valid JWT? Correct API key? OAuth2 token? |
| Authorization |
WHAT they are allowed to do |
Do they have permissions for this resource? |
| Resource |
WHAT is being PROTECTED |
S3 bucket? HTTP endpoint? Application? |
Session is NOT a domain bounded context — it is an infrastructure concern that lives in adapters. The domain only knows ActorType (HUMAN/MACHINE).
File structure
src/hdc_auth_proxy/domain/
├── __init__.py # Re-export of main types
├── identity.py # Principal, SubjectId, ClientId, ActorType, IdentityProvider
├── authentication.py # Credential, TokenClaims, AuthenticationResult, AuthMethod
├── authorization.py # Role, Scope, PolicyRule, AccessRequest, AccessResult, Permission
├── resource.py # ResourceIdentifier, ProtectedResource, ResourceType
└── errors.py # DomainError, AuthenticationError, AuthorizationError
Implementation order (no circular dependencies)
identity.py — no domain dependencies
resource.py — no domain dependencies
errors.py — standalone
authentication.py — depends on identity
authorization.py — depends on identity and resource
__init__.py — re-exports from all modules
Models by module
1. identity.py
| Type |
Class |
Entity/VO |
Key fields |
| StrEnum |
ActorType |
— |
HUMAN, MACHINE |
| StrEnum |
IdentityProvider |
— |
COGNITO, FEDERATED_CIAM |
| dataclass |
SubjectId |
Value Object |
value: str (with non-empty validation) |
| dataclass |
ClientId |
Value Object |
value: str (with non-empty validation) |
| dataclass |
Principal |
Entity |
subject_id, actor_type, provider, client_id: ClientId | None, display_name: str | None, email: str | None |
2. resource.py
| Type |
Class |
Entity/VO |
Key fields |
| StrEnum |
ResourceType |
— |
S3_BUCKET, HTTP_ENDPOINT, APPLICATION |
| dataclass |
ResourceIdentifier |
Value Object |
resource_type, uri: str, name: str | None |
| dataclass |
ProtectedResource |
Entity |
identifier, description: str | None, required_scopes: frozenset[str], allowed_roles: frozenset[str], public: bool |
3. errors.py
| Type |
Class |
Key fields |
| dataclass |
DomainError |
code: str, message: str, details: tuple[tuple[str, str], ...] | None |
| dataclass |
AuthenticationError(DomainError) |
+ method: str |
| dataclass |
AuthorizationError(DomainError) |
+ resource_uri: str, required_permission: str |
Domain errors as dataclasses, NOT as Python exceptions. Exceptions are reserved for unexpected infrastructure conditions.
4. authentication.py
| Type |
Class |
Entity/VO |
Key fields |
| StrEnum |
AuthMethod |
— |
OAUTH2_OIDC, API_KEY, JWT_BEARER |
| StrEnum |
TokenType |
— |
ACCESS, ID, REFRESH |
| dataclass |
TokenClaims |
Value Object |
subject: SubjectId, issuer, audience: str | tuple[str, ...], issued_at, expires_at, scopes: frozenset[str], groups: frozenset[str], custom_claims: tuple[tuple[str, str], ...] + is_expired() method |
| dataclass |
Credential |
Value Object |
method: AuthMethod, token_type: TokenType | None, value: str |
| dataclass |
AuthenticationResult |
Value Object |
principal: Principal | None, claims: TokenClaims | None, credential_method, authenticated: bool, failure_reason: str | None (with invariants in __post_init__) |
5. authorization.py
| Type |
Class |
Entity/VO |
Key fields |
| StrEnum |
Permission |
— |
READ, WRITE, DELETE, ADMIN |
| StrEnum |
AccessDecision |
— |
ALLOW, DENY |
| dataclass |
Role |
Value Object |
name, permissions: frozenset[Permission], description: str | None |
| dataclass |
Scope |
Value Object |
value: str (validation: non-empty, no spaces) |
| dataclass |
PolicyRule |
Value Object |
role_name: str | None, required_scopes: frozenset[str], allowed_permissions: frozenset[Permission], resource_pattern: str, description: str | None |
| dataclass |
AccessRequest |
Value Object |
subject_id, resource: ResourceIdentifier, permission, scopes: frozenset[str], roles: frozenset[str] |
| dataclass |
AccessResult |
Value Object |
decision, request, matched_rule: PolicyRule | None, reason: str, evaluated_at: datetime |
Conventions applied
- All dataclasses:
frozen=True, slots=True
- All modules:
from __future__ import annotations
- Immutable collections:
frozenset and tuple (frozen dataclass compatibility)
X | None instead of Optional[X]
- StrEnum for JSON-friendly serialization
- Validation in
__post_init__ only for domain invariants
- Result-type pattern (AuthenticationResult, AccessResult) instead of exceptions for flow control
What is NOT in the domain
| Concept |
Where it belongs |
Why |
| Session management |
adapters/ |
Infrastructure concern |
| Token parsing/JWT decode |
adapters/ |
External crypto dependency |
| HTTP request/response |
adapters/ |
Transport concern |
| AWS Cognito API |
adapters/ |
External service |
| CIAM federation |
adapters/ |
External service |
| API key storage/hashing |
adapters/ |
Persistence concern |