Strategy pattern for authentication methods
Context and Problem Statement
The system supports multiple authentication methods (OAuth2/OIDC, JWT Bearer, API Key). Each method has different dependencies and validation logic. How should AuthenticationService handle dispatch to the correct authentication flow?
Decision Drivers
- Interface Segregation Principle: each method authenticator should only depend on what it needs
- Open/Closed Principle: adding a new auth method should not modify existing code
- No
isinstancechecks in business logic (see ADR-0005) - Each authentication method has fundamentally different dependencies (token validator vs API key validator)
Considered Options
- Monolithic service with branching (
if method == ...) - Chain of Responsibility pattern
- Strategy pattern with
MethodAuthenticatorProtocol andMapping[AuthMethod, MethodAuthenticator]
Decision Outcome
Chosen option: "Strategy pattern with Protocol-based dispatch", because it satisfies both ISP and OCP while keeping the dispatcher trivially simple.
How it works
Each auth method is implemented as a separate class conforming to the MethodAuthenticator Protocol:
class MethodAuthenticator(Protocol):
def authenticate(self, credential: Credential) -> AuthenticationResult: ...
AuthenticationService receives a Mapping[AuthMethod, MethodAuthenticator] at construction and dispatches:
class AuthenticationService:
def __init__(self, strategies: Mapping[AuthMethod, MethodAuthenticator]) -> None:
self._strategies = strategies
def authenticate(self, credential: Credential) -> AuthenticationResult:
strategy = self._strategies.get(credential.method)
if strategy is None:
return AuthenticationResult(...) # unsupported method
return strategy.authenticate(credential)
Current strategy implementations
| Strategy | Auth method | Dependencies |
|---|---|---|
TokenMethodAuthenticator |
JWT_BEARER, OAUTH2_OIDC | TokenValidator, PrincipalResolver |
ApiKeyMethodAuthenticator |
API_KEY | ApiKeyValidator |
Consequences
- Good, because each authenticator class has only its required dependencies (ISP)
- Good, because adding a new auth method means adding a new class and a mapping entry (OCP)
- Good, because no branching or
isinstancein the dispatcher - Good, because each strategy is independently testable with focused stubs
- Neutral, because the mapping must be wired at composition time (adapters layer)
- Bad, because slightly more types than a single monolithic class
Confirmation
- Architecture tests enforce that services only import from domain and ports
- Each strategy is tested independently in
test_authentication_service.py - The dispatcher is tested for both known and unsupported methods
More Information
- Related: ADR-0002 Adopt DDD, ADR-0005 Result-type dataclasses
- Implementation:
src/hdc_auth_proxy/services/authentication.py