Authentication & Authorization
Overview
The Authentication and Authorization module provides a flexible, policy-based system for securing API endpoints. It is designed to clearly separate the concepts of Authentication (verifying who a user or service is) and Authorization (determining what they are allowed to do).
Key Features
- Policy-Based: Access control is defined by simple, named policies (e.g., "public", "private", "internal").
- Multiple Mechanisms: Out-of-the-box support for common authentication mechanisms like JWT Bearer tokens and API Keys (both shared and per-client).
- Decoupled Architecture: A clean separation between Policies (Executors), which define what access is required, and Authentication Providers, which handle how credentials are validated.
- Extensible: New authentication providers or authorization policies can be added by implementing simple protocols and registering them.
Core Concepts
Policies (Executors)
An Authorization Policy is a named rule that defines the security requirements for an endpoint. For example, a "private" policy might require a valid JWT, while an "internal" policy might require a specific IP address.
In Athomic, policies are implemented by classes that follow the AuthExecutorProtocol. The AuthService uses a central registry to look up the correct executor for a given policy name.
Authentication Providers
An Authentication Provider is a component that performs the actual credential validation. For example, the JWTAuth provider knows how to decode a JWT, verify its signature, and check its expiration. The APIKeyDBProvider knows how to look up an API key's hash in a database.
All providers implement the AuthProviderProtocol, allowing the executors to be decoupled from the specific validation logic.
AuthService
The AuthService is the central orchestrator that ties everything together. When an endpoint needs to be secured, you ask the AuthService to authorize the request against a specific policy. The service finds the correct executor for that policy and runs it, which in turn uses the appropriate authentication provider.
Available Policies (Executors)
Athomic comes with a set of pre-configured policies:
public: Allows anonymous, unrestricted access.private: Requires a valid JWT Bearer token in theAuthorizationheader.shared_api_key: Requires a valid, globally shared API key in the configured header.db_api_key: Requires a valid, per-client API key that is looked up in the database.internal: Requires the request's IP address to be on a configured allowlist.
How to Secure an Endpoint
Securing an endpoint is typically done at the web framework layer, for example, using a FastAPI dependency. The dependency would be responsible for extracting credentials from the request, building a SecurityRequestContext, and calling the AuthService.
Conceptual FastAPI Example
from fastapi import Depends, HTTPException, Request
from nala.athomic.security.auth import AuthService, SecurityRequestContext
from nala.athomic.security.exceptions import AthomicAuthError, AthomicForbiddenError
auth_service = AuthService() # Singleton instance
async def require_private_access(request: Request):
"""A FastAPI dependency that enforces the 'private' policy."""
context = SecurityRequestContext(
client_host=request.client.host,
headers=dict(request.headers)
)
try:
# Ask the AuthService to authorize the request against the "private" policy
authenticated_user = await auth_service.authorize(context, "private")
return authenticated_user
except AthomicAuthError as e:
raise HTTPException(status_code=401, detail=str(e))
except AthomicForbiddenError as e:
raise HTTPException(status_code=403, detail=str(e))
@router.get("/profile")
async def get_user_profile(user: AuthenticatedUser = Depends(require_private_access)):
# This endpoint is now protected. The 'user' object contains the
# validated claims from the JWT.
return {"user_id": user.sub, "scopes": user.scopes}
Configuration
Authentication is configured under the [security.auth] section in your settings.toml.
[default.security.auth]
# A master switch to enable or disable authentication.
enable = true
# The default backend to use. Not always necessary as policies are explicit.
backend = "jwt"
# Configuration for JWT-based authentication.
[default.security.auth.jwt]
# The secret key for signing tokens. Best practice is to use a secret reference.
secret_key = { path = "auth/jwt", key = "secret" }
algorithm = "HS256"
expiration_minutes = 60
# Configuration for API Key-based authentication.
[default.security.auth.api_key]
enabled = true
header_name = "X-API-KEY"
# The secret for the "shared_api_key" policy.
secret_key = { path = "auth/api-key", key = "shared-secret" }
# The database connection for the "db_api_key" policy.
document_db_connection_name = "default_mongo"
Authorization providers — RBAC and ReBAC (switchable by one setting)
Authorization (deciding what a principal may do) is served by a pluggable
AuthorizationProvider port. Two engines are built in and interchangeable via a
single setting — authorization.provider — with zero call-site changes:
rbac(default): the role→permission engine (AuthorizationService) wrapped behind the port. A permissionresource:actionis granted when the principal's scopes/roles (roles expand via the resolver) match it under the wildcard rule.rebac: a Zanzibar-style relation-tuple engine. Access is a relationship fact —RelationTuple(subject, relation, resource)— and the model computes a relation via direct grants, union/computed usersets, and tuple-to-userset hierarchy (e.g. anadminof the parentorgis aneditorof theworkspace). Default-deny. Backed by an infra-freeembeddedstore (in-memory / KV) or by OpenFGA (store = "openfga", recommended for prod).
The port exposes three questions, both engines answer the first:
check(subject, relation, resource, context=None) -> Decision— allow/deny (+ reason).list_objects(subject, relation, resource_type) -> list[str]— "what can X see?".expand(relation, resource) -> ExpandTree— "who has access / why?".
list_objects/expand are relationship questions RBAC cannot answer; the RBAC provider
raises NotSupportedError there rather than returning a misleading result.
mode (disabled | dry_run | enforce) is orthogonal to provider and applies
identically to both engines — a consumer can roll a new provider out in dry_run
(metering authz_decisions_total{result="would_deny", provider=…}) before enforcing.
The requires(...) route guard and get_authorization_provider() are
provider-agnostic: flipping rbac ↔ rebac is a pure settings change.
[default.security.auth.authorization]
provider = "rbac" # rbac | rebac | abac | allow_all
mode = "dry_run" # disabled | dry_run | enforce (orthogonal to provider)
# RBAC: role → permission strings (wildcards allowed)
[default.security.auth.authorization.roles]
admin = ["*:*"]
# ReBAC (read only when provider = "rebac")
[default.security.auth.authorization.rebac]
store = "embedded" # embedded (no infra) | openfga (prod)
[default.security.auth.authorization.rebac.openfga]
api_url = "http://localhost:8080"
store_id = "..."
model_id = "..."
A ReBAC deployment injects its AuthorizationModel + TupleStore at the composition
root (like the RBAC kv_store); the provider surfaces write_tuples/delete_tuples/
read_tuples so a consumer syncs tuples from its own domain events — athomic
consumes none. A local OpenFGA server for dev/CI is available via make openfga-up
(behind the rebac provider only; not required for RBAC deployments).
API Reference
nala.athomic.security.auth.service.AuthService
The central authorization service.
This class acts as an Orchestrator, delegating the specific policy
execution logic to the appropriate AuthExecutorProtocol instance,
which is retrieved through a standardized registry. It integrates metrics
for all policy check attempts.
__init__(registry=None)
Initializes the service with a specific executor registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
Optional[AuthExecutorRegistry]
|
An optional registry instance for dependency injection (DI). Defaults to the global singleton. |
None
|
authorize(context, policy_name)
async
Finds the correct executor in the registry and executes the authorization check.
This method encapsulates the authorization flow: it records the attempt, retrieves the executor, executes the check, and logs the final success or failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SecurityRequestContext
|
The request context containing headers and client information. |
required |
policy_name
|
str
|
The name of the authorization policy to enforce (e.g., 'private', 'db_api_key'). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
AuthenticatedUser |
AuthenticatedUser
|
A user object representing the authenticated identity. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no executor is registered for the specified |
Exception
|
Propagates the exception raised by the executor upon authorization failure. |
nala.athomic.security.auth.executors.protocols.AuthExecutorProtocol
Bases: Protocol
Defines the contract for an authorization execution strategy.
Each executor is responsible for enforcing a single, specific form of validation (e.g., JWT token check, shared API key validation, IP allowlist check), adhering to the Single Responsibility Principle (SRP).
execute(context)
async
Executes the authorization logic against the provided request context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context
|
SecurityRequestContext
|
The request context containing client host and headers necessary for validation. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
AuthenticatedUser |
AuthenticatedUser
|
An object representing the authenticated identity and its claims. |
Raises:
| Type | Description |
|---|---|
AthomicAuthError
|
If authentication fails (e.g., invalid token, expired, revoked). |
AthomicForbiddenError
|
If access is denied (e.g., IP not allowed, insufficient scope). |
nala.athomic.security.auth.protocol.AuthProviderProtocol
Bases: Protocol
check(token)
async
Verify a token and return its decoded payload if valid.
:param token: The token to verify. :return: A dictionary with the decoded payload. :raises: AuthenticationError or other relevant exception if invalid.
create_token(subject, scope=None)
Create a signed token for the given subject.
:param subject: Unique identifier for the token owner (e.g., user ID). :param scope: Optional scope string for authorization. :return: A JWT or compatible token as a string.
nala.athomic.security.auth.authz.provider.AuthorizationProvider
Bases: Protocol
Provider-agnostic authorization contract (RBAC, ReBAC, and later ABAC/allow_all).
check(subject, relation, resource, context=None)
async
Decide whether subject holds relation on resource (default-deny).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subject
|
str
|
The subject URN (e.g. |
required |
relation
|
str
|
The relation/permission token (e.g. |
required |
resource
|
str
|
The resource URN (e.g. |
required |
context
|
Optional[Mapping[str, object]]
|
Optional extra inputs a provider may need (e.g. the RBAC provider
reads the |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Decision
|
class: |
expand(relation, resource)
async
Explain who holds relation on resource (the userset tree).
The explainability primitive ("why / who has access?"). May raise
:class:NotSupportedError where an engine cannot expand.
list_objects(subject, relation, resource_type)
async
List the resource URNs of resource_type on which subject holds relation.
The list-filtering primitive ("what can X see?"). May raise
:class:NotSupportedError for engines that cannot enumerate instances.
nala.athomic.security.auth.authz.provider_factory.build_authorization_provider(settings, *, registry=None, kv_store=None, tuple_store=None, rebac_model=None, openfga_client=None)
Build the provider selected by settings.provider (default-deny by construction).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
AuthorizationSettings
|
Authorization settings ( |
required |
registry
|
Optional[AuthorizationProviderRegistry]
|
Provider-builder registry; defaults to the module-level one. |
None
|
kv_store
|
Optional[KVStoreProtocol]
|
KV client for RBAC's |
None
|
tuple_store
|
Optional[TupleStore]
|
ReBAC tuple store (embedded); defaults to in-memory. |
None
|
rebac_model
|
Optional[AuthorizationModel]
|
ReBAC authorization model (injected at the composition root). |
None
|
openfga_client
|
Optional[object]
|
Pre-built OpenFGA client (for the |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the selected provider is unknown or missing a required dependency. |
nala.athomic.security.auth.authz.rebac.tuples.RelationTuple
Bases: BaseModel
An atomic ReBAC fact: subject has relation on resource.
Attributes:
| Name | Type | Description |
|---|---|---|
subject |
str
|
Subject URN ( |
relation |
str
|
The relation token this tuple grants ( |
resource |
str
|
The resource URN the relation is on ( |
subject_relation |
Optional[str]
|
Optional userset relation on |
key()
A hashable identity for de-duplication.
nala.athomic.security.auth.authz.rebac.model.AuthorizationModel
Bases: BaseModel
The full model: object type → its relation definitions.
rewrite_for(object_type, relation)
Return the rewrite for object_type/relation.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the type or relation is undefined — the engine treats this as a default-deny (an undefined relation grants nothing). |
nala.athomic.security.auth.models.auth_user.AuthenticatedUser
Bases: BaseModel
Represents an authenticated identity extracted from a validated token payload (e.g., JWT).
This model provides a normalized view of the authenticated user's claims, used throughout the application for authorization decisions.
Attributes:
| Name | Type | Description |
|---|---|---|
sub |
str
|
The unique identifier for the user or entity (Subject) that the token represents. |
scopes |
List[str]
|
A list of normalized (lowercase, split) user permission scopes. |
roles |
List[str]
|
A list of roles assigned to the user. |
jti |
str
|
The unique ID of the token (JWT ID), crucial for token denylist/revocation checks. |
exp |
int
|
The timestamp (UNIX epoch) when the token expires. |
raw_scope |
str
|
The original, unmodified scope string from the token payload. |
tenant_id |
Optional[str]
|
The tenant the principal belongs to, when the
token/API-key payload carries a |
from_payload(payload)
classmethod
Creates an AuthenticatedUser instance from a raw decoded token payload dictionary.
This factory method handles normalization, such as splitting the raw scope string into a list of lowercase scopes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Dict[str, Any]
|
The decoded token payload (dictionary). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
AuthenticatedUser |
AuthenticatedUser
|
A validated instance of the user model. |