Authentication Context¶
The authentication context system tracks who is making a request and on whose behalf. It's the foundation for all authorization decisions.
AuthContext¶
AuthContext is a frozen dataclass that stores authentication state for each request.
Key Properties¶
| Property | Type | Description |
|---|---|---|
is_authenticated |
bool |
True if a principal is set |
is_anonymous |
bool |
True if no principal (unauthenticated) |
is_impersonated |
bool |
True if effective != real principal |
is_delegated |
bool |
True if a delegate principal is set |
real_principal |
AuthPrincipal |
The actor performing the action |
effective_principal |
AuthPrincipal |
Subject of the action (actor or impersonated) |
delegate_principal |
AuthPrincipal \| None |
Principal acting on behalf of real |
session_id |
UUID \| None |
Current session identifier |
session_scopes |
set[str] |
Session permission scopes |
impersonation_mode |
ImpersonationMode \| None |
Type of impersonation |
Accessing the Context¶
Migration in progress
current_auth_context will fully replace the legacy g.current_user / g.actor globals, but is not yet available on all backends. Check if your backend has been migrated before using it.
Use current_auth_context to access the auth context within a Flask request:
from shared.iam.helpers import current_auth_context
def my_endpoint():
# Get the principal
user = current_auth_context.real_principal
print(f"Request by user {user.id}")
Never Create AuthContext Directly
Always access auth context via current_auth_context. The context is managed by auth providers and should never be instantiated manually.
Type-Safe Principal Access¶
Use *_as() methods for type-safe principal casting:
from shared.iam.helpers import current_auth_context
# Type-safe access - raises ValueError if wrong type
user = current_auth_context.real_principal_as(User)
employee = current_auth_context.real_principal_as(AlanEmployee)
# For effective principal (the subject)
subject = current_auth_context.effective_principal_as(User)
# For optional delegate
delegate = current_auth_context.delegate_principal_as(ServiceAccount) # Returns None if not set
Impersonation¶
Impersonation allows Alaners to act on behalf of users. When impersonated:
real_principal= The Alaner (actor)effective_principal= The impersonated user (subject)is_impersonated= True
Impersonation Modes¶
Future change
Impersonation modes will likely be replaced by proper scopes in the future.
from shared.iam.helpers import ImpersonationMode
# Read-only: Alaner can view as user but not modify
ImpersonationMode.read_only
# Read-write: Alaner can view and modify as user
ImpersonationMode.read_write
# Service account delegation: Used by API Proxy system
ImpersonationMode.service_account_delegation
Setting Impersonation¶
from shared.iam.helpers import set_auth_context, ImpersonationMode
# In an auth provider
set_auth_context(
real_principal=alaner,
effective_principal=impersonated_user,
impersonation_mode=ImpersonationMode.read_only,
)
Auth Context Providers¶
Migration in progress
Auth context providers are not yet available on all backends. Check if your backend has been migrated before using them.
Providers extract authentication from requests and set up the AuthContext.
Base Class¶
from abc import ABC, abstractmethod
from shared.iam.auth_context.providers.base import AuthContextProvider
class MyAuthProvider(AuthContextProvider):
@abstractmethod
def will_handle_request(self) -> bool:
"""Return True if this provider can handle the current request"""
pass
@abstractmethod
def set_auth_context_from_request(self) -> None:
"""Extract auth info and call set_auth_context()"""
pass
Provider Chain Pattern¶
Providers are evaluated in order - exactly ONE must handle the request.
How the chain works:
- Iterates through providers in order
- Calls
will_handle_request()on each - Exactly ONE provider must return True
- Calls that provider's
set_auth_context_from_request() - Returns 403 if zero or multiple providers match
Default setup: The default provider chain is configured at app initialization via CustomApi.register_default_auth_context_providers(). Most blueprints inherit this default automatically.
Blueprint customization: Individual blueprints can override the default chain:
from shared.api.custom_smorest_blueprint import CustomBlueprint
from shared.iam.auth_context.providers.zero_trust import ZeroTrustAuthContextProvider
from shared.iam.auth_context.providers.anonymous import AnonymousAuthContextProvider
# Blueprint with custom auth providers
public_blueprint = CustomBlueprint(
"public",
__name__,
auth_context_providers=[
ZeroTrustAuthContextProvider(User),
AnonymousAuthContextProvider(), # Allow unauthenticated access
],
)
Built-in Providers¶
AnonymousAuthContextProvider¶
Fallback for unauthenticated requests:
from shared.iam.auth_context.providers.anonymous import AnonymousAuthContextProvider
class AnonymousAuthContextProvider(AuthContextProvider):
def will_handle_request(self) -> bool:
return True # Always matches (use as last in chain)
def set_auth_context_from_request(self) -> None:
# Does nothing - context is anonymous by default
pass
ZeroTrustAuthContextProvider¶
For Cloudflare Zero Trust authentication:
from shared.iam.auth_context.providers.zero_trust import ZeroTrustAuthContextProvider
# Searches for user by email in these model classes
provider = ZeroTrustAuthContextProvider(Alaner, ExternalUser)
# Custom JWT audience config key
provider = ZeroTrustAuthContextProvider(
Alaner,
aud_config_key="MY_ZEROTRUST_AUDIENCE",
)
Behavior:
- Returns False if
Authorizationheader is present - Returns False if no Zero Trust cookie
- Validates JWT token and extracts email
- Looks up principal by email
ServiceAccountAuthContextProvider¶
For Google service account authentication via OAuth tokens.
WebhookAuthContextProvider¶
For webhook authentication with secret validation or HMAC-SHA256 signatures.
Creating Custom Providers¶
from flask import request
from flask_smorest import abort
from shared.iam.auth_context.providers.base import AuthContextProvider
from shared.iam.helpers import set_auth_context
class ApiKeyAuthContextProvider(AuthContextProvider):
"""Authenticate via API key header"""
def __init__(self, *auth_principal_types: type[AuthPrincipal]):
self.auth_principal_types = auth_principal_types
def will_handle_request(self) -> bool:
return "X-API-Key" in request.headers
def set_auth_context_from_request(self) -> None:
api_key = request.headers.get("X-API-Key")
# Validate and lookup principal
principal = self._lookup_by_api_key(api_key)
if not principal:
abort(403, message="Invalid API key")
set_auth_context(real_principal=principal)
def _lookup_by_api_key(self, api_key: str) -> AuthPrincipal | None:
# Implementation here
pass
TransactionAuthContext¶
TransactionAuthContext is a database model that stores auth context for each transaction (database operation). It enables audit trails.
from shared.models.versioning.base import TransactionAuthContextBase
class TransactionAuthContext(TransactionAuthContextBase):
__tablename__ = "transaction_auth_context"
The id column automatically captures current_auth_context.id, linking database operations to the authentication context.
Serialization¶
Auth context is automatically handled in most cases:
- RQ async jobs - Automatically serialized and restored
- Structured logs - Automatically included under the
authnzobject (seeshared/helpers/logging/configure_logging.py)
For other use cases, serialization methods are available:
# Serialize to dict
serialized = current_auth_context.to_dict()
# Restore from dict (context manager)
from shared.iam.helpers import set_auth_context_from_dict
with set_auth_context_from_dict(serialized):
# Auth context is restored here
pass
Helper Functions¶
| Function | Purpose |
|---|---|
set_auth_context(real_principal, ...) |
Set the auth context (for providers) |
reset_auth_context() |
Reset to anonymous |
set_auth_context_from_dict(serialized) |
Context manager to restore from dict |
is_alaner_admin() |
Check if current user is Alaner admin |
is_impersonated() |
Check if current request is impersonated |