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:
| Aspect | Authentication | Authorization |
|---|---|---|
| Question | Who are you? | What can you do? |
| Purpose | Verify identity | Enforce access rules |
| Input | Credentials (password, token, OAuth) | Identity + requested action |
| Output | Authenticated user principal | Allow or deny decision |
| Failure | 401 Unauthorized | 403 Forbidden |
| AllSafe API | auth.sign_in(), providers | auth.user(), AuthorizationService |
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:
Authenticated identity with roles & permissions
Coarse-grained access level (admin, user, moderator)
Fine-grained capability (users:read, billing:write)
Route handler, API endpoint, or data record
The flow works as follows:
- User authenticates through a provider (password, OAuth, OIDC, magic link, or email OTP). On success, a
UserPrincipalis created containing the user's roles and permissions. - 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.
- 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.
- 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:
- Extracts and validates the JWT from the request (cookie or Authorization header).
- Deserializes the
UserPrincipalfrom JWT claims, including roles and permissions. - Checks the authorization constraints you specified against the principal.
- If any check fails, raises
AuthorizationError, which FastAPI translates into a 403 Forbidden response. - If all checks pass, injects the
UserPrincipalinto 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
True, requires the user's email to be verified (email_verified=True). Default: False.has_role(). Use this for simple single-role checks.has_permission().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
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:
True for a valid UserPrincipal.True if the user has the specified role.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"
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:
| Layer | Mechanism | Granularity |
|---|---|---|
| Route-level | auth.user(role=..., permission=...) | Coarse — protects entire endpoints |
| Service-level | AuthorizationService.check_*(...) | Medium — protects business operations |
| Resource-level | Custom checks using UserPrincipal | Fine — 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.