Commit b5766115 authored by Your Name's avatar Your Name

Add proxy support for OAuth2 Chrome extension

parent fbb49301
...@@ -140,6 +140,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -140,6 +140,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- OAuth2 endpoints: `/dashboard/claude/auth/start`, `/dashboard/claude/auth/complete`, `/dashboard/claude/auth/status` - OAuth2 endpoints: `/dashboard/claude/auth/start`, `/dashboard/claude/auth/complete`, `/dashboard/claude/auth/status`
- Extension endpoints: `/dashboard/extension/download`, `/dashboard/oauth2/callback` - Extension endpoints: `/dashboard/extension/download`, `/dashboard/oauth2/callback`
- Comprehensive documentation in CLAUDE_OAUTH2_SETUP.md and CLAUDE_OAUTH2_DEEP_DIVE.md - Comprehensive documentation in CLAUDE_OAUTH2_SETUP.md and CLAUDE_OAUTH2_DEEP_DIVE.md
- Proxy-aware OAuth2 extension serving: detects X-Forwarded-For, X-Forwarded-Host, X-Real-IP headers
- Force interception mechanism: extension activates for localhost when OAuth flow initiated from dashboard
- Supports reverse proxy deployments (nginx, caddy, etc.) with automatic proxy detection
- **Kiro Provider Integration**: Native support for Kiro (Amazon Q Developer / AWS CodeWhisperer) - **Kiro Provider Integration**: Native support for Kiro (Amazon Q Developer / AWS CodeWhisperer)
- KiroAuth class (`aisbf/kiro_auth.py`) for AWS credential management - KiroAuth class (`aisbf/kiro_auth.py`) for AWS credential management
- Support for multiple authentication methods: - Support for multiple authentication methods:
......
...@@ -676,6 +676,9 @@ Model metadata is automatically extracted from provider responses and stored in ...@@ -676,6 +676,9 @@ Model metadata is automatically extracted from provider responses and stored in
- Full OAuth2 PKCE authentication flow - Full OAuth2 PKCE authentication flow
- Automatic token refresh with refresh token rotation - Automatic token refresh with refresh token rotation
- Chrome extension for remote server OAuth2 callback interception - Chrome extension for remote server OAuth2 callback interception
- Proxy-aware extension serving: automatically detects reverse proxy deployments via X-Forwarded-* headers
- Force interception mechanism: extension activates for localhost when OAuth flow initiated from dashboard
- Supports nginx, caddy, and other reverse proxies with automatic proxy detection
- Dashboard integration with authentication UI - Dashboard integration with authentication UI
- Credentials stored in `~/.aisbf/claude_credentials.json` - Credentials stored in `~/.aisbf/claude_credentials.json`
- Optional curl_cffi TLS fingerprinting for Cloudflare bypass - Optional curl_cffi TLS fingerprinting for Cloudflare bypass
...@@ -694,6 +697,14 @@ Model metadata is automatically extracted from provider responses and stored in ...@@ -694,6 +697,14 @@ Model metadata is automatically extracted from provider responses and stored in
- Supports IDE credentials and CLI authentication - Supports IDE credentials and CLI authentication
- Access to Claude models through Kiro - Access to Claude models through Kiro
- No separate API key required (uses Kiro credentials) - No separate API key required (uses Kiro credentials)
### Kilocode
- OAuth2 Device Authorization Grant flow
- Supports both API key and OAuth2 authentication
- Seamless integration with Kilocode services
- Dashboard OAuth2 authentication UI
- Credentials stored in ~/.kilo_credentials.json
- Access to Kilocode AI models and services
- Supports streaming, tool calling, and extended thinking - Supports streaming, tool calling, and extended thinking
## Rotation Models ## Rotation Models
......
...@@ -137,6 +137,34 @@ pip install aisbf ...@@ -137,6 +137,34 @@ pip install aisbf
sudo pip install aisbf sudo pip install aisbf
``` ```
## Post-Installation OAuth2 Setup
AISBF supports OAuth2 authentication for several providers:
### Claude (Anthropic)
- Full OAuth2 PKCE flow for Claude Code (claude.ai)
- Chrome extension for remote server deployments
- Proxy-aware: automatically detects reverse proxy deployments
- Dashboard integration for easy authentication
### Kiro (Amazon Q Developer)
- Native OAuth2 support for AWS CodeWhisperer
- Multiple authentication methods (IDE credentials, kiro-cli, direct refresh token)
- Automatic credential management
### Kilocode
- Device Authorization Grant OAuth2 flow
- Seamless integration with Kilocode services
**Setup Instructions:**
1. Start AISBF: `aisbf`
2. Access dashboard: `http://localhost:17765/dashboard`
3. Navigate to Providers section
4. Configure OAuth2 providers and follow authentication prompts
5. For remote deployments: Install Chrome extension from dashboard
For detailed OAuth2 setup, see README.md and DOCUMENTATION.md in the installed package.
## Troubleshooting ## Troubleshooting
### Build Errors ### Build Errors
......
...@@ -131,6 +131,7 @@ See [`PYPI.md`](PYPI.md) for detailed instructions on publishing to PyPI. ...@@ -131,6 +131,7 @@ See [`PYPI.md`](PYPI.md) for detailed instructions on publishing to PyPI.
- Claude Code (OAuth2 authentication via claude.ai) - Claude Code (OAuth2 authentication via claude.ai)
- Ollama (direct HTTP) - Ollama (direct HTTP)
- Kiro (Amazon Q Developer / AWS CodeWhisperer) - Kiro (Amazon Q Developer / AWS CodeWhisperer)
- Kilocode (OAuth2 Device Authorization Grant)
## Configuration ## Configuration
### SSL/TLS Configuration ### SSL/TLS Configuration
...@@ -287,6 +288,9 @@ AISBF supports Claude Code (claude.ai) as a provider using OAuth2 authentication ...@@ -287,6 +288,9 @@ AISBF supports Claude Code (claude.ai) as a provider using OAuth2 authentication
- Automatic token refresh with refresh token rotation - Automatic token refresh with refresh token rotation
- Chrome extension for remote server OAuth2 callback interception - Chrome extension for remote server OAuth2 callback interception
- Dashboard integration with authentication UI - Dashboard integration with authentication UI
- Proxy-aware extension serving: automatically detects reverse proxy deployments
- Force interception mechanism: extension activates for localhost when OAuth flow initiated from dashboard
- Supports nginx, caddy, and other reverse proxies with X-Forwarded-* header detection
- Credentials stored in `~/.aisbf/claude_credentials.json` - Credentials stored in `~/.aisbf/claude_credentials.json`
- Optional curl_cffi TLS fingerprinting for Cloudflare bypass - Optional curl_cffi TLS fingerprinting for Cloudflare bypass
- Compatible with official claude-cli credentials - Compatible with official claude-cli credentials
......
...@@ -46,6 +46,7 @@ import argparse ...@@ -46,6 +46,7 @@ import argparse
import secrets import secrets
import hashlib import hashlib
import asyncio import asyncio
import httpx
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import defaultdict from collections import defaultdict
...@@ -918,10 +919,62 @@ async def startup_event(): ...@@ -918,10 +919,62 @@ async def startup_event():
logger.warning("TOR hidden service initialization failed") logger.warning("TOR hidden service initialization failed")
# Pre-fetch models at startup for providers without local model config # Pre-fetch models at startup for providers without local model config
# For Kilo providers, check API key, OAuth2 file credentials, and database-stored credentials
logger.info("Pre-fetching models from providers with dynamic model lists...") logger.info("Pre-fetching models from providers with dynamic model lists...")
prefetch_count = 0 prefetch_count = 0
for provider_id, provider_config in config.providers.items(): for provider_id, provider_config in config.providers.items():
if not (hasattr(provider_config, 'models') and provider_config.models): if not (hasattr(provider_config, 'models') and provider_config.models):
# For Kilo providers, check if any authentication method is available
provider_type = getattr(provider_config, 'type', '')
if provider_type in ('kilo', 'kilocode'):
has_valid_auth = False
# Check 1: API key
api_key = getattr(provider_config, 'api_key', None)
if api_key and not api_key.startswith('YOUR_'):
has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has API key configured, fetching models...")
# Check 2: OAuth2 credentials file
if not has_valid_auth:
try:
from aisbf.kilo_oauth2 import KiloOAuth2
kilo_config = getattr(provider_config, 'kilo_config', None)
credentials_file = None
api_base = getattr(provider_config, 'endpoint', 'https://api.kilo.ai')
if kilo_config and isinstance(kilo_config, dict):
credentials_file = kilo_config.get('credentials_file')
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=api_base)
if oauth2.is_authenticated():
has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has valid OAuth2 credentials file, fetching models...")
except Exception as e:
logger.debug(f"Kilo provider '{provider_id}' OAuth2 file check failed: {e}")
# Check 3: Database-stored credentials (uploaded via dashboard)
if not has_valid_auth:
try:
from aisbf.database import get_database
db = get_database()
if db:
# Check for uploaded credentials files for this provider
auth_files = db.get_user_auth_files(0, provider_id) # 0 for admin/global
if auth_files:
for auth_file in auth_files:
file_type = auth_file.get('file_type', '')
file_path = auth_file.get('file_path', '')
if file_type in ('credentials', 'kilo_credentials', 'config') and file_path:
if os.path.exists(file_path):
has_valid_auth = True
logger.info(f"Kilo provider '{provider_id}' has uploaded credentials in database, fetching models...")
break
except Exception as e:
logger.debug(f"Kilo provider '{provider_id}' database credentials check failed: {e}")
if not has_valid_auth:
logger.info(f"Skipping model prefetch for Kilo provider '{provider_id}' (no API key, OAuth2 file, or uploaded credentials)")
continue
try: try:
models = await fetch_provider_models(provider_id) models = await fetch_provider_models(provider_id)
if models: if models:
...@@ -1404,6 +1457,137 @@ async def dashboard_providers(request: Request): ...@@ -1404,6 +1457,137 @@ async def dashboard_providers(request: Request):
"success": "Configuration saved successfully! Restart server for changes to take effect." if success else None "success": "Configuration saved successfully! Restart server for changes to take effect." if success else None
}) })
async def _auto_detect_provider_models(provider_key: str, provider: dict) -> list:
"""
Auto-detect models from a provider's API endpoint.
Tries to fetch models from the provider's /v1/models or /models endpoint.
For Kilo providers, uses OAuth2 authentication if available.
Args:
provider_key: Provider identifier (e.g., 'kilo', 'my-openai-provider')
provider: Provider configuration dict
Returns:
List of model dicts, or empty list if detection fails
"""
import logging
logger = logging.getLogger(__name__)
try:
endpoint = provider.get('endpoint', '')
if not endpoint:
logger.debug(f"No endpoint for provider '{provider_key}', skipping auto-detection")
return []
provider_type = provider.get('type', 'openai')
api_key = provider.get('api_key', '')
# Skip if API key is a placeholder
if api_key and api_key.startswith('YOUR_'):
api_key = ''
# Check if this is a Kilo provider (by type or by endpoint URL)
is_kilo_provider = provider_type in ('kilo', 'kilocode')
if not is_kilo_provider:
# Also check endpoint URL for Kilo domains
kilo_domains = ['kilocode.ai', 'api.kilo.ai', 'kilo.ai']
for domain in kilo_domains:
if domain in endpoint:
is_kilo_provider = True
break
# For Kilo providers, try to get OAuth2 token
if is_kilo_provider:
from aisbf.kilo_oauth2 import KiloOAuth2
kilo_config = provider.get('kilo_config', {})
credentials_file = kilo_config.get('credentials_file', '~/.kilo_credentials.json')
oauth2 = KiloOAuth2(credentials_file=credentials_file, api_base=endpoint)
token = oauth2.get_valid_token()
if token:
api_key = token
logger.info(f"Using OAuth2 token for Kilo provider '{provider_key}'")
else:
logger.warning(f"No OAuth2 token available for Kilo provider '{provider_key}', please authenticate first")
return []
# Skip if no authentication available
if not api_key:
logger.debug(f"No API key or token for provider '{provider_key}', skipping auto-detection")
return []
# Build models URL - try multiple paths
models_url = None
response_data = None
for path in ['/v1/models', '/models']:
test_url = endpoint.rstrip('/') + path
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
headers = {'Authorization': f'Bearer {api_key}'}
response = await client.get(test_url, headers=headers)
if response.status_code == 200:
models_url = test_url
response_data = response.json()
break
elif response.status_code == 401:
logger.debug(f"Authentication failed for {test_url}")
else:
logger.debug(f"Got status {response.status_code} from {test_url}")
except Exception as e:
logger.debug(f"Error fetching {test_url}: {e}")
continue
if not models_url or not response_data:
logger.debug(f"Could not reach models endpoint for provider '{provider_key}'")
return []
# Parse response - handle both OpenAI format {data: [...]} and array format
models_list = response_data.get('data', response_data) if isinstance(response_data, dict) else response_data
if not isinstance(models_list, list):
logger.debug(f"Unexpected models response format for provider '{provider_key}'")
return []
# Convert to our model format
detected_models = []
for model_data in models_list:
if isinstance(model_data, str):
# Simple string model ID
detected_models.append({
'name': model_data,
'rate_limit': 0,
'max_request_tokens': 100000,
'context_size': 100000
})
elif isinstance(model_data, dict):
# Dict with id/name
model_id = model_data.get('id', model_data.get('model', ''))
if not model_id:
continue
# Extract context size
context_size = (
model_data.get('context_window') or
model_data.get('context_length') or
model_data.get('max_input_tokens')
)
detected_models.append({
'name': model_id,
'rate_limit': 0,
'max_request_tokens': int(context_size) if context_size else 100000,
'context_size': int(context_size) if context_size else 100000
})
logger.info(f"Auto-detected {len(detected_models)} models for provider '{provider_key}' from {models_url}")
return detected_models
except Exception as e:
logger.warning(f"Failed to auto-detect models for provider '{provider_key}': {e}")
return []
@app.post("/dashboard/providers") @app.post("/dashboard/providers")
async def dashboard_providers_save(request: Request, config: str = Form(...)): async def dashboard_providers_save(request: Request, config: str = Form(...)):
"""Save providers configuration""" """Save providers configuration"""
...@@ -1423,6 +1607,23 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)): ...@@ -1423,6 +1607,23 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
if 'condense_context' not in model or model.get('condense_context') is None: if 'condense_context' not in model or model.get('condense_context') is None:
model['condense_context'] = 80 model['condense_context'] = 80
# Auto-detect models for providers with no models configured
auto_detected_count = 0
for provider_key, provider in providers_data.items():
# Check if provider has no models or empty models list
has_models = 'models' in provider and isinstance(provider.get('models'), list) and len(provider.get('models', [])) > 0
if not has_models:
# Try to auto-detect models from API
detected_models = await _auto_detect_provider_models(provider_key, provider)
if detected_models:
provider['models'] = detected_models
auto_detected_count += 1
logger.info(f"Auto-detected {len(detected_models)} models for provider '{provider_key}'")
if auto_detected_count > 0:
logger.info(f"Auto-detected models for {auto_detected_count} provider(s)")
# Load existing config to preserve structure # Load existing config to preserve structure
config_path = Path.home() / '.aisbf' / 'providers.json' config_path = Path.home() / '.aisbf' / 'providers.json'
if not config_path.exists(): if not config_path.exists():
...@@ -1441,11 +1642,15 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)): ...@@ -1441,11 +1642,15 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
with open(save_path, 'w') as f: with open(save_path, 'w') as f:
json.dump(full_config, f, indent=2) json.dump(full_config, f, indent=2)
success_msg = "Configuration saved successfully! Restart server for changes to take effect."
if auto_detected_count > 0:
success_msg = f"Configuration saved successfully! Auto-detected models for {auto_detected_count} provider(s). Restart server for changes to take effect."
return templates.TemplateResponse("dashboard/providers.html", { return templates.TemplateResponse("dashboard/providers.html", {
"request": request, "request": request,
"session": request.session, "session": request.session,
"providers_json": json.dumps(providers_data), "providers_json": json.dumps(providers_data),
"success": "Configuration saved successfully! Restart server for changes to take effect." "success": success_msg
}) })
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Reload current config on error # Reload current config on error
...@@ -2152,7 +2357,6 @@ async def dashboard_restart(request: Request): ...@@ -2152,7 +2357,6 @@ async def dashboard_restart(request: Request):
if auth_check: if auth_check:
return auth_check return auth_check
import os
import signal import signal
logger.info("Server restart requested from dashboard") logger.info("Server restart requested from dashboard")
...@@ -5689,8 +5893,16 @@ async def dashboard_claude_auth_start(request: Request): ...@@ -5689,8 +5893,16 @@ async def dashboard_claude_auth_start(request: Request):
request_host = request.headers.get('host', '').split(':')[0] request_host = request.headers.get('host', '').split(':')[0]
is_localhost_request = request_host in ['127.0.0.1', 'localhost', '::1'] is_localhost_request = request_host in ['127.0.0.1', 'localhost', '::1']
# Use local callback if accessing from localhost # Check if request is coming through a proxy
use_extension = not (is_local_access or is_localhost_request) has_proxy_headers = (
'X-Forwarded-For' in request.headers or
'X-Forwarded-Host' in request.headers or
'X-Real-IP' in request.headers
)
# Use local callback only if truly accessing from localhost (not behind proxy)
# If behind proxy, always serve extension even if request appears local
use_extension = not (is_local_access or is_localhost_request) or has_proxy_headers
# If using localhost, start the callback server # If using localhost, start the callback server
if not use_extension: if not use_extension:
...@@ -5893,5 +6105,272 @@ async def dashboard_claude_auth_status(request: Request): ...@@ -5893,5 +6105,272 @@ async def dashboard_claude_auth_status(request: Request):
) )
# Kilo OAuth2 authentication endpoints
@app.post("/dashboard/kilo/auth/start")
async def dashboard_kilo_auth_start(request: Request):
"""Start Kilo OAuth2 Device Authorization Grant flow"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
data = await request.json()
provider_key = data.get('provider_key')
credentials_file = data.get('credentials_file', '~/.kilo_credentials.json')
if not provider_key:
return JSONResponse(
status_code=400,
content={"success": False, "error": "Provider key is required"}
)
# Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2
# Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file)
# Initiate device authorization (async method)
device_auth = await auth.initiate_device_auth()
if not device_auth:
return JSONResponse(
status_code=500,
content={"success": False, "error": "Failed to initiate device authorization"}
)
# Store device code in session for polling
request.session['kilo_device_code'] = device_auth['code']
request.session['kilo_provider'] = provider_key
request.session['kilo_credentials_file'] = credentials_file
request.session['kilo_expires_at'] = time.time() + device_auth['expiresIn']
return JSONResponse({
"success": True,
"user_code": device_auth['code'],
"verification_uri": device_auth['verificationUrl'],
"expires_in": device_auth['expiresIn'],
"interval": 3,
"message": f"Please visit {device_auth['verificationUrl']} and enter code: {device_auth['code']}"
})
except Exception as e:
logger.error(f"Error starting Kilo auth: {e}")
return JSONResponse(
status_code=500,
content={"success": False, "error": str(e)}
)
@app.post("/dashboard/kilo/auth/poll")
async def dashboard_kilo_auth_poll(request: Request):
"""Poll Kilo OAuth2 device authorization status"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
# Get device code from session
device_code = request.session.get('kilo_device_code')
credentials_file = request.session.get('kilo_credentials_file', '~/.kilo_credentials.json')
expires_at = request.session.get('kilo_expires_at', 0)
if not device_code:
return JSONResponse(
status_code=400,
content={"success": False, "status": "error", "error": "No device authorization in progress"}
)
# Check if expired
if time.time() > expires_at:
# Clear session
request.session.pop('kilo_device_code', None)
request.session.pop('kilo_provider', None)
request.session.pop('kilo_credentials_file', None)
request.session.pop('kilo_expires_at', None)
return JSONResponse({
"success": False,
"status": "expired",
"error": "Device authorization expired"
})
# Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2
# Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file)
# Poll device authorization status (async method)
result = await auth.poll_device_auth(device_code)
if result['status'] == 'approved':
# Save credentials
token = result.get('token')
user_email = result.get('userEmail')
if token:
credentials = {
"type": "oauth",
"access": token,
"refresh": token, # Same token for both
"expires": int(time.time()) + (365 * 24 * 60 * 60), # 1 year
"userEmail": user_email
}
auth._save_credentials(credentials)
logger.info(f"KiloOAuth2: Saved credentials for {user_email}")
# Clear session data
request.session.pop('kilo_device_code', None)
request.session.pop('kilo_provider', None)
request.session.pop('kilo_credentials_file', None)
request.session.pop('kilo_expires_at', None)
return JSONResponse({
"success": True,
"status": "approved",
"message": "Authentication completed successfully"
})
elif result['status'] == 'pending':
return JSONResponse({
"success": True,
"status": "pending",
"message": "Waiting for user authorization"
})
elif result['status'] == 'denied':
# Clear session
request.session.pop('kilo_device_code', None)
request.session.pop('kilo_provider', None)
request.session.pop('kilo_credentials_file', None)
request.session.pop('kilo_expires_at', None)
return JSONResponse({
"success": False,
"status": "denied",
"error": "User denied authorization"
})
elif result['status'] == 'expired':
# Clear session
request.session.pop('kilo_device_code', None)
request.session.pop('kilo_provider', None)
request.session.pop('kilo_credentials_file', None)
request.session.pop('kilo_expires_at', None)
return JSONResponse({
"success": False,
"status": "expired",
"error": "Device authorization expired"
})
elif result['status'] == 'slow_down':
return JSONResponse({
"success": True,
"status": "slow_down",
"message": "Polling too frequently, slowing down"
})
else:
return JSONResponse({
"success": False,
"status": "error",
"error": result.get('error', 'Unknown error')
})
except Exception as e:
logger.error(f"Error polling Kilo auth: {e}")
return JSONResponse(
status_code=500,
content={"success": False, "status": "error", "error": str(e)}
)
@app.post("/dashboard/kilo/auth/status")
async def dashboard_kilo_auth_status(request: Request):
"""Check Kilo authentication status"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
data = await request.json()
provider_key = data.get('provider_key')
credentials_file = data.get('credentials_file', '~/.kilo_credentials.json')
if not provider_key:
return JSONResponse(
status_code=400,
content={"authenticated": False, "error": "Provider key is required"}
)
# Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2
# Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file)
# Check if authenticated
if auth.is_authenticated():
# Try to get a valid token (will refresh if needed)
token = auth.get_valid_token()
if token:
# Get token expiration info
expires_at = auth.credentials.get('expires', 0)
return JSONResponse({
"authenticated": True,
"expires_in": max(0, expires_at - time.time()),
"email": auth.credentials.get('userEmail')
})
return JSONResponse({
"authenticated": False
})
except Exception as e:
logger.error(f"Error checking Kilo auth status: {e}")
return JSONResponse(
status_code=500,
content={"authenticated": False, "error": str(e)}
)
@app.post("/dashboard/kilo/auth/logout")
async def dashboard_kilo_auth_logout(request: Request):
"""Logout from Kilo OAuth2 (clear stored credentials)"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
data = await request.json()
provider_key = data.get('provider_key')
credentials_file = data.get('credentials_file', '~/.kilo_credentials.json')
if not provider_key:
return JSONResponse(
status_code=400,
content={"success": False, "error": "Provider key is required"}
)
# Import KiloOAuth2
from aisbf.kilo_oauth2 import KiloOAuth2
# Create auth instance
auth = KiloOAuth2(credentials_file=credentials_file)
# Logout (clear credentials)
auth.logout()
return JSONResponse({
"success": True,
"message": "Logged out successfully"
})
except Exception as e:
logger.error(f"Error logging out from Kilo: {e}")
return JSONResponse(
status_code=500,
content={"success": False, "error": str(e)}
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
...@@ -14,7 +14,8 @@ const DEFAULT_CONFIG = { ...@@ -14,7 +14,8 @@ const DEFAULT_CONFIG = {
enabled: true, enabled: true,
remoteServer: '', // Will be set from AISBF dashboard remoteServer: '', // Will be set from AISBF dashboard
ports: [54545], // Default OAuth callback ports to intercept ports: [54545], // Default OAuth callback ports to intercept
paths: ['/callback', '/oauth/callback', '/auth/callback'] paths: ['/callback', '/oauth/callback', '/auth/callback'],
forceInterception: false // Override for OAuth flows initiated from AISBF
}; };
// Current configuration // Current configuration
...@@ -76,11 +77,16 @@ function generateRules() { ...@@ -76,11 +77,16 @@ function generateRules() {
// If the remote server is on localhost, we don't need to intercept // If the remote server is on localhost, we don't need to intercept
// The OAuth2 callback can go directly to localhost without redirection // The OAuth2 callback can go directly to localhost without redirection
if (isRemoteLocal) { // EXCEPTION: If we have an ongoing OAuth flow initiated from AISBF (forceInterception flag)
if (isRemoteLocal && !config.forceInterception) {
console.log('[AISBF] Remote server is localhost - no interception needed'); console.log('[AISBF] Remote server is localhost - no interception needed');
return rules; return rules;
} }
if (isRemoteLocal && config.forceInterception) {
console.log('[AISBF] Remote server is localhost but force interception is enabled for active OAuth flow');
}
for (const port of config.ports) { for (const port of config.ports) {
for (const path of config.paths) { for (const path of config.paths) {
// Rule for 127.0.0.1 // Rule for 127.0.0.1
...@@ -217,7 +223,8 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => ...@@ -217,7 +223,8 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) =>
enabled: true, enabled: true,
remoteServer: message.remoteServer || sender.url.replace(/\/dashboard.*$/, ''), remoteServer: message.remoteServer || sender.url.replace(/\/dashboard.*$/, ''),
ports: message.ports || config.ports, ports: message.ports || config.ports,
paths: message.paths || config.paths paths: message.paths || config.paths,
forceInterception: message.forceInterception || false
}; };
saveConfig(newConfig).then(success => { saveConfig(newConfig).then(success => {
sendResponse({ success, config: newConfig }); sendResponse({ success, config: newConfig });
......
...@@ -48,6 +48,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -48,6 +48,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<option value="ollama">Ollama</option> <option value="ollama">Ollama</option>
<option value="kiro">Kiro (Amazon Q Developer)</option> <option value="kiro">Kiro (Amazon Q Developer)</option>
<option value="claude">Claude (OAuth2)</option> <option value="claude">Claude (OAuth2)</option>
<option value="kilocode">Kilocode (OAuth2)</option>
</select> </select>
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Select the type of provider to configure appropriate settings</small> <small style="color: #a0a0a0; display: block; margin-top: 5px;">Select the type of provider to configure appropriate settings</small>
</div> </div>
...@@ -125,6 +126,7 @@ function renderProviderDetails(key) { ...@@ -125,6 +126,7 @@ function renderProviderDetails(key) {
const provider = providersData[key]; const provider = providersData[key];
const isKiroProvider = provider.type === 'kiro'; const isKiroProvider = provider.type === 'kiro';
const isClaudeProvider = provider.type === 'claude'; const isClaudeProvider = provider.type === 'claude';
const isKiloProvider = provider.type === 'kilocode';
// Initialize kiro_config if this is a kiro provider and doesn't have it // Initialize kiro_config if this is a kiro provider and doesn't have it
if (isKiroProvider && !provider.kiro_config) { if (isKiroProvider && !provider.kiro_config) {
...@@ -146,8 +148,17 @@ function renderProviderDetails(key) { ...@@ -146,8 +148,17 @@ function renderProviderDetails(key) {
}; };
} }
// Initialize kilo_config if this is a kilocode provider and doesn't have it
if (isKiloProvider && !provider.kilo_config) {
provider.kilo_config = {
credentials_file: '~/.kilo_credentials.json',
api_base: 'https://kilocode.ai/api/openrouter'
};
}
const kiroConfig = provider.kiro_config || {}; const kiroConfig = provider.kiro_config || {};
const claudeConfig = provider.claude_config || {}; const claudeConfig = provider.claude_config || {};
const kiloConfig = provider.kilo_config || {};
// Build authentication fields based on provider type // Build authentication fields based on provider type
let authFieldsHtml = ''; let authFieldsHtml = '';
...@@ -219,6 +230,61 @@ function renderProviderDetails(key) { ...@@ -219,6 +230,61 @@ function renderProviderDetails(key) {
<div id="kiro-upload-status-${key}" style="margin-top: 10px;"></div> <div id="kiro-upload-status-${key}" style="margin-top: 10px;"></div>
</div> </div>
`; `;
} else if (isKiloProvider) {
// Kilocode authentication fields - supports both API key and OAuth2
authFieldsHtml = `
<div style="background: #0f2840; padding: 15px; border-radius: 5px; margin-bottom: 15px; border-left: 3px solid #4a9eff;">
<h4 style="margin: 0 0 15px 0; color: #4a9eff;">Kilocode Authentication</h4>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant.
</small>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 1: API Key / Auth Token</h5>
<div class="form-group">
<label>API Key</label>
<input type="password" value="${provider.api_key || ''}" onchange="updateProvider('${key}', 'api_key', this.value)" placeholder="Enter your Kilocode API key">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">If provided, API key authentication will be used instead of OAuth2</small>
</div>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Option 2: OAuth2 Authentication</h5>
<small style="color: #a0a0a0; display: block; margin-bottom: 15px;">
If no API key is provided, OAuth2 will be used automatically.
</small>
<div class="form-group">
<label>API Base URL</label>
<input type="text" value="${kiloConfig.api_base || 'https://kilocode.ai/api/openrouter'}" readonly style="background: #0f2840; cursor: not-allowed;" placeholder="https://kilocode.ai/api/openrouter">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Kilocode API base URL (fixed)</small>
</div>
<div class="form-group">
<label>Credentials File Path</label>
<input type="text" value="${kiloConfig.credentials_file || '~/.kilo_credentials.json'}" onchange="updateKiloConfig('${key}', 'credentials_file', this.value)" placeholder="~/.kilo_credentials.json">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Path where OAuth2 credentials will be stored</small>
</div>
<div style="margin-top: 15px;">
<button type="button" class="btn" onclick="authenticateKilo('${key}')" style="background: #4a9eff;">
🔐 Authenticate with Kilocode OAuth2
</button>
<button type="button" class="btn btn-secondary" onclick="checkKiloAuth('${key}')" style="margin-left: 10px;">
Check Status
</button>
</div>
<div id="kilo-auth-status-${key}" style="margin-top: 10px; padding: 10px; border-radius: 3px; display: none;">
<!-- Auth status will be displayed here -->
</div>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Or Upload OAuth2 Credentials File</h5>
<div class="form-group">
<label>Upload Credentials File</label>
<input type="file" id="kilo-creds-file-${key}" accept=".json" onchange="uploadKiloFile('${key}', this.files[0])">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Upload Kilocode OAuth2 credentials JSON file</small>
</div>
<div id="kilo-upload-status-${key}" style="margin-top: 10px;"></div>
</div>
`;
} else if (isClaudeProvider) { } else if (isClaudeProvider) {
// Claude OAuth2 authentication fields // Claude OAuth2 authentication fields
authFieldsHtml = ` authFieldsHtml = `
...@@ -286,8 +352,9 @@ function renderProviderDetails(key) { ...@@ -286,8 +352,9 @@ function renderProviderDetails(key) {
<div class="form-group"> <div class="form-group">
<label>Endpoint</label> <label>Endpoint</label>
<input type="text" value="${provider.endpoint}" onchange="updateProvider('${key}', 'endpoint', this.value)" required> <input type="text" value="${provider.endpoint}" onchange="updateProvider('${key}', 'endpoint', this.value)" ${provider.type === 'kilocode' ? 'readonly style="background: #0f2840; cursor: not-allowed;"' : ''} required>
${isKiroProvider ? '<small style="color: #a0a0a0; display: block; margin-top: 5px;">Typically: https://q.us-east-1.amazonaws.com</small>' : ''} ${isKiroProvider ? '<small style="color: #a0a0a0; display: block; margin-top: 5px;">Typically: https://q.us-east-1.amazonaws.com</small>' : ''}
${provider.type === 'kilocode' ? '<small style="color: #a0a0a0; display: block; margin-top: 5px;">Fixed endpoint for Kilocode provider</small>' : ''}
</div> </div>
<div class="form-group"> <div class="form-group">
...@@ -299,6 +366,7 @@ function renderProviderDetails(key) { ...@@ -299,6 +366,7 @@ function renderProviderDetails(key) {
<option value="ollama" ${provider.type === 'ollama' ? 'selected' : ''}>Ollama</option> <option value="ollama" ${provider.type === 'ollama' ? 'selected' : ''}>Ollama</option>
<option value="kiro" ${provider.type === 'kiro' ? 'selected' : ''}>Kiro (Amazon Q Developer)</option> <option value="kiro" ${provider.type === 'kiro' ? 'selected' : ''}>Kiro (Amazon Q Developer)</option>
<option value="claude" ${provider.type === 'claude' ? 'selected' : ''}>Claude (OAuth2)</option> <option value="claude" ${provider.type === 'claude' ? 'selected' : ''}>Claude (OAuth2)</option>
<option value="kilocode" ${provider.type === 'kilocode' ? 'selected' : ''}>Kilocode (OAuth2)</option>
</select> </select>
</div> </div>
...@@ -522,7 +590,8 @@ function updateNewProviderDefaults() { ...@@ -522,7 +590,8 @@ function updateNewProviderDefaults() {
'anthropic': 'Anthropic provider (Claude). Uses API key authentication. Endpoint: https://api.anthropic.com/v1', 'anthropic': 'Anthropic provider (Claude). Uses API key authentication. Endpoint: https://api.anthropic.com/v1',
'ollama': 'Ollama local provider. No API key required by default. Endpoint: http://localhost:11434/api', 'ollama': 'Ollama local provider. No API key required by default. Endpoint: http://localhost:11434/api',
'kiro': 'Kiro (Amazon Q Developer) provider. Uses Kiro credentials (IDE, CLI, or direct tokens). Endpoint: https://q.us-east-1.amazonaws.com', 'kiro': 'Kiro (Amazon Q Developer) provider. Uses Kiro credentials (IDE, CLI, or direct tokens). Endpoint: https://q.us-east-1.amazonaws.com',
'claude': 'Claude Code provider. Uses OAuth2 authentication (browser-based login). Endpoint: https://api.anthropic.com/v1' 'claude': 'Claude Code provider. Uses OAuth2 authentication (browser-based login). Endpoint: https://api.anthropic.com/v1',
'kilocode': 'Kilocode provider. Uses OAuth2 Device Authorization Grant. Endpoint: https://kilocode.ai/api/openrouter'
}; };
descriptionEl.textContent = descriptions[providerType] || 'Standard provider configuration.'; descriptionEl.textContent = descriptions[providerType] || 'Standard provider configuration.';
...@@ -558,7 +627,7 @@ function confirmAddProvider() { ...@@ -558,7 +627,7 @@ function confirmAddProvider() {
name: key, name: key,
endpoint: '', endpoint: '',
type: providerType, type: providerType,
api_key_required: providerType !== 'kiro' && providerType !== 'ollama' && providerType !== 'claude', api_key_required: providerType !== 'kiro' && providerType !== 'ollama' && providerType !== 'claude' && providerType !== 'kilocode',
rate_limit: 0, rate_limit: 0,
default_rate_limit: 0, default_rate_limit: 0,
models: [] models: []
...@@ -583,6 +652,13 @@ function confirmAddProvider() { ...@@ -583,6 +652,13 @@ function confirmAddProvider() {
newProvider.claude_config = { newProvider.claude_config = {
credentials_file: '~/.claude_credentials.json' credentials_file: '~/.claude_credentials.json'
}; };
} else if (providerType === 'kilocode') {
newProvider.endpoint = 'https://kilocode.ai/api/openrouter';
newProvider.name = key + ' (Kilocode OAuth2)';
newProvider.kilo_config = {
credentials_file: '~/.kilo_credentials.json',
api_base: 'https://kilocode.ai/api/openrouter'
};
} else if (providerType === 'openai') { } else if (providerType === 'openai') {
newProvider.endpoint = 'https://api.openai.com/v1'; newProvider.endpoint = 'https://api.openai.com/v1';
newProvider.api_key = ''; newProvider.api_key = '';
...@@ -674,6 +750,7 @@ function updateProviderType(key, newType) { ...@@ -674,6 +750,7 @@ function updateProviderType(key, newType) {
client_secret: '' client_secret: ''
}; };
delete providersData[key].claude_config; delete providersData[key].claude_config;
delete providersData[key].kilo_config;
// Set default endpoint for kiro // Set default endpoint for kiro
if (!providersData[key].endpoint || providersData[key].endpoint === '') { if (!providersData[key].endpoint || providersData[key].endpoint === '') {
providersData[key].endpoint = 'https://q.us-east-1.amazonaws.com'; providersData[key].endpoint = 'https://q.us-east-1.amazonaws.com';
...@@ -685,15 +762,28 @@ function updateProviderType(key, newType) { ...@@ -685,15 +762,28 @@ function updateProviderType(key, newType) {
credentials_file: '~/.claude_credentials.json' credentials_file: '~/.claude_credentials.json'
}; };
delete providersData[key].kiro_config; delete providersData[key].kiro_config;
delete providersData[key].kilo_config;
// Set default endpoint for claude // Set default endpoint for claude
if (!providersData[key].endpoint || providersData[key].endpoint === '') { if (!providersData[key].endpoint || providersData[key].endpoint === '') {
providersData[key].endpoint = 'https://api.anthropic.com/v1'; providersData[key].endpoint = 'https://api.anthropic.com/v1';
} }
} else if (newType !== 'kiro' && newType !== 'claude' && (oldType === 'kiro' || oldType === 'claude')) { } else if (newType === 'kilocode' && oldType !== 'kilocode') {
// Transitioning FROM kiro/claude: remove special configs, set api_key_required to true // Transitioning TO kilocode: initialize kilo_config, set api_key_required to false
providersData[key].api_key_required = false;
providersData[key].kilo_config = {
credentials_file: '~/.kilo_credentials.json',
api_base: 'https://kilocode.ai/api/openrouter'
};
delete providersData[key].kiro_config;
delete providersData[key].claude_config;
// Set endpoint for kilocode (fixed, not modifiable)
providersData[key].endpoint = 'https://kilocode.ai/api/openrouter';
} else if (newType !== 'kiro' && newType !== 'claude' && newType !== 'kilocode' && (oldType === 'kiro' || oldType === 'claude' || oldType === 'kilocode')) {
// Transitioning FROM kiro/claude/kilocode: remove special configs, set api_key_required to true
providersData[key].api_key_required = true; providersData[key].api_key_required = true;
delete providersData[key].kiro_config; delete providersData[key].kiro_config;
delete providersData[key].claude_config; delete providersData[key].claude_config;
delete providersData[key].kilo_config;
} }
// Re-render to show appropriate fields // Re-render to show appropriate fields
...@@ -714,6 +804,193 @@ function updateClaudeConfig(key, field, value) { ...@@ -714,6 +804,193 @@ function updateClaudeConfig(key, field, value) {
providersData[key].claude_config[field] = value; providersData[key].claude_config[field] = value;
} }
function updateKiloConfig(key, field, value) {
if (!providersData[key].kilo_config) {
providersData[key].kilo_config = {};
}
providersData[key].kilo_config[field] = value;
}
async function authenticateKilo(key) {
const statusEl = document.getElementById(`kilo-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting OAuth2 Device Authorization flow...</p>';
try {
const response = await fetch('/dashboard/kilo/auth/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].kilo_config?.credentials_file || '~/.kilo_credentials.json',
api_base: providersData[key].kilo_config?.api_base || 'https://api.kilo.ai'
})
});
const data = await response.json();
if (!data.success) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Failed to start authentication: ${data.error || 'Unknown error'}</p>`;
return;
}
// Display the verification URL and code to the user
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = `
<div style="margin: 0;">
<p style="margin: 0 0 10px 0; color: #4a9eff; font-weight: bold;">🔐 Authorization Required</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Please visit: <a href="${data.verification_uri}" target="_blank" style="color: #4eff9e; text-decoration: underline;">${data.verification_uri}</a>
</p>
<p style="margin: 0 0 10px 0; color: #e0e0e0;">
Enter code: <strong style="color: #4eff9e; font-size: 18px; letter-spacing: 2px;">${data.user_code}</strong>
</p>
<p style="margin: 0; color: #a0a0a0; font-size: 13px;">
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
</p>
</div>
`;
// Open the verification URL in a new window
try {
window.open(data.verification_uri, 'kilo-auth', 'width=600,height=700');
} catch (e) {
console.error('Could not open auth window:', e);
}
// Poll for authorization completion
let pollCount = 0;
const maxPolls = Math.floor(data.expires_in / data.interval);
const pollInterval = setInterval(async () => {
pollCount++;
try {
const pollResponse = await fetch('/dashboard/kilo/auth/poll', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const pollData = await pollResponse.json();
if (pollData.status === 'approved') {
clearInterval(pollInterval);
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
statusEl.innerHTML = '<p style="margin: 0; color: #4eff9e;">✓ Authentication successful! Credentials saved.</p>';
} else if (pollData.status === 'denied') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization denied by user.</p>';
} else if (pollData.status === 'expired') {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authorization code expired. Please try again.</p>';
} else if (pollData.status === 'slow_down') {
// Server asked us to slow down, but we'll continue at the same rate
console.log('Server requested slow down');
}
// status === 'pending', continue polling
} catch (error) {
console.error('Error polling Kilo auth:', error);
}
// Timeout after max polls
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Authentication timeout. Please try again.</p>';
}
}, data.interval * 1000); // Convert seconds to milliseconds
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function checkKiloAuth(key) {
const statusEl = document.getElementById(`kilo-auth-status-${key}`);
statusEl.style.display = 'block';
statusEl.style.background = '#0f2840';
statusEl.style.border = '1px solid #4a9eff';
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Checking authentication status...</p>';
try {
const response = await fetch('/dashboard/kilo/auth/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider_key: key,
credentials_file: providersData[key].kilo_config?.credentials_file || '~/.kilo_credentials.json'
})
});
const data = await response.json();
if (data.authenticated) {
statusEl.style.background = '#0f4020';
statusEl.style.border = '1px solid #4eff9e';
const expiresIn = data.expires_in ? ` (expires in ${Math.floor(data.expires_in / (24 * 60 * 60))} days)` : '';
statusEl.innerHTML = `<p style="margin: 0; color: #4eff9e;">✓ Authenticated${expiresIn}</p>`;
} else {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = '<p style="margin: 0; color: #ff4a4a;">✗ Not authenticated. Click "Authenticate with Kilo" to log in.</p>';
}
} catch (error) {
statusEl.style.background = '#402010';
statusEl.style.border = '1px solid #ff4a4a';
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
async function uploadKiloFile(providerKey, file) {
if (!file) return;
const statusEl = document.getElementById(`kilo-upload-status-${providerKey}`);
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Uploading file...</p>';
const formData = new FormData();
formData.append('file', file);
formData.append('provider_key', providerKey);
formData.append('file_type', 'credentials');
try {
const response = await fetch('/dashboard/providers/upload-auth-file', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
statusEl.innerHTML = `<p style="margin: 0; color: #4eff9e;">✓ File uploaded successfully! Path: ${data.file_path}</p>`;
// Update the config with the new file path
updateKiloConfig(providerKey, 'credentials_file', data.file_path);
} else {
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Upload failed: ${data.error}</p>`;
}
} catch (error) {
statusEl.innerHTML = `<p style="margin: 0; color: #ff4a4a;">✗ Error: ${error.message}</p>`;
}
}
// Extension detection and configuration // Extension detection and configuration
let extensionInstalled = false; let extensionInstalled = false;
let extensionCheckComplete = false; let extensionCheckComplete = false;
...@@ -1015,6 +1292,9 @@ function showExtensionInstructions() { ...@@ -1015,6 +1292,9 @@ function showExtensionInstructions() {
<p style="margin: 0; color: #4eff9e; font-weight: bold;">ℹ️ About This Extension</p> <p style="margin: 0; color: #4eff9e; font-weight: bold;">ℹ️ About This Extension</p>
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 14px;"> <p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 14px;">
This extension intercepts OAuth2 callbacks from localhost:54545 and redirects them to your AISBF server, enabling remote authentication. This extension intercepts OAuth2 callbacks from localhost:54545 and redirects them to your AISBF server, enabling remote authentication.
<p style="margin: 5px 0 0 0; color: #a0a0a0; font-size: 14px;">
<strong>Proxy Support:</strong> The extension automatically detects reverse proxy deployments (nginx, caddy, etc.) via X-Forwarded-* headers and activates when OAuth flows are initiated from the dashboard.
</p>
</p> </p>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment