Authorization Overview

Authorization is the process of determining whether an authenticated user is allowed to perform a specific action or access a specific resource. AllSafe Fast provides a layered authorization system that combines role-based access control (RBAC) and permission-based access control, giving you fine-grained control over who can do what in your application.

Authentication vs. Authorization

These two concepts are often confused, but they answer fundamentally different questions:

AspectAuthenticationAuthorization
QuestionWho are you?What can you do?
PurposeVerify identityEnforce access rules
InputCredentials (password, token, OAuth)Identity + requested action
OutputAuthenticated user principalAllow or deny decision
Failure401 Unauthorized403 Forbidden
AllSafe APIauth.sign_in(), providersauth.user(), AuthorizationService
ℹ Key Distinction

Authentication establishes who the user is. Authorization determines what that user is allowed to do. A user can be authenticated (logged in) but not authorized to access a particular resource.

The Authorization Flow

AllSafe Fast follows a clear flow from user identity to protected resource access:

User
Authenticated identity with roles & permissions
Role
Coarse-grained access level (admin, user, moderator)
Permission
Fine-grained capability (users:read, billing:write)
Protected Resource
Route handler, API endpoint, or data record

The flow works as follows:

  1. User authenticates through a provider (password, OAuth, OIDC, magic link, or email OTP). On success, a UserPrincipal is created containing the user's roles and permissions.
  2. Roles are coarse-grained access levels assigned to the user. They are stored on the User model and embedded in JWT claims for fast access.
  3. Permissions are fine-grained capabilities. They are also stored on the User model and included in JWT claims, allowing route-level enforcement without database lookups.
  4. Protected Resources are your application's routes and endpoints. They are guarded by the auth.user() dependency, which checks roles and permissions before the handler runs.

How auth.user() Enforces Authorization

The auth.user() method returns a FastAPI dependency that enforces authorization at the route level. When a request arrives, the dependency:

  1. Extracts and validates the JWT from the request (cookie or Authorization header).
  2. Deserializes the UserPrincipal from JWT claims, including roles and permissions.
  3. Checks the authorization constraints you specified against the principal.
  4. If any check fails, raises AuthorizationError, which FastAPI translates into a 403 Forbidden response.
  5. If all checks pass, injects the UserPrincipal into your route handler.
from allsafe_fast import Auth, AuthConfig, UserPrincipal
from fastapi import FastAPI, Depends

app = FastAPI()
auth = Auth(AuthConfig(secret_key="your-secret-key"))
app.include_router(auth.router)

# Any authenticated user can access this route
@app.get("/api/me")
async def get_me(user: UserPrincipal = Depends(auth.user())):
    return {"email": user.email, "roles": user.roles}

# Only users with the "admin" role can access this route
@app.get("/api/admin")
async def admin_panel(user: UserPrincipal = Depends(auth.user(role="admin"))):
    return {"message": "Welcome, admin"}

# Only users with the "reports:export" permission
@app.post("/api/reports/export")
async def export_report(user: UserPrincipal = Depends(auth.user(permission="reports:export"))):
    return generate_report()

# User must have ANY of the specified roles (any-of semantics)
@app.get("/api/moderation")
async def moderation(user: UserPrincipal = Depends(auth.user(roles=("admin", "moderator")))):
    return get_moderation_queue()

# User must have ALL of the specified permissions (all-of semantics)
@app.delete("/api/users/{user_id}")
async def delete_user(user_id: str, user: UserPrincipal = Depends(auth.user(permissions=("users:read", "users:delete")))):
    return remove_user(user_id)

auth.user() Parameters

verified
If True, requires the user's email to be verified (email_verified=True). Default: False.
role
A single role string the user must have. Checks using has_role(). Use this for simple single-role checks.
permission
A single permission string the user must have. Checks using has_permission().
roles
A tuple of role strings. The user must have any of these roles (any-of semantics).
permissions
A tuple of permission strings. The user must have all of these permissions (all-of semantics).

AuthorizationService Static Methods

For cases where you need to perform authorization checks inside your business logic (not just at the route level), AllSafe Fast provides the AuthorizationService class with static methods:

from allsafe_fast import AuthorizationService, UserPrincipal

# All methods are static and raise AuthorizationError on failure
AuthorizationService.check_authenticated(user)        # User must be authenticated
AuthorizationService.check_verified(user)              # User must have verified email
AuthorizationService.check_role(user, "admin")          # User must have specific role
AuthorizationService.check_permission(user, "users:read")  # User must have specific permission
AuthorizationService.check_any_role(user, ("admin", "mod"))   # Any of the roles
AuthorizationService.check_all_permissions(user, ("a", "b")) # All of the permissions
💡 When to Use AuthorizationService

Use auth.user() for route-level guards (most common). Use AuthorizationService when you need to perform checks inside service-layer code, conditional branches, or before performing sensitive operations within a handler.

UserPrincipal

The UserPrincipal is a frozen dataclass that represents the authenticated user. It is injected into your route handlers by the auth.user() dependency:

id
UUID — the user's unique identifier.
email
str — the user's email address.
email_verified
bool — whether the user's email has been verified.
roles
tuple[str, ...] — the user's assigned roles.
permissions
tuple[str, ...] — the user's assigned permissions.
is_authenticated
bool — always True for a valid UserPrincipal.
has_role(role)
Returns True if the user has the specified role.
has_permission(perm)
Returns True if the user has the specified permission.
from allsafe_fast import UserPrincipal

# UserPrincipal is a frozen dataclass — it cannot be modified after creation
user = UserPrincipal(
    id="550e8400-e29b-41d4-a716-446655440000",
    email="alice@example.com",
    email_verified=True,
    roles=("admin",),
    permissions=("users:read", "users:write"),
)

print(user.has_role("admin"))            # True
print(user.has_permission("users:read"))  # True
print(user.is_authenticated)             # True

Error Behavior

When authorization fails, AllSafe Fast raises an AuthorizationError. This exception is caught by FastAPI's exception handling and converted into an HTTP 403 Forbidden response:

from allsafe_fast import AuthorizationError

# The error contains a descriptive message
try:
    AuthorizationService.check_role(user, "admin")
except AuthorizationError as e:
    print(str(e))  # "User does not have required role: admin"
⚠ 403 vs 401

If the user is not authenticated at all (no valid token), the response is 401 Unauthorized. If the user is authenticated but lacks the required role or permission, the response is 403 Forbidden. This distinction helps clients understand whether they need to log in or request additional access.

Layered Authorization Strategy

AllSafe Fast encourages a layered approach to authorization:

LayerMechanismGranularity
Route-levelauth.user(role=..., permission=...)Coarse — protects entire endpoints
Service-levelAuthorizationService.check_*(...)Medium — protects business operations
Resource-levelCustom checks using UserPrincipalFine — protects individual records

For most applications, route-level authorization with auth.user() is sufficient. For more complex scenarios, combine it with service-level checks and custom resource-level logic.