Skip to content

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):

  1. Validate token → TokenValidationResult
  2. Check expiration (domain logic)
  3. Resolve principal → Principal | None
  4. Return AuthenticationResult

ApiKeyMethodAuthenticator (API_KEY):

  1. Validate key → KeyValidationResult
  2. Return AuthenticationResult (no claims step)

AuthenticationService (dispatcher):

  • Receives Mapping[AuthMethod, MethodAuthenticator]
  • Dispatches credential.method to the right strategy
  • Returns "unsupported method" failure for unknown methods

Authorization — Default Deny

AuthorizationService:

  1. Look up resource → if public=True, ALLOW immediately
  2. Load PolicyRules matching the resource URI
  3. Evaluate rules: glob pattern + permission + role + scopes
  4. 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.