Compare commits

...

3 Commits

Author SHA1 Message Date
enyst
8316ec9eb3 Integrate AuthSystem docs into documentation navigation
- Add AuthSystem design documents to docs.json navigation under Architecture section
- Convert .md files to .mdx format for proper documentation system integration
- Add frontmatter with appropriate titles for both documents
- Documents now accessible in deployed documentation under OpenHands Developers > Architecture
2025-09-04 07:43:29 +00:00
enyst
059e5b7af6 Clean up AuthSystem docs: remove emojis, fix token references, focus on architectural benefits
- Remove all emojis for professional presentation
- Fix provider_tokens references (was dependency injection, not route parameters)
- Streamline benefits section to focus on architectural/codebase/extensibility improvements
- Remove promotional language and focus on technical merits
- Maintain clear documentation of the design and implementation approach
2025-09-04 07:06:12 +00:00
enyst
d858882c3d Add AuthSystem design documentation
- Comprehensive AuthSystem design supporting None/SU/MU strategies
- Based on GitHub issues #10751 and #10730 analysis
- Includes before/after code examples and Mermaid diagrams
- Provides clean abstraction boundaries and migration plan
- Extension points for custom builds with multi-user support

Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-04 06:54:53 +00:00
5 changed files with 2143 additions and 1 deletions

View File

@@ -153,7 +153,9 @@
"group": "Architecture",
"pages": [
"usage/architecture/backend",
"usage/architecture/runtime"
"usage/architecture/runtime",
"usage/architecture/auth-system-summary",
"usage/architecture/auth-system-design"
]
},
"usage/how-to/debugging",

View File

@@ -0,0 +1,856 @@
# OpenHands AuthSystem Design
## Executive Summary
This document proposes a comprehensive AuthSystem design for OpenHands that supports three authentication strategies: **None** (current behavior), **Single User (SU)** with GitHub OAuth, and **Multi User (MU)** (for custom builds). The design introduces clean abstraction boundaries, eliminates scattered `user_id` threading, and provides a foundation for future authentication enhancements.
## Problem Statement
### Current Issues
1. **No Auth Strategy Abstraction**: OpenHands currently has a monolithic `DefaultUserAuth` that always returns `None` for `user_id`, with no clear path to support different authentication modes.
2. **Scattered user_id Threading**: 339+ occurrences of `user_id` across 68 files, with complex threading through:
- Storage partitioning (`users/{user_id}/` paths)
- Conversation/session scoping
- API route dependencies
- Provider token resolution
- Data model fields
3. **Provider Token Pollution**: Routes accept `provider_tokens` parameters and thread them through `ProviderHandler`, creating security risks and complex signatures.
4. **No Single User Support**: No way to enable GitHub OAuth for personal/single-user deployments while maintaining the simplicity of the current "None" mode.
5. **Boundary Violations**: Auth concerns are mixed with business logic throughout the codebase, making it difficult to switch between authentication modes.
### Requirements from GitHub Issues
From **Issue #10751** (user_id audit):
- Support None, SU, and MU modes
- Introduce `UserContext` and `StorageNamespace` abstractions
- Remove redundant `if user_id` guards (7 identified)
- Clean up storage path helpers
From **Issue #10730** (token provider):
- Remove `provider_tokens` dependency injection
- Introduce `TokenProvider` boundary abstraction
- Support backend-only credential resolution
- Enable custom builds with token refresh/rotation patterns
## Solution Architecture
### Core Components
#### 1. AuthStrategy Interface
```python
# openhands/auth/strategies/base.py
from abc import ABC, abstractmethod
from typing import Optional
from fastapi import Request
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
class AuthStrategy(ABC):
"""Base class for authentication strategies"""
@abstractmethod
def get_name(self) -> str:
"""Return strategy name for logging/debugging"""
@abstractmethod
def requires_auth(self) -> bool:
"""Whether this strategy requires user authentication"""
@abstractmethod
async def authenticate(self, request: Request) -> Optional[UserContext]:
"""Authenticate request and return UserContext or None"""
@abstractmethod
async def get_token_provider(self, request: Request) -> TokenProvider:
"""Get token provider for this request"""
@abstractmethod
def get_login_url(self) -> Optional[str]:
"""Get login URL for frontend, None if no auth required"""
```
#### 2. UserContext
```python
# openhands/auth/user_context.py
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass(frozen=True)
class UserContext:
"""Immutable user context for authenticated requests"""
user_id: str
email: Optional[str] = None
username: Optional[str] = None
github_id: Optional[int] = None
github_username: Optional[str] = None
is_admin: bool = False
created_at: Optional[datetime] = None
last_login: Optional[datetime] = None
@property
def storage_namespace(self) -> str:
"""Get storage namespace for this user"""
return self.user_id
```
#### 3. TokenProvider Interface
```python
# openhands/auth/token_provider.py
from abc import ABC, abstractmethod
from typing import Optional, Mapping
from openhands.integrations.service_types import ProviderType
from openhands.integrations.provider import ProviderToken
class TokenProvider(ABC):
"""Abstract token provider for git integrations"""
@abstractmethod
async def get_token(self, provider: ProviderType) -> Optional[ProviderToken]:
"""Get token for specific provider"""
@abstractmethod
async def get_all_tokens(self) -> Mapping[ProviderType, ProviderToken]:
"""Get all available provider tokens"""
```
#### 4. StorageNamespace
```python
# openhands/auth/storage_namespace.py
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class StorageNamespace:
"""Encapsulates storage path logic for user data"""
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
def get_conversation_events_dir(self, sid: str) -> str:
return f'{self.get_conversation_dir(sid)}events/'
def get_conversation_metadata_filename(self, sid: str) -> str:
return f'{self.get_conversation_dir(sid)}metadata.json'
# ... other path methods
```
### Authentication Strategies
#### 1. None Strategy (Current Behavior)
```python
# openhands/auth/strategies/none_strategy.py
from typing import Optional
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider, DefaultTokenProvider
class NoneStrategy(AuthStrategy):
"""No authentication - current OpenHands behavior"""
def get_name(self) -> str:
return "none"
def requires_auth(self) -> bool:
return False
async def authenticate(self, request: Request) -> Optional[UserContext]:
return None # No user context
async def get_token_provider(self, request: Request) -> TokenProvider:
return DefaultTokenProvider() # Uses secrets.json
def get_login_url(self) -> Optional[str]:
return None
```
#### 2. Single User Strategy
```python
# openhands/auth/strategies/single_user_strategy.py
from typing import Optional
from fastapi import Request, HTTPException
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider, SingleUserTokenProvider
from openhands.server.shared import server_config
class SingleUserStrategy(AuthStrategy):
"""Single user with GitHub OAuth"""
def get_name(self) -> str:
return "single_user"
def requires_auth(self) -> bool:
return server_config.enable_su_auth
async def authenticate(self, request: Request) -> Optional[UserContext]:
if not self.requires_auth():
# SU mode without auth - create virtual user
return UserContext(
user_id="local",
username="local_user",
is_admin=True
)
# Extract JWT token from cookie/header
token = self._extract_token(request)
if not token:
return None
# Validate JWT and extract user info
user_data = self._validate_jwt(token)
if not user_data:
return None
# Verify user is allowed (if configured)
if (server_config.su_github_username and
user_data.get('github_username') != server_config.su_github_username):
raise HTTPException(403, "Access denied")
return UserContext(
user_id=user_data['github_username'],
email=user_data.get('email'),
username=user_data['github_username'],
github_id=user_data.get('github_id'),
github_username=user_data['github_username'],
is_admin=True # SU user is always admin
)
async def get_token_provider(self, request: Request) -> TokenProvider:
user_context = await self.authenticate(request)
return SingleUserTokenProvider(user_context)
def get_login_url(self) -> Optional[str]:
if not self.requires_auth():
return None
return f"/api/auth/github/login"
```
#### 3. Multi User Strategy (Custom Build Extension Point)
```python
# openhands/auth/strategies/multi_user_strategy.py
from typing import Optional
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
class MultiUserStrategy(AuthStrategy):
"""Multi-user strategy - extension point for custom builds"""
def get_name(self) -> str:
return "multi_user"
def requires_auth(self) -> bool:
return True
async def authenticate(self, request: Request) -> Optional[UserContext]:
# This would be implemented by custom builds/applications built on OH
raise NotImplementedError("Multi-user strategy not available in base OpenHands")
async def get_token_provider(self, request: Request) -> TokenProvider:
raise NotImplementedError("Multi-user strategy not available in base OpenHands")
def get_login_url(self) -> Optional[str]:
return "/api/auth/login"
```
### Integration Points
#### 1. Updated UserAuth
```python
# openhands/server/user_auth/strategy_user_auth.py
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.storage_namespace import StorageNamespace
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.settings.settings_store import SettingsStore
from openhands.storage.secrets.secrets_store import SecretsStore
class StrategyUserAuth(UserAuth):
"""UserAuth implementation using AuthStrategy pattern"""
def __init__(self, strategy: AuthStrategy, user_context: Optional[UserContext]):
self.strategy = strategy
self.user_context = user_context
self._storage_namespace = StorageNamespace(
user_context.storage_namespace if user_context else None
)
async def get_user_id(self) -> str | None:
return self.user_context.user_id if self.user_context else None
async def get_user_email(self) -> str | None:
return self.user_context.email if self.user_context else None
# ... other methods using storage_namespace
```
#### 2. FastAPI Dependencies
```python
# openhands/server/dependencies/auth.py
from fastapi import Depends, Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
from openhands.server.shared import get_auth_strategy
async def get_current_user(
request: Request,
strategy: AuthStrategy = Depends(get_auth_strategy)
) -> Optional[UserContext]:
"""Get current user context"""
return await strategy.authenticate(request)
async def get_token_provider(
request: Request,
strategy: AuthStrategy = Depends(get_auth_strategy)
) -> TokenProvider:
"""Get token provider for current request"""
return await strategy.get_token_provider(request)
async def require_auth(
user: Optional[UserContext] = Depends(get_current_user)
) -> UserContext:
"""Require authentication"""
if not user:
raise HTTPException(401, "Authentication required")
return user
```
#### 3. Updated Routes
```python
# openhands/server/routes/git.py (AFTER)
from fastapi import APIRouter, Depends
from openhands.auth.token_provider import TokenProvider
from openhands.auth.user_context import UserContext
from openhands.server.dependencies.auth import get_token_provider, get_current_user
from openhands.integrations.provider import ProviderHandler
app = APIRouter(prefix='/api/user')
@app.get('/repositories')
async def get_user_repositories(
sort: str = "pushed",
selected_provider: ProviderType | None = None,
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user)
):
"""Get user repositories - no provider_tokens parameter!"""
client = ProviderHandler(token_provider=token_provider)
return await client.get_repositories(sort, selected_provider)
```
## Before/After Code Comparison
### Before: Current Implementation
```python
# BEFORE: openhands/server/routes/git.py
@app.get('/repositories', response_model=list[Repository])
async def get_user_repositories(
sort: str = Query(default='pushed'),
selected_provider: ProviderType | None = Query(default=None),
page: int | None = Query(default=None),
per_page: int | None = Query(default=None),
installation_id: str | None = Query(default=None),
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
access_token: SecretStr | None = Depends(get_access_token),
user_id: str | None = Depends(get_user_id),
):
if provider_tokens:
client = ProviderHandler(
provider_tokens=provider_tokens,
external_auth_token=access_token,
external_auth_id=user_id,
)
# ... complex logic
```
```python
# BEFORE: openhands/storage/locations.py
def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
if user_id:
return f'users/{user_id}/conversations/{sid}/'
else:
return f'sessions/{sid}/'
```
```python
# BEFORE: openhands/server/user_auth/default_user_auth.py
class DefaultUserAuth(UserAuth):
async def get_user_id(self) -> str | None:
return None # Always None - no multi-tenancy support
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
user_secrets = await self.get_user_secrets()
if user_secrets is None:
return None
return user_secrets.provider_tokens
```
### After: Proposed Implementation
```python
# AFTER: openhands/server/routes/git.py
@app.get('/repositories', response_model=list[Repository])
async def get_user_repositories(
sort: str = Query(default='pushed'),
selected_provider: ProviderType | None = Query(default=None),
page: int | None = Query(default=None),
per_page: int | None = Query(default=None),
installation_id: str | None = Query(default=None),
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user),
):
client = ProviderHandler(token_provider=token_provider)
return await client.get_repositories(
sort, server_config.app_mode, selected_provider, page, per_page, installation_id
)
```
```python
# AFTER: openhands/auth/storage_namespace.py
@dataclass(frozen=True)
class StorageNamespace:
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
```
```python
# AFTER: openhands/server/user_auth/strategy_user_auth.py
class StrategyUserAuth(UserAuth):
def __init__(self, strategy: AuthStrategy, user_context: Optional[UserContext]):
self.strategy = strategy
self.user_context = user_context
self.storage_namespace = StorageNamespace(
user_context.storage_namespace if user_context else None
)
async def get_user_id(self) -> str | None:
return self.user_context.user_id if self.user_context else None
```
## Configuration
### Environment Variables
```bash
# Authentication Strategy
OH_AUTH_STRATEGY=none # Options: none, single_user, multi_user
# Single User Mode Settings
OH_ENABLE_SU_AUTH=false # Enable GitHub OAuth in SU mode
OH_SU_GITHUB_USERNAME=your_username # Restrict access to specific user
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
# Multi User Mode (custom build extension point)
OH_MU_ADMIN_USERNAME=admin_user
```
### Configuration Modes
#### 1. None Mode (Current Default)
```bash
OH_AUTH_STRATEGY=none
# No additional config needed
```
#### 2. Single User - No Auth
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
```
#### 3. Single User - GitHub Auth
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=your_username
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
```
## Implementation Benefits
### 1. Clean Separation of Concerns
- Auth logic isolated in strategy classes
- Business logic doesn't need to know about user_id
- Clear boundaries between auth and core functionality
### 2. Reduced Complexity
- Eliminates 7 redundant `if user_id` guards
- Removes provider_tokens dependency injection
- Simplifies method signatures throughout codebase
### 3. Forward Compatibility
- custom builds can extend with custom strategies
- Token refresh/rotation support built-in
- Multi-tenancy ready without core changes
### 4. Security Improvements
- Tokens never exposed in route parameters
- Centralized token management
- Immutable user context prevents tampering
### 5. Developer Experience
- Clear configuration options
- Easy mode switching
- Consistent patterns across codebase
## Migration Strategy
### Phase 1: Foundation
1. Introduce auth strategy interfaces
2. Add UserContext and StorageNamespace
3. Create TokenProvider abstraction
4. Update core dependencies
### Phase 2: Strategy Implementation
1. Implement NoneStrategy (backward compatible)
2. Implement SingleUserStrategy
3. Add configuration support
4. Update UserAuth integration
### Phase 3: Route Migration
1. Update FastAPI dependencies
2. Remove provider_tokens dependency injection
3. Update ProviderHandler integration
4. Clean up redundant if-guards
### Phase 4: Storage Migration
1. Replace storage path helpers
2. Update conversation managers
3. Migrate event stores
4. Clean up legacy code
## Testing Strategy
### Unit Tests
- Strategy implementations
- UserContext immutability
- StorageNamespace path generation
- TokenProvider implementations
### Integration Tests
- End-to-end auth flows
- Route authentication
- Storage partitioning
- Configuration switching
### Migration Tests
- Backward compatibility
- Data migration paths
- Configuration validation
## Future Extensions
### custom builds Integration Points
```python
# custom builds can provide their own strategies
class custom buildsMultiUserStrategy(AuthStrategy):
async def authenticate(self, request: Request) -> Optional[UserContext]:
# Custom custom builds authentication logic
pass
async def get_token_provider(self, request: Request) -> TokenProvider:
# custom builds token refresh/rotation
return CustomBuildTokenProvider(request)
```
### Additional Auth Methods
- SAML/OIDC strategies
- API key authentication
- Custom JWT providers
- Enterprise SSO integration
## Architecture Diagrams
### 1. Overall Auth System Architecture
```mermaid
graph TB
subgraph "FastAPI Application"
Routes[API Routes]
Deps[FastAPI Dependencies]
end
subgraph "Auth Layer"
AuthStrategy[AuthStrategy Interface]
NoneStrategy[NoneStrategy]
SUStrategy[SingleUserStrategy]
MUStrategy[MultiUserStrategy]
end
subgraph "Core Abstractions"
UserContext[UserContext]
TokenProvider[TokenProvider Interface]
StorageNamespace[StorageNamespace]
end
subgraph "Token Providers"
DefaultTP[DefaultTokenProvider]
SingleUserTP[SingleUserTokenProvider]
custom buildsTP[CustomBuildTokenProvider]
end
subgraph "Storage Layer"
SecretsStore[SecretsStore]
SettingsStore[SettingsStore]
ConversationStore[ConversationStore]
end
Routes --> Deps
Deps --> AuthStrategy
AuthStrategy --> UserContext
AuthStrategy --> TokenProvider
UserContext --> StorageNamespace
NoneStrategy --> DefaultTP
SUStrategy --> SingleUserTP
MUStrategy --> custom buildsTP
TokenProvider --> SecretsStore
StorageNamespace --> ConversationStore
StorageNamespace --> SettingsStore
```
### 2. Authentication Flow - None Strategy
```mermaid
sequenceDiagram
participant Client
participant Route
participant NoneStrategy
participant DefaultTP
participant SecretsStore
Client->>Route: API Request
Route->>NoneStrategy: authenticate(request)
NoneStrategy->>Route: None (no user context)
Route->>NoneStrategy: get_token_provider(request)
NoneStrategy->>DefaultTP: create()
DefaultTP->>SecretsStore: load secrets.json
SecretsStore->>DefaultTP: provider tokens
DefaultTP->>Route: token provider
Route->>Client: API Response
```
### 3. Authentication Flow - Single User Strategy (No Auth)
```mermaid
sequenceDiagram
participant Client
participant Route
participant SUStrategy
participant UserContext
participant SingleUserTP
participant SecretsStore
Client->>Route: API Request
Route->>SUStrategy: authenticate(request)
SUStrategy->>UserContext: create virtual user (local)
UserContext->>SUStrategy: user context
SUStrategy->>Route: UserContext(user_id="local")
Route->>SUStrategy: get_token_provider(request)
SUStrategy->>SingleUserTP: create(user_context)
SingleUserTP->>SecretsStore: load user secrets
SecretsStore->>SingleUserTP: provider tokens
SingleUserTP->>Route: token provider
Route->>Client: API Response
```
### 4. Authentication Flow - Single User Strategy (GitHub Auth)
```mermaid
sequenceDiagram
participant Client
participant Route
participant SUStrategy
participant GitHub
participant UserContext
participant SingleUserTP
participant Database
Client->>Route: API Request with JWT cookie
Route->>SUStrategy: authenticate(request)
SUStrategy->>SUStrategy: extract JWT token
SUStrategy->>SUStrategy: validate JWT
SUStrategy->>SUStrategy: check allowed user
SUStrategy->>UserContext: create from JWT data
UserContext->>SUStrategy: user context
SUStrategy->>Route: UserContext
Route->>SUStrategy: get_token_provider(request)
SUStrategy->>SingleUserTP: create(user_context)
SingleUserTP->>Database: load encrypted tokens
Database->>SingleUserTP: encrypted provider tokens
SingleUserTP->>Route: token provider
Route->>Client: API Response
```
### 5. GitHub OAuth Flow - Single User Strategy
```mermaid
sequenceDiagram
participant Client
participant AuthRoute
participant GitHub
participant SUStrategy
participant Database
participant UserContext
Client->>AuthRoute: GET /auth/login
AuthRoute->>Client: GitHub OAuth URL
Client->>GitHub: OAuth authorization
GitHub->>AuthRoute: GET /auth/callback?code=xxx
AuthRoute->>GitHub: exchange code for token
GitHub->>AuthRoute: access token + user info
AuthRoute->>SUStrategy: validate user allowed
SUStrategy->>AuthRoute: user authorized
AuthRoute->>Database: create/update user record
Database->>AuthRoute: user saved
AuthRoute->>AuthRoute: create JWT token
AuthRoute->>Client: Set JWT cookie + redirect
```
### 6. Storage Namespace Architecture
```mermaid
graph TB
subgraph "User Context"
UC[UserContext]
UC --> SN[StorageNamespace]
end
subgraph "Storage Paths"
SN --> ConvDir[get_conversation_dir]
SN --> EventsDir[get_events_dir]
SN --> MetaFile[get_metadata_file]
SN --> StateFile[get_state_file]
end
subgraph "Path Examples"
ConvDir --> NonePath[sessions/sid/]
ConvDir --> UserPath[users/user_id/conversations/sid/]
EventsDir --> NoneEvents[sessions/sid/events/]
EventsDir --> UserEvents[users/user_id/conversations/sid/events/]
end
subgraph "Strategy Impact"
NoneStrategy2[NoneStrategy] --> NonePath
SUStrategy2[SingleUserStrategy] --> UserPath
MUStrategy2[MultiUserStrategy] --> UserPath
end
```
### 7. Token Provider Architecture
```mermaid
graph TB
subgraph "Token Provider Interface"
TP[TokenProvider]
TP --> GetToken[get_token(provider)]
TP --> GetAllTokens[get_all_tokens()]
end
subgraph "Implementations"
DefaultTP2[DefaultTokenProvider]
SingleUserTP2[SingleUserTokenProvider]
custom buildsTP2[CustomBuildTokenProvider]
end
subgraph "Token Sources"
SecretsJSON[secrets.json]
UserDB[User Database]
Custom Build API[custom builds Token API]
end
subgraph "Provider Integration"
ProviderHandler[ProviderHandler]
GitHubService[GitHubService]
GitLabService[GitLabService]
BitBucketService[BitBucketService]
end
DefaultTP2 --> SecretsJSON
SingleUserTP2 --> UserDB
custom buildsTP2 --> Custom Build API
TP --> ProviderHandler
ProviderHandler --> GitHubService
ProviderHandler --> GitLabService
ProviderHandler --> BitBucketService
```
### 8. Configuration-Driven Strategy Selection
```mermaid
graph TB
subgraph "Configuration"
Config[OH_AUTH_STRATEGY]
Config --> None[none]
Config --> SU[single_user]
Config --> MU[multi_user]
end
subgraph "Strategy Factory"
Factory[AuthStrategyFactory]
Factory --> CreateNone[create NoneStrategy]
Factory --> CreateSU[create SingleUserStrategy]
Factory --> CreateMU[create MultiUserStrategy]
end
subgraph "Additional Config"
SUConfig[OH_ENABLE_SU_AUTH<br/>OH_SU_GITHUB_USERNAME<br/>OH_GITHUB_CLIENT_ID]
MUConfig[OH_MU_ADMIN_USERNAME<br/>Database Config]
end
None --> CreateNone
SU --> CreateSU
MU --> CreateMU
CreateSU --> SUConfig
CreateMU --> MUConfig
```
## Conclusion
This AuthSystem design provides OpenHands with a robust, extensible authentication foundation that:
1. **Maintains backward compatibility** with the current "None" mode
2. **Enables Single User mode** with optional GitHub OAuth
3. **Provides extension points** for custom builds with multi-user implementations
4. **Cleans up the codebase** by removing scattered user_id threading
5. **Improves security** by centralizing token management
6. **Simplifies development** with clear abstractions and patterns
The design is ready for implementation and will significantly improve OpenHands' authentication capabilities while maintaining its current simplicity for users who don't need authentication.

View File

@@ -0,0 +1,860 @@
---
title: AuthSystem Design - Complete Specification
---
# OpenHands AuthSystem Design
## Executive Summary
This document proposes a comprehensive AuthSystem design for OpenHands that supports three authentication strategies: **None** (current behavior), **Single User (SU)** with GitHub OAuth, and **Multi User (MU)** (for custom builds). The design introduces clean abstraction boundaries, eliminates scattered `user_id` threading, and provides a foundation for future authentication enhancements.
## Problem Statement
### Current Issues
1. **No Auth Strategy Abstraction**: OpenHands currently has a monolithic `DefaultUserAuth` that always returns `None` for `user_id`, with no clear path to support different authentication modes.
2. **Scattered user_id Threading**: 339+ occurrences of `user_id` across 68 files, with complex threading through:
- Storage partitioning (`users/{user_id}/` paths)
- Conversation/session scoping
- API route dependencies
- Provider token resolution
- Data model fields
3. **Provider Token Pollution**: Routes accept `provider_tokens` parameters and thread them through `ProviderHandler`, creating security risks and complex signatures.
4. **No Single User Support**: No way to enable GitHub OAuth for personal/single-user deployments while maintaining the simplicity of the current "None" mode.
5. **Boundary Violations**: Auth concerns are mixed with business logic throughout the codebase, making it difficult to switch between authentication modes.
### Requirements from GitHub Issues
From **Issue #10751** (user_id audit):
- Support None, SU, and MU modes
- Introduce `UserContext` and `StorageNamespace` abstractions
- Remove redundant `if user_id` guards (7 identified)
- Clean up storage path helpers
From **Issue #10730** (token provider):
- Remove `provider_tokens` dependency injection
- Introduce `TokenProvider` boundary abstraction
- Support backend-only credential resolution
- Enable custom builds with token refresh/rotation patterns
## Solution Architecture
### Core Components
#### 1. AuthStrategy Interface
```python
# openhands/auth/strategies/base.py
from abc import ABC, abstractmethod
from typing import Optional
from fastapi import Request
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
class AuthStrategy(ABC):
"""Base class for authentication strategies"""
@abstractmethod
def get_name(self) -> str:
"""Return strategy name for logging/debugging"""
@abstractmethod
def requires_auth(self) -> bool:
"""Whether this strategy requires user authentication"""
@abstractmethod
async def authenticate(self, request: Request) -> Optional[UserContext]:
"""Authenticate request and return UserContext or None"""
@abstractmethod
async def get_token_provider(self, request: Request) -> TokenProvider:
"""Get token provider for this request"""
@abstractmethod
def get_login_url(self) -> Optional[str]:
"""Get login URL for frontend, None if no auth required"""
```
#### 2. UserContext
```python
# openhands/auth/user_context.py
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass(frozen=True)
class UserContext:
"""Immutable user context for authenticated requests"""
user_id: str
email: Optional[str] = None
username: Optional[str] = None
github_id: Optional[int] = None
github_username: Optional[str] = None
is_admin: bool = False
created_at: Optional[datetime] = None
last_login: Optional[datetime] = None
@property
def storage_namespace(self) -> str:
"""Get storage namespace for this user"""
return self.user_id
```
#### 3. TokenProvider Interface
```python
# openhands/auth/token_provider.py
from abc import ABC, abstractmethod
from typing import Optional, Mapping
from openhands.integrations.service_types import ProviderType
from openhands.integrations.provider import ProviderToken
class TokenProvider(ABC):
"""Abstract token provider for git integrations"""
@abstractmethod
async def get_token(self, provider: ProviderType) -> Optional[ProviderToken]:
"""Get token for specific provider"""
@abstractmethod
async def get_all_tokens(self) -> Mapping[ProviderType, ProviderToken]:
"""Get all available provider tokens"""
```
#### 4. StorageNamespace
```python
# openhands/auth/storage_namespace.py
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class StorageNamespace:
"""Encapsulates storage path logic for user data"""
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
def get_conversation_events_dir(self, sid: str) -> str:
return f'{self.get_conversation_dir(sid)}events/'
def get_conversation_metadata_filename(self, sid: str) -> str:
return f'{self.get_conversation_dir(sid)}metadata.json'
# ... other path methods
```
### Authentication Strategies
#### 1. None Strategy (Current Behavior)
```python
# openhands/auth/strategies/none_strategy.py
from typing import Optional
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider, DefaultTokenProvider
class NoneStrategy(AuthStrategy):
"""No authentication - current OpenHands behavior"""
def get_name(self) -> str:
return "none"
def requires_auth(self) -> bool:
return False
async def authenticate(self, request: Request) -> Optional[UserContext]:
return None # No user context
async def get_token_provider(self, request: Request) -> TokenProvider:
return DefaultTokenProvider() # Uses secrets.json
def get_login_url(self) -> Optional[str]:
return None
```
#### 2. Single User Strategy
```python
# openhands/auth/strategies/single_user_strategy.py
from typing import Optional
from fastapi import Request, HTTPException
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider, SingleUserTokenProvider
from openhands.server.shared import server_config
class SingleUserStrategy(AuthStrategy):
"""Single user with GitHub OAuth"""
def get_name(self) -> str:
return "single_user"
def requires_auth(self) -> bool:
return server_config.enable_su_auth
async def authenticate(self, request: Request) -> Optional[UserContext]:
if not self.requires_auth():
# SU mode without auth - create virtual user
return UserContext(
user_id="local",
username="local_user",
is_admin=True
)
# Extract JWT token from cookie/header
token = self._extract_token(request)
if not token:
return None
# Validate JWT and extract user info
user_data = self._validate_jwt(token)
if not user_data:
return None
# Verify user is allowed (if configured)
if (server_config.su_github_username and
user_data.get('github_username') != server_config.su_github_username):
raise HTTPException(403, "Access denied")
return UserContext(
user_id=user_data['github_username'],
email=user_data.get('email'),
username=user_data['github_username'],
github_id=user_data.get('github_id'),
github_username=user_data['github_username'],
is_admin=True # SU user is always admin
)
async def get_token_provider(self, request: Request) -> TokenProvider:
user_context = await self.authenticate(request)
return SingleUserTokenProvider(user_context)
def get_login_url(self) -> Optional[str]:
if not self.requires_auth():
return None
return f"/api/auth/github/login"
```
#### 3. Multi User Strategy (Custom Build Extension Point)
```python
# openhands/auth/strategies/multi_user_strategy.py
from typing import Optional
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
class MultiUserStrategy(AuthStrategy):
"""Multi-user strategy - extension point for custom builds"""
def get_name(self) -> str:
return "multi_user"
def requires_auth(self) -> bool:
return True
async def authenticate(self, request: Request) -> Optional[UserContext]:
# This would be implemented by custom builds/applications built on OH
raise NotImplementedError("Multi-user strategy not available in base OpenHands")
async def get_token_provider(self, request: Request) -> TokenProvider:
raise NotImplementedError("Multi-user strategy not available in base OpenHands")
def get_login_url(self) -> Optional[str]:
return "/api/auth/login"
```
### Integration Points
#### 1. Updated UserAuth
```python
# openhands/server/user_auth/strategy_user_auth.py
from fastapi import Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.storage_namespace import StorageNamespace
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.settings.settings_store import SettingsStore
from openhands.storage.secrets.secrets_store import SecretsStore
class StrategyUserAuth(UserAuth):
"""UserAuth implementation using AuthStrategy pattern"""
def __init__(self, strategy: AuthStrategy, user_context: Optional[UserContext]):
self.strategy = strategy
self.user_context = user_context
self._storage_namespace = StorageNamespace(
user_context.storage_namespace if user_context else None
)
async def get_user_id(self) -> str | None:
return self.user_context.user_id if self.user_context else None
async def get_user_email(self) -> str | None:
return self.user_context.email if self.user_context else None
# ... other methods using storage_namespace
```
#### 2. FastAPI Dependencies
```python
# openhands/server/dependencies/auth.py
from fastapi import Depends, Request
from openhands.auth.strategies.base import AuthStrategy
from openhands.auth.user_context import UserContext
from openhands.auth.token_provider import TokenProvider
from openhands.server.shared import get_auth_strategy
async def get_current_user(
request: Request,
strategy: AuthStrategy = Depends(get_auth_strategy)
) -> Optional[UserContext]:
"""Get current user context"""
return await strategy.authenticate(request)
async def get_token_provider(
request: Request,
strategy: AuthStrategy = Depends(get_auth_strategy)
) -> TokenProvider:
"""Get token provider for current request"""
return await strategy.get_token_provider(request)
async def require_auth(
user: Optional[UserContext] = Depends(get_current_user)
) -> UserContext:
"""Require authentication"""
if not user:
raise HTTPException(401, "Authentication required")
return user
```
#### 3. Updated Routes
```python
# openhands/server/routes/git.py (AFTER)
from fastapi import APIRouter, Depends
from openhands.auth.token_provider import TokenProvider
from openhands.auth.user_context import UserContext
from openhands.server.dependencies.auth import get_token_provider, get_current_user
from openhands.integrations.provider import ProviderHandler
app = APIRouter(prefix='/api/user')
@app.get('/repositories')
async def get_user_repositories(
sort: str = "pushed",
selected_provider: ProviderType | None = None,
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user)
):
"""Get user repositories - no provider_tokens parameter!"""
client = ProviderHandler(token_provider=token_provider)
return await client.get_repositories(sort, selected_provider)
```
## Before/After Code Comparison
### Before: Current Implementation
```python
# BEFORE: openhands/server/routes/git.py
@app.get('/repositories', response_model=list[Repository])
async def get_user_repositories(
sort: str = Query(default='pushed'),
selected_provider: ProviderType | None = Query(default=None),
page: int | None = Query(default=None),
per_page: int | None = Query(default=None),
installation_id: str | None = Query(default=None),
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
access_token: SecretStr | None = Depends(get_access_token),
user_id: str | None = Depends(get_user_id),
):
if provider_tokens:
client = ProviderHandler(
provider_tokens=provider_tokens,
external_auth_token=access_token,
external_auth_id=user_id,
)
# ... complex logic
```
```python
# BEFORE: openhands/storage/locations.py
def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
if user_id:
return f'users/{user_id}/conversations/{sid}/'
else:
return f'sessions/{sid}/'
```
```python
# BEFORE: openhands/server/user_auth/default_user_auth.py
class DefaultUserAuth(UserAuth):
async def get_user_id(self) -> str | None:
return None # Always None - no multi-tenancy support
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
user_secrets = await self.get_user_secrets()
if user_secrets is None:
return None
return user_secrets.provider_tokens
```
### After: Proposed Implementation
```python
# AFTER: openhands/server/routes/git.py
@app.get('/repositories', response_model=list[Repository])
async def get_user_repositories(
sort: str = Query(default='pushed'),
selected_provider: ProviderType | None = Query(default=None),
page: int | None = Query(default=None),
per_page: int | None = Query(default=None),
installation_id: str | None = Query(default=None),
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user),
):
client = ProviderHandler(token_provider=token_provider)
return await client.get_repositories(
sort, server_config.app_mode, selected_provider, page, per_page, installation_id
)
```
```python
# AFTER: openhands/auth/storage_namespace.py
@dataclass(frozen=True)
class StorageNamespace:
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
```
```python
# AFTER: openhands/server/user_auth/strategy_user_auth.py
class StrategyUserAuth(UserAuth):
def __init__(self, strategy: AuthStrategy, user_context: Optional[UserContext]):
self.strategy = strategy
self.user_context = user_context
self.storage_namespace = StorageNamespace(
user_context.storage_namespace if user_context else None
)
async def get_user_id(self) -> str | None:
return self.user_context.user_id if self.user_context else None
```
## Configuration
### Environment Variables
```bash
# Authentication Strategy
OH_AUTH_STRATEGY=none # Options: none, single_user, multi_user
# Single User Mode Settings
OH_ENABLE_SU_AUTH=false # Enable GitHub OAuth in SU mode
OH_SU_GITHUB_USERNAME=your_username # Restrict access to specific user
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
# Multi User Mode (custom build extension point)
OH_MU_ADMIN_USERNAME=admin_user
```
### Configuration Modes
#### 1. None Mode (Current Default)
```bash
OH_AUTH_STRATEGY=none
# No additional config needed
```
#### 2. Single User - No Auth
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
```
#### 3. Single User - GitHub Auth
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=your_username
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
```
## Implementation Benefits
### 1. Clean Separation of Concerns
- Auth logic isolated in strategy classes
- Business logic doesn't need to know about user_id
- Clear boundaries between auth and core functionality
### 2. Reduced Complexity
- Eliminates 7 redundant `if user_id` guards
- Removes provider_tokens dependency injection
- Simplifies method signatures throughout codebase
### 3. Forward Compatibility
- custom builds can extend with custom strategies
- Token refresh/rotation support built-in
- Multi-tenancy ready without core changes
### 4. Security Improvements
- Tokens never exposed in route parameters
- Centralized token management
- Immutable user context prevents tampering
### 5. Developer Experience
- Clear configuration options
- Easy mode switching
- Consistent patterns across codebase
## Migration Strategy
### Phase 1: Foundation
1. Introduce auth strategy interfaces
2. Add UserContext and StorageNamespace
3. Create TokenProvider abstraction
4. Update core dependencies
### Phase 2: Strategy Implementation
1. Implement NoneStrategy (backward compatible)
2. Implement SingleUserStrategy
3. Add configuration support
4. Update UserAuth integration
### Phase 3: Route Migration
1. Update FastAPI dependencies
2. Remove provider_tokens dependency injection
3. Update ProviderHandler integration
4. Clean up redundant if-guards
### Phase 4: Storage Migration
1. Replace storage path helpers
2. Update conversation managers
3. Migrate event stores
4. Clean up legacy code
## Testing Strategy
### Unit Tests
- Strategy implementations
- UserContext immutability
- StorageNamespace path generation
- TokenProvider implementations
### Integration Tests
- End-to-end auth flows
- Route authentication
- Storage partitioning
- Configuration switching
### Migration Tests
- Backward compatibility
- Data migration paths
- Configuration validation
## Future Extensions
### custom builds Integration Points
```python
# custom builds can provide their own strategies
class custom buildsMultiUserStrategy(AuthStrategy):
async def authenticate(self, request: Request) -> Optional[UserContext]:
# Custom custom builds authentication logic
pass
async def get_token_provider(self, request: Request) -> TokenProvider:
# custom builds token refresh/rotation
return CustomBuildTokenProvider(request)
```
### Additional Auth Methods
- SAML/OIDC strategies
- API key authentication
- Custom JWT providers
- Enterprise SSO integration
## Architecture Diagrams
### 1. Overall Auth System Architecture
```mermaid
graph TB
subgraph "FastAPI Application"
Routes[API Routes]
Deps[FastAPI Dependencies]
end
subgraph "Auth Layer"
AuthStrategy[AuthStrategy Interface]
NoneStrategy[NoneStrategy]
SUStrategy[SingleUserStrategy]
MUStrategy[MultiUserStrategy]
end
subgraph "Core Abstractions"
UserContext[UserContext]
TokenProvider[TokenProvider Interface]
StorageNamespace[StorageNamespace]
end
subgraph "Token Providers"
DefaultTP[DefaultTokenProvider]
SingleUserTP[SingleUserTokenProvider]
custom buildsTP[CustomBuildTokenProvider]
end
subgraph "Storage Layer"
SecretsStore[SecretsStore]
SettingsStore[SettingsStore]
ConversationStore[ConversationStore]
end
Routes --> Deps
Deps --> AuthStrategy
AuthStrategy --> UserContext
AuthStrategy --> TokenProvider
UserContext --> StorageNamespace
NoneStrategy --> DefaultTP
SUStrategy --> SingleUserTP
MUStrategy --> custom buildsTP
TokenProvider --> SecretsStore
StorageNamespace --> ConversationStore
StorageNamespace --> SettingsStore
```
### 2. Authentication Flow - None Strategy
```mermaid
sequenceDiagram
participant Client
participant Route
participant NoneStrategy
participant DefaultTP
participant SecretsStore
Client->>Route: API Request
Route->>NoneStrategy: authenticate(request)
NoneStrategy->>Route: None (no user context)
Route->>NoneStrategy: get_token_provider(request)
NoneStrategy->>DefaultTP: create()
DefaultTP->>SecretsStore: load secrets.json
SecretsStore->>DefaultTP: provider tokens
DefaultTP->>Route: token provider
Route->>Client: API Response
```
### 3. Authentication Flow - Single User Strategy (No Auth)
```mermaid
sequenceDiagram
participant Client
participant Route
participant SUStrategy
participant UserContext
participant SingleUserTP
participant SecretsStore
Client->>Route: API Request
Route->>SUStrategy: authenticate(request)
SUStrategy->>UserContext: create virtual user (local)
UserContext->>SUStrategy: user context
SUStrategy->>Route: UserContext(user_id="local")
Route->>SUStrategy: get_token_provider(request)
SUStrategy->>SingleUserTP: create(user_context)
SingleUserTP->>SecretsStore: load user secrets
SecretsStore->>SingleUserTP: provider tokens
SingleUserTP->>Route: token provider
Route->>Client: API Response
```
### 4. Authentication Flow - Single User Strategy (GitHub Auth)
```mermaid
sequenceDiagram
participant Client
participant Route
participant SUStrategy
participant GitHub
participant UserContext
participant SingleUserTP
participant Database
Client->>Route: API Request with JWT cookie
Route->>SUStrategy: authenticate(request)
SUStrategy->>SUStrategy: extract JWT token
SUStrategy->>SUStrategy: validate JWT
SUStrategy->>SUStrategy: check allowed user
SUStrategy->>UserContext: create from JWT data
UserContext->>SUStrategy: user context
SUStrategy->>Route: UserContext
Route->>SUStrategy: get_token_provider(request)
SUStrategy->>SingleUserTP: create(user_context)
SingleUserTP->>Database: load encrypted tokens
Database->>SingleUserTP: encrypted provider tokens
SingleUserTP->>Route: token provider
Route->>Client: API Response
```
### 5. GitHub OAuth Flow - Single User Strategy
```mermaid
sequenceDiagram
participant Client
participant AuthRoute
participant GitHub
participant SUStrategy
participant Database
participant UserContext
Client->>AuthRoute: GET /auth/login
AuthRoute->>Client: GitHub OAuth URL
Client->>GitHub: OAuth authorization
GitHub->>AuthRoute: GET /auth/callback?code=xxx
AuthRoute->>GitHub: exchange code for token
GitHub->>AuthRoute: access token + user info
AuthRoute->>SUStrategy: validate user allowed
SUStrategy->>AuthRoute: user authorized
AuthRoute->>Database: create/update user record
Database->>AuthRoute: user saved
AuthRoute->>AuthRoute: create JWT token
AuthRoute->>Client: Set JWT cookie + redirect
```
### 6. Storage Namespace Architecture
```mermaid
graph TB
subgraph "User Context"
UC[UserContext]
UC --> SN[StorageNamespace]
end
subgraph "Storage Paths"
SN --> ConvDir[get_conversation_dir]
SN --> EventsDir[get_events_dir]
SN --> MetaFile[get_metadata_file]
SN --> StateFile[get_state_file]
end
subgraph "Path Examples"
ConvDir --> NonePath[sessions/sid/]
ConvDir --> UserPath[users/user_id/conversations/sid/]
EventsDir --> NoneEvents[sessions/sid/events/]
EventsDir --> UserEvents[users/user_id/conversations/sid/events/]
end
subgraph "Strategy Impact"
NoneStrategy2[NoneStrategy] --> NonePath
SUStrategy2[SingleUserStrategy] --> UserPath
MUStrategy2[MultiUserStrategy] --> UserPath
end
```
### 7. Token Provider Architecture
```mermaid
graph TB
subgraph "Token Provider Interface"
TP[TokenProvider]
TP --> GetToken[get_token(provider)]
TP --> GetAllTokens[get_all_tokens()]
end
subgraph "Implementations"
DefaultTP2[DefaultTokenProvider]
SingleUserTP2[SingleUserTokenProvider]
custom buildsTP2[CustomBuildTokenProvider]
end
subgraph "Token Sources"
SecretsJSON[secrets.json]
UserDB[User Database]
Custom Build API[custom builds Token API]
end
subgraph "Provider Integration"
ProviderHandler[ProviderHandler]
GitHubService[GitHubService]
GitLabService[GitLabService]
BitBucketService[BitBucketService]
end
DefaultTP2 --> SecretsJSON
SingleUserTP2 --> UserDB
custom buildsTP2 --> Custom Build API
TP --> ProviderHandler
ProviderHandler --> GitHubService
ProviderHandler --> GitLabService
ProviderHandler --> BitBucketService
```
### 8. Configuration-Driven Strategy Selection
```mermaid
graph TB
subgraph "Configuration"
Config[OH_AUTH_STRATEGY]
Config --> None[none]
Config --> SU[single_user]
Config --> MU[multi_user]
end
subgraph "Strategy Factory"
Factory[AuthStrategyFactory]
Factory --> CreateNone[create NoneStrategy]
Factory --> CreateSU[create SingleUserStrategy]
Factory --> CreateMU[create MultiUserStrategy]
end
subgraph "Additional Config"
SUConfig[OH_ENABLE_SU_AUTH<br/>OH_SU_GITHUB_USERNAME<br/>OH_GITHUB_CLIENT_ID]
MUConfig[OH_MU_ADMIN_USERNAME<br/>Database Config]
end
None --> CreateNone
SU --> CreateSU
MU --> CreateMU
CreateSU --> SUConfig
CreateMU --> MUConfig
```
## Conclusion
This AuthSystem design provides OpenHands with a robust, extensible authentication foundation that:
1. **Maintains backward compatibility** with the current "None" mode
2. **Enables Single User mode** with optional GitHub OAuth
3. **Provides extension points** for custom builds with multi-user implementations
4. **Cleans up the codebase** by removing scattered user_id threading
5. **Improves security** by centralizing token management
6. **Simplifies development** with clear abstractions and patterns
The design is ready for implementation and will significantly improve OpenHands' authentication capabilities while maintaining its current simplicity for users who don't need authentication.

View File

@@ -0,0 +1,210 @@
# OpenHands AuthSystem Design - Executive Summary
## Goal
Design a flexible authentication system for OpenHands that supports three strategies:
- **None**: Current behavior (no auth, optional GitHub token)
- **SU (Single User)**: GitHub OAuth for personal use
- **MU (Multi User)**: Extension point for custom builds (not in base OH)
## Current Problems
- 339+ `user_id` occurrences scattered across 68 files
- No auth strategy abstraction
- `provider_tokens` dependency injection complexity
- No single-user GitHub OAuth support
- Mixed auth/business logic concerns
## Solution Architecture
### Core Components
1. **AuthStrategy Interface** - Pluggable auth strategies
2. **UserContext** - Immutable user data container
3. **TokenProvider** - Centralized token management
4. **StorageNamespace** - Clean storage path abstraction
### Auth Strategies
```python
# None Strategy (current behavior)
OH_AUTH_STRATEGY=none
# Single User - No Auth (virtual user)
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
# Single User - GitHub OAuth
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=your_username
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
```
## 🔄 Key Changes
### Before (Current)
```python
# Route with complex dependencies
async def get_repositories(
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
user_id: str | None = Depends(get_user_id),
):
client = ProviderHandler(
provider_tokens=provider_tokens,
external_auth_id=user_id,
)
# Scattered path logic
def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
if user_id:
return f'users/{user_id}/conversations/{sid}/'
else:
return f'sessions/{sid}/'
```
### After (Proposed)
```python
# Clean route signature
async def get_repositories(
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user),
):
client = ProviderHandler(token_provider=token_provider)
# Encapsulated storage logic
@dataclass(frozen=True)
class StorageNamespace:
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
```
## Architectural Benefits
### Codebase Cleanup
- Removes 7 redundant `if user_id` guards across the codebase
- Eliminates `provider_tokens` dependency injection complexity
- Reduces method signature complexity throughout the system
- Centralizes storage path logic in dedicated abstractions
### Extensibility
- Strategy pattern enables custom build extension points
- Token refresh/rotation patterns built-in
- Multi-tenancy ready without core changes
- Additional auth methods can be added without refactoring
### Code Organization
- Clear separation of auth and business logic
- Consistent patterns across all authentication modes
- Centralized token and credential management
- Immutable user context prevents state corruption
## Implementation Plan
### Phase 1: Foundation
- [ ] Auth strategy interfaces
- [ ] UserContext & StorageNamespace
- [ ] TokenProvider abstraction
- [ ] Core dependencies
### Phase 2: Strategies
- [ ] NoneStrategy (backward compatible)
- [ ] SingleUserStrategy
- [ ] Configuration support
- [ ] UserAuth integration
### Phase 3: Routes
- [ ] Update FastAPI dependencies
- [ ] Remove provider_tokens
- [ ] Update ProviderHandler
- [ ] Clean redundant guards
### Phase 4: Storage
- [ ] Replace path helpers
- [ ] Update conversation managers
- [ ] Migrate event stores
- [ ] Legacy cleanup
## Architecture Highlights
### Strategy Pattern
```python
class AuthStrategy(ABC):
@abstractmethod
async def authenticate(self, request: Request) -> Optional[UserContext]:
pass
@abstractmethod
async def get_token_provider(self, request: Request) -> TokenProvider:
pass
```
### Immutable User Context
```python
@dataclass(frozen=True)
class UserContext:
user_id: str
email: Optional[str] = None
username: Optional[str] = None
is_admin: bool = False
```
### Token Provider Interface
```python
class TokenProvider(ABC):
@abstractmethod
async def get_token(self, provider: ProviderType) -> Optional[ProviderToken]:
pass
```
## 🔧 Configuration Examples
### Current Default (None)
```bash
# No configuration needed - maintains current behavior
```
### Personal Use (SU without auth)
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
# Creates virtual "local" user, uses secrets.json
```
### Personal Use (SU with GitHub)
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=myusername
OH_GITHUB_CLIENT_ID=abc123
OH_GITHUB_CLIENT_SECRET=secret456
# Requires GitHub OAuth, restricts to specific user
```
## Implementation Readiness
### Backward Compatibility
- None strategy maintains exact current behavior
- No breaking changes for existing users
- Gradual migration path available
### Code Quality Improvements
- Reduces complexity from 339 to ~50 user_id references
- Introduces clear abstractions and boundaries
- Enables better testing and maintainability
### Extensibility Foundation
- Custom builds can add authentication strategies
- Token refresh/rotation patterns built-in
- Multi-tenancy foundation without core changes
## Summary
This design provides a clean authentication architecture for OpenHands with three key outcomes:
1. **Maintains simplicity** - Current users see no changes
2. **Enables extension** - Custom builds can add authentication features
3. **Improves codebase** - Reduces scattered auth logic and complexity
The architecture is well-defined with a clear migration path.

View File

@@ -0,0 +1,214 @@
---
title: AuthSystem Design - Executive Summary
---
# OpenHands AuthSystem Design - Executive Summary
## Goal
Design a flexible authentication system for OpenHands that supports three strategies:
- **None**: Current behavior (no auth, optional GitHub token)
- **SU (Single User)**: GitHub OAuth for personal use
- **MU (Multi User)**: Extension point for custom builds (not in base OH)
## Current Problems
- 339+ `user_id` occurrences scattered across 68 files
- No auth strategy abstraction
- `provider_tokens` dependency injection complexity
- No single-user GitHub OAuth support
- Mixed auth/business logic concerns
## Solution Architecture
### Core Components
1. **AuthStrategy Interface** - Pluggable auth strategies
2. **UserContext** - Immutable user data container
3. **TokenProvider** - Centralized token management
4. **StorageNamespace** - Clean storage path abstraction
### Auth Strategies
```python
# None Strategy (current behavior)
OH_AUTH_STRATEGY=none
# Single User - No Auth (virtual user)
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
# Single User - GitHub OAuth
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=your_username
OH_GITHUB_CLIENT_ID=your_client_id
OH_GITHUB_CLIENT_SECRET=your_client_secret
```
## 🔄 Key Changes
### Before (Current)
```python
# Route with complex dependencies
async def get_repositories(
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
user_id: str | None = Depends(get_user_id),
):
client = ProviderHandler(
provider_tokens=provider_tokens,
external_auth_id=user_id,
)
# Scattered path logic
def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
if user_id:
return f'users/{user_id}/conversations/{sid}/'
else:
return f'sessions/{sid}/'
```
### After (Proposed)
```python
# Clean route signature
async def get_repositories(
token_provider: TokenProvider = Depends(get_token_provider),
user: Optional[UserContext] = Depends(get_current_user),
):
client = ProviderHandler(token_provider=token_provider)
# Encapsulated storage logic
@dataclass(frozen=True)
class StorageNamespace:
namespace: Optional[str]
def get_conversation_dir(self, sid: str) -> str:
if self.namespace:
return f'users/{self.namespace}/conversations/{sid}/'
return f'sessions/{sid}/'
```
## Architectural Benefits
### Codebase Cleanup
- Removes 7 redundant `if user_id` guards across the codebase
- Eliminates `provider_tokens` dependency injection complexity
- Reduces method signature complexity throughout the system
- Centralizes storage path logic in dedicated abstractions
### Extensibility
- Strategy pattern enables custom build extension points
- Token refresh/rotation patterns built-in
- Multi-tenancy ready without core changes
- Additional auth methods can be added without refactoring
### Code Organization
- Clear separation of auth and business logic
- Consistent patterns across all authentication modes
- Centralized token and credential management
- Immutable user context prevents state corruption
## Implementation Plan
### Phase 1: Foundation
- [ ] Auth strategy interfaces
- [ ] UserContext & StorageNamespace
- [ ] TokenProvider abstraction
- [ ] Core dependencies
### Phase 2: Strategies
- [ ] NoneStrategy (backward compatible)
- [ ] SingleUserStrategy
- [ ] Configuration support
- [ ] UserAuth integration
### Phase 3: Routes
- [ ] Update FastAPI dependencies
- [ ] Remove provider_tokens
- [ ] Update ProviderHandler
- [ ] Clean redundant guards
### Phase 4: Storage
- [ ] Replace path helpers
- [ ] Update conversation managers
- [ ] Migrate event stores
- [ ] Legacy cleanup
## Architecture Highlights
### Strategy Pattern
```python
class AuthStrategy(ABC):
@abstractmethod
async def authenticate(self, request: Request) -> Optional[UserContext]:
pass
@abstractmethod
async def get_token_provider(self, request: Request) -> TokenProvider:
pass
```
### Immutable User Context
```python
@dataclass(frozen=True)
class UserContext:
user_id: str
email: Optional[str] = None
username: Optional[str] = None
is_admin: bool = False
```
### Token Provider Interface
```python
class TokenProvider(ABC):
@abstractmethod
async def get_token(self, provider: ProviderType) -> Optional[ProviderToken]:
pass
```
## 🔧 Configuration Examples
### Current Default (None)
```bash
# No configuration needed - maintains current behavior
```
### Personal Use (SU without auth)
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=false
# Creates virtual "local" user, uses secrets.json
```
### Personal Use (SU with GitHub)
```bash
OH_AUTH_STRATEGY=single_user
OH_ENABLE_SU_AUTH=true
OH_SU_GITHUB_USERNAME=myusername
OH_GITHUB_CLIENT_ID=abc123
OH_GITHUB_CLIENT_SECRET=secret456
# Requires GitHub OAuth, restricts to specific user
```
## Implementation Readiness
### Backward Compatibility
- None strategy maintains exact current behavior
- No breaking changes for existing users
- Gradual migration path available
### Code Quality Improvements
- Reduces complexity from 339 to ~50 user_id references
- Introduces clear abstractions and boundaries
- Enables better testing and maintainability
### Extensibility Foundation
- Custom builds can add authentication strategies
- Token refresh/rotation patterns built-in
- Multi-tenancy foundation without core changes
## Summary
This design provides a clean authentication architecture for OpenHands with three key outcomes:
1. **Maintains simplicity** - Current users see no changes
2. **Enables extension** - Custom builds can add authentication features
3. **Improves codebase** - Reduces scattered auth logic and complexity
The architecture is well-defined with a clear migration path.