Ports and Services
Context
With the domain layer in place, this document defines the ports (Protocol-based interfaces) and services (business logic) following hexagonal architecture.
- Ports define boundaries using Protocols (static duck typing)
- Services implement business logic, depending only on domain types and ports
- Dependencies flow inward:
adapters → services → ports → domain
File structure
src/hdc_auth_proxy/
├── ports/
│ ├── __init__.py # Re-export all Protocols
│ ├── authentication.py # MethodAuthenticator, TokenValidator, PrincipalResolver, ApiKeyValidator
│ ├── authorization.py # PolicyRepository, ResourceRepository
│ └── input.py # Authenticator, Authorizer (input ports / use cases)
│
├── services/
│ ├── __init__.py # Re-export service classes
│ ├── authentication.py # TokenMethodAuthenticator, ApiKeyMethodAuthenticator, AuthenticationService
│ └── authorization.py # AuthorizationService
Output Ports (driven side — what adapters implement)
ports/authentication.py
| Protocol | Method | Input | Output |
|---|---|---|---|
MethodAuthenticator |
authenticate(credential) |
Credential |
AuthenticationResult |
TokenValidator |
validate(token) |
str |
TokenValidationResult |
PrincipalResolver |
resolve(claims) |
TokenClaims |
Principal \| None |
ApiKeyValidator |
validate(api_key) |
str |
KeyValidationResult |
ports/authorization.py
| Protocol | Method | Input | Output |
|---|---|---|---|
PolicyRepository |
rules_for_resource(resource_uri) |
str |
tuple[PolicyRule, ...] |
ResourceRepository |
find(identifier) |
ResourceIdentifier |
ProtectedResource \| None |
Input Ports (driving side — what services expose)
ports/input.py
| Protocol | Method | Input | Output |
|---|---|---|---|
Authenticator |
authenticate(credential) |
Credential |
AuthenticationResult |
Authorizer |
authorize(request) |
AccessRequest |
AccessResult |
Services
Authentication — Strategy Pattern
Instead of a monolithic service that branches on AuthMethod, each auth method has its own MethodAuthenticator implementation with only the dependencies it needs.
┌─────────────────────────────┐
│ AuthenticationService │
│ strategies: Mapping[ │
│ AuthMethod, │
│ MethodAuthenticator │
│ ] │
└──────────┬──────────────────┘
│ dispatches via Mapping
┌──────────────┴──────────────┐
│ │
┌───────────▼──────────┐ ┌────────────▼─────────┐
│ TokenMethod │ │ ApiKeyMethod │
│ Authenticator │ │ Authenticator │
│ │ │ │
│ deps: │ │ deps: │
│ - TokenValidator │ │ - ApiKeyValidator │
│ - PrincipalResolver │ │ │
└──────────────────────┘ └───────────────────────┘
TokenMethodAuthenticator (OAUTH2_OIDC, JWT_BEARER):
- Validate token →
TokenValidationResult - Check expiration (domain logic)
- Resolve principal →
Principal | None - Return
AuthenticationResult
ApiKeyMethodAuthenticator (API_KEY):
- Validate key →
KeyValidationResult - Return
AuthenticationResult(no claims step)
AuthenticationService (dispatcher):
- Receives
Mapping[AuthMethod, MethodAuthenticator] - Dispatches
credential.methodto the right strategy - Returns "unsupported method" failure for unknown methods
Authorization — Default Deny
AuthorizationService:
- Look up resource → if
public=True, ALLOW immediately - Load
PolicyRules matching the resource URI - Evaluate rules: glob pattern + permission + role + scopes
- First matching rule → ALLOW; no match → DENY
Rule matching uses fnmatch for glob-style URI patterns.
Design Decisions
| Decision | Rationale | Reference |
|---|---|---|
| Strategy pattern for auth methods | ISP: each method has only its deps. OCP: add method = add class. No branching. | ADR-0004 |
Result-type dataclasses (TokenValidationResult, KeyValidationResult) |
Avoids isinstance in services. Consistent with domain's AuthenticationResult pattern. |
ADR-0005 |
| Sync Protocols | Async variants will be added when the adapter layer introduces I/O. | ADR-0006 |
No core/ layer yet |
No internal plugin system needed. DDD bounded contexts suffice for now. | See ADR-0002 |
| Postel's Law in ports | Output ports accept str (liberal input), return domain types (conservative output). |
Project typing guidelines |
Testing approach
| Test category | What it verifies | File |
|---|---|---|
| Architecture tests | DDD dependency direction rules | tests/test_architecture.py |
| Domain invariant tests | Result dataclass __post_init__ validation |
tests/test_domain.py |
| Authentication service TDD | Strategy dispatch, token flow, API key flow | tests/test_authentication_service.py |
| Authorization service TDD | Default deny, public shortcut, rule matching | tests/test_authorization_service.py |
All service tests use stub implementations of output port Protocols — no mocks, no external dependencies.