feat: add --no-browser OAuth mode for headless servers
## Feature Description
Enables adding Google accounts on remote servers without a desktop
environment (headless Linux, Docker containers, SSH sessions) where
automatic browser opening is not possible. Users can manually copy the
authorization URL to a device with a browser, complete authentication,
and paste the authorization code back.
## Usage
npm run accounts:add -- --no-browser
## Code Architecture
### New Modules
1. oauth.js - extractCodeFromInput()
- Parses user input (full callback URL or raw authorization code)
- Extracts code and state parameters
- Handles OAuth error responses
2. accounts.js - addAccountNoBrowser()
- Account addition flow for no-browser mode
- Displays authorization URL for manual copying
- Waits for user to paste authorization code
- Calls extractCodeFromInput to parse input
- Completes OAuth flow and saves account
3. tests/test-oauth-no-browser.cjs
- 13 unit tests covering valid URLs, raw codes, error handling, edge cases
### Modified Modules
1. accounts.js - interactiveAdd()
- Added noBrowser parameter
- Selects addAccount or addAccountNoBrowser based on mode
2. accounts.js - main()
- Parses --no-browser CLI argument
- Updated help information
## User Flow
┌─────────────────────────────────────────────────────────────┐
│ Headless Server Terminal │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ npm run accounts:add -- --no-browser │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Display Google OAuth URL │
│ (manual copy required) │
└─────────────────────────────────┘
│
┌────────────────────┴────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ Local Browser │ │ Phone/Other Device │
│ Open URL │ │ Open URL │
│ Google Sign-in │ │ Google Sign-in │
│ Authorize App │ │ Authorize App │
└─────────────────┘ └─────────────────────┘
│ │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Browser redirects to localhost │
│ (page won't load - this is OK) │
│ Copy full URL or code parameter │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Return to server terminal │
│ Paste URL or authorization code │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ ✓ Account added successfully │
└─────────────────────────────────┘
## Security Considerations
- Supports state parameter validation (when user pastes full URL)
- Warns on state mismatch but allows continuation (manual mode tolerance)
- Authorization code length validation to prevent incorrect input
## Compatibility
- Does not affect existing automatic browser OAuth flow
- All changes are additive, no modifications to upstream logic
- Easy to merge future upstream updates
This commit is contained in:
committed by
Badri Narayanan S
parent
d03c79cc39
commit
573ba57db6
@@ -57,6 +57,56 @@ export function getAuthorizationUrl() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract authorization code and state from user input.
|
||||
* User can paste either:
|
||||
* - Full callback URL: http://localhost:51121/oauth-callback?code=xxx&state=xxx
|
||||
* - Just the code parameter: 4/0xxx...
|
||||
*
|
||||
* @param {string} input - User input (URL or code)
|
||||
* @returns {{code: string, state: string|null}} Extracted code and optional state
|
||||
*/
|
||||
export function extractCodeFromInput(input) {
|
||||
if (!input || typeof input !== 'string') {
|
||||
throw new Error('No input provided');
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
|
||||
// Check if it looks like a URL
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const error = url.searchParams.get('error');
|
||||
|
||||
if (error) {
|
||||
throw new Error(`OAuth error: ${error}`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No authorization code found in URL');
|
||||
}
|
||||
|
||||
return { code, state };
|
||||
} catch (e) {
|
||||
if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error('Invalid URL format');
|
||||
}
|
||||
}
|
||||
|
||||
// Assume it's a raw code
|
||||
// Google auth codes typically start with "4/" and are long
|
||||
if (trimmed.length < 10) {
|
||||
throw new Error('Input is too short to be a valid authorization code');
|
||||
}
|
||||
|
||||
return { code: trimmed, state: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a local server to receive the OAuth callback
|
||||
* Returns a promise that resolves with the authorization code
|
||||
@@ -338,6 +388,7 @@ export async function completeOAuthFlow(code, verifier) {
|
||||
|
||||
export default {
|
||||
getAuthorizationUrl,
|
||||
extractCodeFromInput,
|
||||
startCallbackServer,
|
||||
exchangeCode,
|
||||
refreshAccessToken,
|
||||
|
||||
Reference in New Issue
Block a user