Authentication & Authorization
This document explains how authentication and authorization work in the Rego backend.
Overview
The Rego API uses JWT (JSON Web Token) based authentication powered by the fastapi-users library. Users can authenticate using either Bearer tokens in headers or HTTP-only cookies, providing flexibility for different client types.
Authentication Methods
1. Bearer Token Authentication
The primary authentication method uses JWT tokens in the Authorization header:
GET /api/boards HTTP/1.1
Host: localhost:8000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Pros: - Standard REST API convention - Works with any HTTP client - Easy to implement in mobile apps
Cons: - Requires client-side token storage - Vulnerable to XSS if stored in localStorage
2. Cookie-Based Authentication
Alternatively, the JWT can be sent as an HTTP-only cookie:
GET /api/boards HTTP/1.1
Host: localhost:8000
Cookie: rego_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Pros: - More secure (HTTP-only, can't be accessed by JavaScript) - Automatic inclusion in requests by browser - Better protection against XSS attacks
Cons: - Requires CSRF protection - Only works in browser environments
User Model
The User model extends fastapi-users' base user table:
Location: rego/core/models.py
class User(SQLAlchemyBaseUserTable[UUID], BaseTable):
id: Mapped[UUID]
email: Mapped[str] # Validated email
username: Mapped[str] # Unique username
hashed_password: Mapped[str] # Argon2 hash
firstname: Mapped[str | None]
lastname: Mapped[str | None]
# fastapi-users fields
is_active: Mapped[bool] # Can user log in?
is_verified: Mapped[bool] # Email verified?
is_superuser: Mapped[bool] # Admin privileges?
# Relationships
boards: Mapped[list[Board]] # Boards user is member of
assigned_cards: Mapped[list[Card]] # Cards assigned to user
Password Security
- Passwords are hashed using Argon2, the winner of the Password Hashing Competition
- Plain passwords are never stored or logged
- Password complexity requirements can be enforced at registration
- Hashing is CPU-intensive by design to prevent brute-force attacks
Authentication Endpoints
All auth endpoints are prefixed with /auth.
Register
Create a new user account:
POST /auth/register
Content-Type: application/json
{
"email": "user@example.com",
"username": "johndoe",
"password": "SecurePassword123!",
"firstname": "John",
"lastname": "Doe"
}
Response (201 Created):
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"username": "johndoe",
"firstname": "John",
"lastname": "Doe",
"is_active": true,
"is_verified": false,
"is_superuser": false
}
Validation: - Email must be valid format and unique - Username must be unique - Password must meet complexity requirements (if configured)
Login
Authenticate and receive a JWT token:
POST /auth/login
Content-Type: application/x-www-form-urlencoded
username=user@example.com&password=SecurePassword123!
Response (200 OK):
Cookie variant: If using cookie auth, the token is set as an HTTP-only cookie in the response:
Set-Cookie: rego_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...;
Path=/;
HttpOnly;
SameSite=Lax;
Max-Age=3600
Logout
Invalidate the current session:
Response (200 OK):
For cookie-based auth, this clears the cookie.
Get Current User
Retrieve the authenticated user's profile:
Response (200 OK):
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"username": "johndoe",
"firstname": "John",
"lastname": "Doe",
"is_active": true,
"is_verified": false,
"is_superuser": false
}
Update Profile
Modify the current user's profile:
PATCH /auth/me
Authorization: Bearer <token>
Content-Type: application/json
{
"firstname": "Jonathan",
"lastname": "Doe"
}
Response (200 OK):
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"username": "johndoe",
"firstname": "Jonathan",
"lastname": "Doe",
"is_active": true,
"is_verified": false,
"is_superuser": false
}
Change Password
Update the user's password:
POST /auth/change-password
Authorization: Bearer <token>
Content-Type: application/json
{
"current_password": "OldPassword123!",
"new_password": "NewSecurePassword456!"
}
Response (200 OK):
JWT Token Structure
The JWT contains the following claims:
{
"sub": "123e4567-e89b-12d3-a456-426614174000", // User ID
"aud": ["fastapi-users:auth"], // Audience
"exp": 1735689600, // Expiration timestamp
"iat": 1735686000 // Issued at timestamp
}
Token Configuration
Location: rego/core/settings/jwt.py
class JWTSettings(BaseSettings):
secret: str = "CHANGE_ME" # Signing key (MUST change in production)
algorithm: str = "HS256" # Signing algorithm
lifetime_seconds: int = 3600 # Token valid for 1 hour
cookie_name: str = "rego_auth" # Cookie name for cookie-based auth
Security Requirements:
In production: - Secret must be at least 32 characters - Secret must be different from default "CHANGE_ME" - Use a cryptographically random string - Never commit secrets to version control
Authorization System
Authorization in Rego is based on two levels: system-level and board-level permissions.
System-Level Permissions
Controlled by the is_superuser flag on the User model:
Superusers can: - Access debug endpoints (if enabled) - View system-wide statistics - Perform administrative tasks
Board-Level Permissions
Each user's relationship to a board is defined by their role in the BoardMember table:
class BoardMember(BaseLinkTable):
board_id: Mapped[UUID]
user_id: Mapped[UUID]
role: Mapped[MemberRole] # owner or member
Member Roles
class MemberRole(str, Enum):
owner = "owner" # Full control over board
member = "member" # Can view and edit, but limited admin
Owner permissions: - Update board details (title, description) - Delete the board - Add/remove members - Create/modify automation rules - All member permissions
Member permissions: - View board and all its contents - Create/update/delete columns - Create/update/delete cards - Create/update/delete labels - Assign themselves and other members to cards - Create/update/delete checklists and items
Access Control Dependencies
The access domain provides reusable dependencies for enforcing board-level permissions.
Location: rego/domains/access/dependencies.py
Require Board Access
Ensures the authenticated user is a member of the board with an optional minimum role:
@router.delete("/boards/{board_id}")
async def delete_board(
board_id: UUID,
user: User = Depends(require_board_access(MemberRole.owner)),
):
# Only board owners can reach this code
...
Usage patterns:
# Require any membership (default)
Depends(require_board_access())
# Require owner role
Depends(require_board_access(MemberRole.owner))
# Require member role (explicitly)
Depends(require_board_access(MemberRole.member))
# Custom board resolver (for nested resources)
Depends(require_board_access(
MemberRole.member,
get_board=get_board_from_card
))
Error responses:
403 Forbidden: User is not a board member403 Forbidden: User has insufficient role (e.g., member trying to delete board)
Require Board Members
Validates that a list of users are all members of a board:
@router.post("/cards/{card_id}/assignees")
async def assign_users(
card_id: UUID,
user_ids: list[UUID],
validated_users: list[User] = Depends(require_board_members()),
):
# All users in user_ids are confirmed board members
card.assignees.extend(validated_users)
...
Error responses:
404 Not Found: One or more users don't exist403 Forbidden: One or more users are not board members
Require User Is Board Member
Validates that a specific user (from path parameter) is a board member:
@router.put("/cards/{card_id}/assignees/{user_id}")
async def assign_user(
card_id: UUID,
validated_user: User = Depends(require_user_is_board_member()),
):
# The user from user_id is confirmed to be a board member
card.assignees.append(validated_user)
...
Protecting Endpoints
Using Dependencies
The most common pattern is to use FastAPI's dependency injection:
from rego.domains.auth.dependencies import current_user
from rego.domains.access.dependencies import require_board_access
@router.post("/boards/{board_id}/columns")
async def create_column(
board_id: UUID,
payload: ColumnCreate,
user: User = Depends(current_user), # Must be authenticated
board: Board = Depends(require_board_access()), # Must be board member
):
# Both checks passed, proceed with logic
...
Path-Level Dependencies
For multiple endpoints with the same requirements:
router = APIRouter(
dependencies=[Depends(current_user)] # All routes require authentication
)
@router.get("/boards")
async def list_boards():
# Already authenticated
...
@router.get("/boards/{board_id}")
async def get_board():
# Already authenticated
...
Operation-Level Dependencies
For specific routes only:
@router.delete(
"/boards/{board_id}",
dependencies=[Depends(require_board_access(MemberRole.owner))]
)
async def delete_board(board_id: UUID):
# Only owners can delete
...
Security Best Practices
Password Storage
- Never log or display passwords
- Use Argon2 for hashing (built-in to fastapi-users)
- Enforce minimum password complexity
- Consider password breach checking (haveibeenpwned API)
Token Management
- Keep token lifetime short (default: 1 hour)
- Implement refresh tokens for long-lived sessions
- Invalidate tokens on password change
- Use HTTPS in production to prevent token interception
Cookie Security
When using cookie-based auth:
# Production settings
cookie_secure = True # HTTPS only
cookie_samesite = "lax" # CSRF protection
cookie_httponly = True # No JavaScript access
cookie_max_age = 3600 # 1 hour expiration
Environment-Specific Security
The application enforces security requirements based on environment:
# Development
JWT_SECRET = "CHANGE_ME" # Allowed
CORS_ALLOWED_ORIGINS = "*" # Allowed
# Production
JWT_SECRET = "CHANGE_ME" # ERROR: Must be changed
JWT_SECRET = "short" # ERROR: Must be 32+ chars
CORS_ALLOWED_ORIGINS = "*" # ERROR: Must specify exact origins
Common Authentication Flows
Web Application Flow
- User visits login page
- Client POSTs credentials to
/auth/login - Server returns JWT in HTTP-only cookie
- All subsequent requests include cookie automatically
- Server validates cookie on each request
- On logout, server clears cookie
Mobile/SPA Flow
- User enters credentials in app
- App POSTs to
/auth/login - Server returns JWT in response body
- App stores token in secure storage
- App includes
Authorization: Bearer <token>header on each request - Server validates token on each request
- On logout, app deletes stored token
WebSocket Authentication
WebSockets can't send custom headers, so token is passed as query parameter:
const token = localStorage.getItem('auth_token');
const ws = new WebSocket(`ws://localhost:8000/ws/boards/${boardId}?token=${token}`);
The server validates the token before accepting the connection:
@router.websocket("/boards/{board_id}")
async def board_websocket(websocket: WebSocket, board_id: UUID):
token = websocket.query_params.get("token")
user = await validate_token(token)
if not user:
await websocket.close(code=1008) # Policy violation
return
# Check board membership
is_member = await check_board_membership(user.id, board_id)
if not is_member:
await websocket.close(code=1008)
return
await manager.connect(board_id, websocket)
...
Testing Authentication
Getting a Test Token
In development, you can quickly get a token for testing:
-
Register a user:
-
Login to get token:
-
Use token in requests:
Testing Protected Endpoints
Without authentication:
With authentication:
curl http://localhost:8000/boards \
-H "Authorization: Bearer <token>"
# Response: 200 OK with board list
With insufficient permissions:
curl -X DELETE http://localhost:8000/boards/{board_id} \
-H "Authorization: Bearer <member_token>"
# Response: 403 Forbidden (only owners can delete)
Troubleshooting
"Could not validate credentials"
- Token is malformed or corrupted
- Token has expired (check
expclaim) - Token was signed with different secret
- User account was deactivated
"User must be a board member"
- User is authenticated but not a member of the requested board
- Board was deleted
- User was removed from board
"Insufficient permissions"
- User is a board member but lacks required role
- Trying to perform owner-only action as member
- Check role in BoardMember table
Token not found
- Missing Authorization header
- Header not formatted as "Bearer
" - Cookie not being sent (check cookie settings)
- CORS blocking credentials (need
credentials: 'include')