Skip to main content
Asta supports multi-user authentication with JWT tokens and role-based access control (RBAC). This page explains the authentication system, user roles, and how to manage users.

Overview

Dual Authentication Modes

Asta automatically switches between single-user and multi-user modes:
  • Single-user mode: No users in database → Legacy Bearer token or open access
  • Multi-user mode: Users exist → JWT authentication with roles

Authentication Flow

Multi-User Mode (JWT)

When users exist in the database:
1

User Registration

New users self-register via the login page:
POST /api/auth/register
{
  "username": "alice",
  "password": "secure_password"
}
  • Creates user with user role (non-admin)
  • Password hashed with bcrypt
  • Returns user object (no token yet)
2

Login

Users authenticate to receive JWT:
POST /api/auth/login
{
  "username": "alice",
  "password": "secure_password"
}
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "alice",
    "role": "user"
  }
}
3

Request Authentication

Include JWT in requests:Authorization Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Or Query Parameter:
GET /api/chat/conversations?token=eyJhbGciOiJIUzI1NiIs...
4

Token Validation

AuthMiddleware validates the JWT:
  • Decodes token and verifies signature
  • Extracts user_id, username, role
  • Sets request.state.user_id, request.state.user_role
  • Rejects invalid/expired tokens with 401

Single-User Mode (Legacy)

When no users exist in the database:
1

Check for Token

Asta checks for ASTA_API_TOKEN in environment:
# backend/.env
ASTA_API_TOKEN=your_secret_token
2

Local Access

Requests from localhost (127.0.0.1, ::1) skip auth automatically
3

Remote Access

Remote requests (e.g., via Cloudflare Tunnel) must provide Bearer token:
Authorization: Bearer your_secret_token
4

Open Access

If no ASTA_API_TOKEN is set, all requests allowed (local dev)

User Roles

Admin Role

Permissions:
  • ✅ Full access to all features
  • ✅ Create, edit, delete users
  • ✅ Access all settings tabs
  • ✅ Configure API keys and integrations
  • ✅ Manage skills, agents, cron jobs
  • ✅ Execute any command (exec tool)
  • ✅ Access all files and directories
  • ✅ View system status and logs
UI Access:
  • General (full)
  • Memories
  • Keys
  • Models
  • Permissions
  • Skills
  • Agents
  • Channels
  • Cron
  • Google
  • Spotify
  • Network
  • Knowledge
  • About
How to create:
# First user is always admin
POST /api/auth/register
{
  "username": "admin",
  "password": "secure_admin_pass"
}

Implementation Details

JWT Token Structure

Payload:
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",  // user_id
  "username": "alice",
  "role": "user",
  "exp": 1735689600  // Expiry (30 days)
}
Signing (backend/app/auth_utils.py):
import jwt
from datetime import datetime, timedelta
from app.config import get_settings

def create_jwt(user_id: str, username: str, role: str) -> str:
    """Create JWT token valid for 30 days."""
    payload = {
        "sub": user_id,
        "username": username,
        "role": role,
        "exp": datetime.utcnow() + timedelta(days=30),
    }
    return jwt.encode(payload, get_settings().jwt_secret_key, algorithm="HS256")
Validation:
def decode_jwt(token: str) -> dict | None:
    """Decode and validate JWT. Returns None if invalid/expired."""
    try:
        return jwt.decode(
            token,
            get_settings().jwt_secret_key,
            algorithms=["HS256"],
        )
    except jwt.ExpiredSignatureError:
        return None  # Token expired
    except jwt.InvalidTokenError:
        return None  # Invalid token

AuthMiddleware

Location: backend/app/auth_middleware.py
class AuthMiddleware(BaseHTTPMiddleware):
    """JWT auth when users exist; legacy Bearer token fallback otherwise."""

    async def dispatch(self, request: Request, call_next):
        # Always allow CORS preflight
        if request.method == "OPTIONS":
            return await call_next(request)

        # Always allow public paths
        if request.url.path in _PUBLIC_PATHS:
            return await call_next(request)

        multi_user = await _check_multi_user()

        if multi_user:
            return await self._jwt_auth(request, call_next)
        else:
            return await self._legacy_bearer_auth(request, call_next)

    async def _jwt_auth(self, request: Request, call_next):
        """Multi-user JWT mode."""
        from app.auth_utils import decode_jwt

        token = self._extract_token(request)
        if not token:
            return JSONResponse({"detail": "Authentication required"}, status_code=401)

        payload = decode_jwt(token)
        if not payload:
            return JSONResponse({"detail": "Invalid or expired token"}, status_code=401)

        request.state.user_id = payload.get("sub", "")
        request.state.user_role = payload.get("role", "user")
        request.state.username = payload.get("username", "")
        return await call_next(request)

    async def _legacy_bearer_auth(self, request: Request, call_next):
        """No users → single-user mode with optional Bearer token."""
        from app.config import get_settings

        token = (get_settings().asta_api_token or "").strip()

        # No token configured → open access
        if not token:
            request.state.user_id = "default"
            request.state.user_role = "admin"
            request.state.username = "admin"
            return await call_next(request)

        # Local requests skip auth
        client_host = request.client.host if request.client else ""
        if client_host in ("127.0.0.1", "::1", "localhost"):
            request.state.user_id = "default"
            request.state.user_role = "admin"
            request.state.username = "admin"
            return await call_next(request)

        # Check Bearer token
        provided = self._extract_bearer(request)
        if provided and provided == token:
            request.state.user_id = "default"
            request.state.user_role = "admin"
            request.state.username = "admin"
            return await call_next(request)

        return JSONResponse({"detail": "Unauthorized"}, status_code=401)

Role-Based Access Control

Helper Functions (backend/app/auth_utils.py):
def get_current_user_id(request: Request) -> str:
    """Extract user_id from request state."""
    return getattr(request.state, "user_id", "default")

def get_current_user_role(request: Request) -> str:
    """Extract user role from request state."""
    return getattr(request.state, "user_role", "user")

def require_admin(request: Request) -> None:
    """Raise 403 if user is not admin."""
    if get_current_user_role(request) != "admin":
        raise HTTPException(403, "Admin access required")
Usage in Endpoints:
from app.auth_utils import require_admin, get_current_user_id

@router.post("/settings/keys")
async def update_keys(request: Request, body: KeysUpdate):
    require_admin(request)  # Only admin can update keys
    user_id = get_current_user_id(request)
    # ... rest of endpoint

Per-User Data Isolation

User Memories:
workspace/
├── USER.md                      # Global (single-user mode)
└── users/
    ├── user-a-uuid/
    │   └── USER.md              # User A's memories
    └── user-b-uuid/
        └── USER.md              # User B's memories
Database Queries:
# All queries scoped by user_id
await db.get_conversations(user_id)
await db.get_user_settings(user_id)
await db.get_skill_toggles(user_id)
await db.get_provider_runtime_states(user_id, providers)

User Management API

POST /api/auth/login

Login with username and password.Request:
{
  "username": "alice",
  "password": "secure_password"
}
Response (200):
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "alice",
    "role": "user"
  }
}
Error (401):
{"detail": "Invalid username or password"}

POST /api/auth/register

Self-registration (creates non-admin user).Request:
{
  "username": "bob",
  "password": "bob_password"
}
Response (200):
{
  "user": {
    "id": "660e8400-e29b-41d4-a716-446655440000",
    "username": "bob",
    "role": "user"
  }
}
Errors:
  • 403 - Registration not available (no existing users)
  • 409 - Username already exists
  • 400 - Invalid username/password (too short)

GET /api/auth/me

Get current authenticated user.Response:
{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "alice",
  "role": "user"
}

Security Considerations

Password Security

  • Passwords hashed with bcrypt (cost factor 12)
  • Minimum 4 characters (configurable)
  • Never stored or logged in plaintext
  • Secure comparison prevents timing attacks

JWT Security

  • Signed with HS256 algorithm
  • 30-day expiry (configurable)
  • Secret key from JWT_SECRET_KEY env var
  • Invalidation on server restart if secret changes

CORS Protection

  • CORS middleware wraps auth
  • Proper preflight handling
  • 401 responses include CORS headers
  • Configurable allowed origins

Rate Limiting

  • Consider adding rate limiting for login endpoint
  • Prevent brute force attacks
  • IP-based or user-based limits
  • Not currently implemented - can be added via middleware
Security Best Practices:
  • Set strong JWT_SECRET_KEY in production (32+ random bytes)
  • Use HTTPS in production (Cloudflare Tunnel recommended)
  • Regularly rotate JWT secret if compromised
  • Monitor failed login attempts
  • Implement account lockout after N failed attempts

Migration from Single-User to Multi-User

1

Backup Workspace

cp -r workspace workspace.backup
cp backend/asta.db backend/asta.db.backup
2

Create First Admin User

curl -X POST http://localhost:8000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin_password"}'
First registered user automatically becomes admin.
3

Migrate USER.md

Default user’s workspace/USER.md is automatically used for user “default” in single-user mode.For multi-user:
# Create per-user directory
mkdir -p workspace/users/<admin-user-id>/

# Copy USER.md
cp workspace/USER.md workspace/users/<admin-user-id>/USER.md
4

Update Environment

Remove ASTA_API_TOKEN from .env (no longer used in multi-user mode):
# backend/.env
# ASTA_API_TOKEN=...  # Remove this line
5

Restart Asta

./asta.sh restart
Middleware automatically detects multi-user mode and switches to JWT auth.

Troubleshooting

In multi-user mode:
  • Ensure you’re sending valid JWT token
  • Check token hasn’t expired (30 days)
  • Verify JWT_SECRET_KEY hasn’t changed
  • Try logging in again to get fresh token
In single-user mode:
  • Check if ASTA_API_TOKEN is set in .env
  • Verify token matches what you’re sending
  • Confirm request is from localhost or includes token
If registration fails with 403:
  • No users exist yet (database empty)
  • First user must be created directly in database or via special setup
If registration fails with 409:
  • Username already taken
  • Try a different username
Database fix:
# Promote user to admin
sqlite3 backend/asta.db "UPDATE users SET role='admin' WHERE username='alice';"

# Restart Asta
./asta.sh restart
Check system time:
  • JWT expiry uses UTC time
  • Ensure system clock is correct
  • Check exp claim in JWT:
    # Decode JWT (without verification)
    echo 'eyJhbGc...' | base64 -d
    

Next Steps

Architecture

Understand how auth fits into Asta’s architecture

API Reference

Full authentication API documentation

Quickstart

Get started with setting up your first user

Security

Security best practices and guidelines

Build docs developers (and LLMs) love