Commit 07bb5e5
Add OAuth 2.0 Device Flow CLI authentication (#192)
* Add OAuth 2.0 Device Flow CLI authentication
- Implement 'openhands login' command with OAuth 2.0 Device Flow
- Add secure encrypted token storage for API keys
- Implement 'openhands logout' command for credential management
- Add user settings storage for future synchronization
- Integrate authentication commands into main CLI parser
- Add required dependencies: httpx, cryptography, python-dotenv
This enables CLI users to authenticate with OpenHands Cloud and receive
API keys for accessing cloud features.
Co-authored-by: openhands <openhands@all-hands.dev>
* Simplify OAuth 2.0 Device Flow client implementation
- Remove client_id and scope requirements from device authorization requests
- Simplify DeviceFlowClient constructor to only require server_url
- Replace complex encrypted token storage with simple plain text API key storage
- Store API key directly in PERSISTENCE_DIR/api_key.txt for simplicity
- Maintain backward compatibility with legacy token storage methods
- Match simplified server-side implementation that no longer requires client_id/scope
Co-authored-by: openhands <openhands@all-hands.dev>
* Add configurable cloud URL support via OPENHANDS_CLOUD_URL environment variable
- Add OPENHANDS_CLOUD_URL environment variable support for login command
- Default remains https://app.all-hands.dev if environment variable is not set
- Environment variable can be overridden by --server-url command line argument
- Update help text to document the environment variable option
Co-authored-by: openhands <openhands@all-hands.dev>
* Remove grant_type from device token requests
Simplify the OAuth Device Flow client by removing the unnecessary
grant_type field from token polling requests. The server no longer
requires or validates this field.
Co-authored-by: openhands <openhands@all-hands.dev>
* Update uv.lock
* Implement LLM API key and settings fetch after OAuth completion
- Add OpenHandsApiClient class to fetch LLM API key and user settings
- Enhance login flow to call /api/keys/llm/byor and /api/settings endpoints
- Create Agent object with OpenHands provider and fetched configuration
- Add LLM summarizing condenser by default
- Save Agent configuration using AgentStore
- Add comprehensive error handling with user-friendly messages
- Create utility functions for fetching user data
Co-authored-by: openhands <openhands@all-hands.dev>
* Improve login flow: pull settings from remote when already logged in
Instead of exiting early and asking users to logout first when they are
already logged in, the login command now:
1. Detects existing authentication tokens
2. Uses existing tokens to pull latest settings from remote
3. Synchronizes LLM API key and user settings locally
4. Provides appropriate feedback to the user
This provides a better user experience by allowing users to refresh their
local settings without having to logout and login again.
Also fixes a bug in UserSettings._get_encryption_key() that was trying to
call a non-existent method on TokenStorage.
Co-authored-by: openhands <openhands@all-hands.dev>
* Remove encryption from settings and token storage
- Remove all encryption code from UserSettings class
- Store user settings as plain JSON files instead of encrypted data
- Remove cryptography dependency from pyproject.toml
- Update uv.lock to reflect dependency changes
- Token storage already used plain text files
This simplifies the codebase and removes the complexity of encryption
for local settings storage. Settings files are still protected by
file system permissions (600).
Co-authored-by: openhands <openhands@all-hands.dev>
* Remove custom UserSettings class - replaced with Agent object and AgentStore.save
The login command now uses Agent object with values from BYOR endpoint (LLM key)
and /settings endpoint (model value), then saves using AgentStore.save().
This removes the custom settings class in favor of the standard Agent approach.
Co-authored-by: openhands <openhands@all-hands.dev>
* Add detailed logging when saving agent configuration
Shows what configurations were saved (model, provider, tools count, condenser settings)
and where they were saved, while keeping the LLM API key secure.
Co-authored-by: openhands <openhands@all-hands.dev>
* Create separate LLM objects for agent and condenser
Ensures different usage tracking by creating separate LLM instances for the main
agent and the condenser, allowing proper differentiation in usage analytics.
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix LLM configuration: remove custom provider and add usage_id
- Remove custom_llm_provider='openhands' since openhands is a basic provider
- Add usage_id='agent' for main LLM and usage_id='condenser' for condenser LLM
- Update logging to show usage_id instead of provider
Co-authored-by: openhands <openhands@all-hands.dev>
* Add base_url to LLM configurations
Set base_url to 'https://llm-proxy.app.all-hands.dev/' for both agent and
condenser LLMs to route requests through OpenHands LLM proxy.
Also added base_url to configuration logging output.
Co-authored-by: openhands <openhands@all-hands.dev>
* rm lib
* Refactor auth parsers into separate file
- Move login and logout parser definitions to auth_parser.py
- Update main_parser.py to use helper functions for auth parsers
- Fix server parser to use helper function instead of inline definition
- Remove unused os import from main_parser.py
Co-authored-by: openhands <openhands@all-hands.dev>
* add back acp parser
* Delete __init__.py
* Simplify and deduplicate OAuth Device Flow implementation
- Create shared BaseHttpClient for centralized HTTP handling and error extraction
- Refactor DeviceFlowClient and OpenHandsApiClient to use BaseHttpClient
- Remove legacy token storage methods (store_tokens, get_tokens, etc.)
- Remove global get_token_storage() function, use direct TokenStorage() instantiation
- Update all callers to use simplified API key interface
- Net code reduction: 118 lines (216 deletions - 98 insertions)
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix formatting after merge
Co-authored-by: openhands <openhands@all-hands.dev>
* fix import
* cleanup client
* Delete user_data.py
* Update device_flow.py
* simplify
* Add comprehensive unit tests for OAuth device flow authentication
- Created tests/auth/ directory with 83 unit tests covering all auth modules
- test_token_storage.py: 15 tests for token storage, validation, and edge cases
- test_http_client.py: 16 tests for HTTP requests, error handling, timeouts
- test_device_flow.py: 20 tests for OAuth 2.0 Device Flow and token polling
- test_api_client.py: 14 tests for API client and user data fetching
- test_login_command.py: 11 tests for login flows and error scenarios
- test_logout_command.py: 7 tests for logout scenarios and cleanup
- All tests pass with comprehensive mocking and async/await support
- Covers critical paths, error handling, and security scenarios
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix linting issues in auth tests
- Remove unused mock_print variables
- Fix line length issues in comments
- Fix HTTPStatusError constructor in HTTP client test
- Add missing MagicMock import
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: enhance device flow with automatic browser opening and URL user_code parameter
- Update device flow URL to include user_code parameter: {server_url}/oauth/device/verify?user_code={USER_CODE}
- Add automatic browser opening using webbrowser module
- Remove manual code entry instructions from user flow
- Add graceful fallback when browser opening fails
- Update tests to cover new browser functionality
- Maintain backward compatibility and error handling
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix auth messaging and refactor polling
- Fix LLM API key display to show only 3 characters instead of 10
- Simplify login/logout messages to use 'OpenHands Cloud' instead of server URLs
- Add helpful error message for sync failures suggesting re-login
- Refactor poll_for_token from attempt-based to timeout-based polling (600s default)
- Create shared utils.py with _p function to deduplicate across auth files
- Update all tests to match new messaging
- All linting checks pass
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix formatting: add newline at end of utils.py
Co-authored-by: openhands <openhands@all-hands.dev>
* Replace exit(1) with sys.exit(1) in simple_main.py
- Add sys import to simple_main.py
- Replace exit(1) calls with sys.exit(1) for consistency
- All other files already use sys.exit(1) correctly
Co-authored-by: openhands <openhands@all-hands.dev>
* Refactor auth module to improve separation of concerns
- Move agent creation logic from auth module to AgentStore
- Add create_and_save_from_settings method to AgentStore
- Remove duplicate DEFAULT_MODEL and DEFAULT_LLM_BASE_URL constants
- Update default model to claude-sonnet-4-5-20250929
- Improve condenser handling with proper isinstance checks
- Simplify condenser creation to use class defaults
- Update tests to match refactored function signatures
Co-authored-by: openhands <openhands@all-hands.dev>
* Update login_command.py
* Fix OAuth device flow token endpoint to use form data
- Update HTTP client to support application/x-www-form-urlencoded data
- Change device flow client to send form data instead of JSON to /oauth/device/token
- Update tests to reflect the new form data usage
- Ensures RFC 8628 compliance with server endpoint changes
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix linting issues: shorten docstring lines
- Shortened form_data parameter descriptions to fit line length limit
- All linting checks now pass
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix code formatting in test file
- Apply automatic formatting to test_http_client.py
- Ensures consistent code style
Co-authored-by: openhands <openhands@all-hands.dev>
* Update token storage path to .openhands/cloud and add secure permissions
- Change TokenStorage to store API keys in ~/.openhands/cloud/ instead of ~/.openhands/
- Add chmod 600 permissions when storing API keys for better security
- Update tests to reflect new path structure
- Add test for file permissions verification
Co-authored-by: openhands <openhands@all-hands.dev>
* Add user consent before overriding existing agent settings
- Modified create_and_save_from_settings to check for existing configurations
- Added interactive prompt asking user permission before overwriting
- Shows comparison between current and new settings
- Added force_overwrite parameter to skip consent when needed
- Updated API client to handle user consent gracefully
- Preserves existing configuration when user declines
Fixes issue where OAuth login would silently overwrite manually configured settings
* Refactor consent logic: move UI interaction from AgentStore to API client
- Moved user consent logic from AgentStore.create_and_save_from_settings to API client
- AgentStore now only handles data persistence (load/save operations)
- API client handles user interaction and consent before calling AgentStore
- Added proper HTML escaping for model names and URLs in consent prompt
- Updated test to mock the new load() method call
- Maintains separation of concerns: data layer vs presentation layer
This addresses the architectural concern about UI logic being in the data store.
Co-authored-by: openhands <openhands@all-hands.dev>
* Fix line length linting issue
- Split long HTML string in consent prompt to comply with 88 character limit
Co-authored-by: openhands <openhands@all-hands.dev>
* use simple input
* Replace hardcoded colors with OPENHANDS_THEME in auth module
- Replace all hardcoded color tags (<green>, <red>, <yellow>, <cyan>, <white>) with OPENHANDS_THEME variables
- Fix missing f-string prefixes in 5 locations where {OPENHANDS_THEME.*} variables weren't being interpolated
- Simplify _p() function in auth/utils.py to only handle console printing
- Fix all line length violations (E501) by breaking long lines appropriately
- All auth modules now use rich library with theme-based colors for consistency
- All linting checks now pass (ruff format, ruff lint, pycodestyle, pyright)
Files modified:
- openhands_cli/auth/utils.py: Simplified _p() function
- openhands_cli/auth/device_flow.py: Theme colors + formatting fixes
- openhands_cli/auth/login_command.py: Theme colors + formatting fixes
- openhands_cli/auth/logout_command.py: Theme colors + formatting fixes
- openhands_cli/auth/api_client.py: Theme colors + formatting fixes
Color mapping:
- <green> → [{OPENHANDS_THEME.success}] (#ffe165)
- <red> → [{OPENHANDS_THEME.error}] (#ff6b6b)
- <yellow> → [{OPENHANDS_THEME.warning}] (#ffe165)
- <cyan> → [{OPENHANDS_THEME.accent}] (#277dff)
- <white> → [{OPENHANDS_THEME.secondary}] (#ffffff)
* fix imports
* update messaging
* Fix failing test in test_login_command.py
- Update test_fetch_user_data_with_context_new_login_success to check for the correct message
- The test was expecting 'cloud features' but the function actually prints 'synchronized successfully'
- The 'cloud features' message is printed in the main login_command function, not in _fetch_user_data_with_context
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>1 parent d801798 commit 07bb5e5
22 files changed
Lines changed: 2802 additions & 10 deletions
File tree
- openhands_cli
- argparsers
- auth
- tui/settings
- tests/auth
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
6 | 8 | | |
7 | 9 | | |
8 | 10 | | |
| |||
35 | 37 | | |
36 | 38 | | |
37 | 39 | | |
| 40 | + | |
| 41 | + | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
| |||
102 | 106 | | |
103 | 107 | | |
104 | 108 | | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
105 | 112 | | |
106 | 113 | | |
107 | 114 | | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | 115 | | |
118 | 116 | | |
119 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
120 | 122 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
0 commit comments