Skip to content

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)

  1. identity.py — no domain dependencies
  2. resource.py — no domain dependencies
  3. errors.py — standalone
  4. authentication.py — depends on identity
  5. authorization.py — depends on identity and resource
  6. __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