-
-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Description
Create a command-line tool that helps developers test OAuth flows interactively. This tool will make it easy to test different OAuth providers, debug issues, and understand how the library works.
What needs to be done
-
Create CLI interface (
src/cli.ts):- Accept provider configuration via flags
- Support common OAuth providers with presets
- Display received tokens (safely)
- Save tokens to file
- Support custom provider configuration
-
Add binary entry to package.json:
"bin": { "oauth-test": "./dist/cli.js" }
-
Features to implement:
- Interactive provider selection
- Token exchange simulation
- PKCE support testing
- Error scenario testing
- Token refresh testing
Why this matters
A CLI tool provides:
- Quick way to test OAuth integrations
- Learning tool for developers new to OAuth
- Debugging aid for production issues
- Provider compatibility testing
- Token management utility
Without a CLI tool:
- Developers must write custom code to test
- Harder to debug OAuth issues
- No standard way to test the library
Implementation considerations
-
Security: How do we display tokens safely? Should we mask them by default?
-
Provider registry: Should we maintain a registry of common providers or let users define everything?
-
Alternative approach: Instead of a CLI, create a web-based testing tool with a nice UI
-
Token storage: Where should we save tokens? Should we integrate with system keychains?
-
Interactive vs scriptable: Should the CLI be interactive (prompts) or fully scriptable (flags only)?
Suggested implementation
#!/usr/bin/env node
// src/cli.ts
import { parseArgs } from "node:util";
import { getAuthCode, OAuthError } from "./index";
import { fileStore } from "./storage/file";
import * as readline from "node:readline/promises";
// Provider presets
const PROVIDERS = {
github: {
name: "GitHub",
authUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
scopes: ["user", "repo", "gist"]
},
google: {
name: "Google",
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
scopes: ["openid", "email", "profile"]
},
microsoft: {
name: "Microsoft",
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
scopes: ["User.Read", "Mail.Read"]
}
};
interface CLIOptions {
provider?: string;
clientId?: string;
clientSecret?: string;
authUrl?: string;
tokenUrl?: string;
scopes?: string[];
port?: number;
save?: boolean;
interactive?: boolean;
pkce?: boolean;
showTokens?: boolean;
}
class OAuthCLI {
private rl?: readline.Interface;
async run(options: CLIOptions) {
console.log("🔐 OAuth Callback Test CLI\\n");
// Interactive mode
if (options.interactive) {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
options = await this.promptForOptions(options);
this.rl.close();
}
// Validate required options
if (!options.clientId) {
console.error("❌ Error: --client-id is required");
process.exit(1);
}
// Build authorization URL
const authUrl = this.buildAuthUrl(options);
console.log("\\n📋 Configuration:");
console.log(` Provider: ${options.provider || 'custom'}`);
console.log(` Client ID: ${options.clientId}`);
console.log(` Scopes: ${options.scopes?.join(', ') || 'none'}`);
console.log(` PKCE: ${options.pkce ? 'enabled' : 'disabled'}`);
console.log(` Callback Port: ${options.port || 3000}`);
console.log("\\n🌐 Opening browser for authorization...");
console.log(` URL: ${authUrl}\\n`);
try {
// Start OAuth flow
const startTime = Date.now();
const result = await getAuthCode({
authorizationUrl: authUrl,
port: options.port || 3000,
validateState: true,
pkce: options.pkce
});
const duration = Date.now() - startTime;
console.log(`\\n✅ Authorization successful! (${duration}ms)`);
console.log(`\\n📦 Callback parameters:`);
for (const [key, value] of Object.entries(result)) {
if (key === 'code' && !options.showTokens) {
console.log(` ${key}: ${value.substring(0, 8)}...`);
} else {
console.log(` ${key}: ${value}`);
}
}
// Exchange code for tokens (if token URL provided)
if (options.tokenUrl && options.clientSecret) {
console.log("\\n🔄 Exchanging authorization code for tokens...");
const tokens = await this.exchangeCode(
result.code!,
options,
result.code_verifier // From PKCE
);
console.log("\\n🎉 Token exchange successful!");
if (options.showTokens) {
console.log("\\n🔑 Received tokens:");
console.log(JSON.stringify(tokens, null, 2));
} else {
console.log("\\n🔑 Received tokens: [use --show-tokens to display]");
}
// Save tokens if requested
if (options.save) {
const store = fileStore();
await store.set(options.provider || 'custom', tokens);
console.log("\\n💾 Tokens saved to ~/.mcp/tokens.json");
}
}
} catch (error) {
if (error instanceof OAuthError) {
console.error(`\\n❌ OAuth Error: ${error.error}`);
if (error.error_description) {
console.error(` Description: ${error.error_description}`);
}
if (error.error_uri) {
console.error(` More info: ${error.error_uri}`);
}
} else {
console.error(`\\n❌ Error: ${error.message}`);
}
process.exit(1);
}
}
private async promptForOptions(options: CLIOptions): Promise<CLIOptions> {
// Interactive prompts for missing options
if (!options.provider) {
console.log("Select a provider:");
Object.entries(PROVIDERS).forEach(([key, provider], index) => {
console.log(` ${index + 1}. ${provider.name}`);
});
console.log(` ${Object.keys(PROVIDERS).length + 1}. Custom`);
const choice = await this.rl!.question("\\nChoice: ");
// ... handle choice
}
if (!options.clientId) {
options.clientId = await this.rl!.question("Client ID: ");
}
// ... more prompts
return options;
}
private buildAuthUrl(options: CLIOptions): string {
const provider = options.provider ? PROVIDERS[options.provider] : null;
const baseUrl = options.authUrl || provider?.authUrl;
if (!baseUrl) {
throw new Error("Authorization URL is required");
}
const url = new URL(baseUrl);
url.searchParams.set("client_id", options.clientId!);
url.searchParams.set("redirect_uri", `http://localhost:${options.port || 3000}/callback`);
url.searchParams.set("response_type", "code");
if (options.scopes && options.scopes.length > 0) {
url.searchParams.set("scope", options.scopes.join(" "));
}
return url.toString();
}
private async exchangeCode(
code: string,
options: CLIOptions,
codeVerifier?: string
): Promise<any> {
// Token exchange implementation
const response = await fetch(options.tokenUrl!, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: options.clientId!,
client_secret: options.clientSecret!,
redirect_uri: `http://localhost:${options.port || 3000}/callback`,
...(codeVerifier && { code_verifier: codeVerifier })
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return response.json();
}
}
// Parse CLI arguments
const { values } = parseArgs({
options: {
provider: { type: 'string', short: 'p' },
'client-id': { type: 'string', short: 'c' },
'client-secret': { type: 'string', short: 's' },
'auth-url': { type: 'string' },
'token-url': { type: 'string' },
scopes: { type: 'string', multiple: true },
port: { type: 'string' },
save: { type: 'boolean' },
interactive: { type: 'boolean', short: 'i' },
pkce: { type: 'boolean' },
'show-tokens': { type: 'boolean' },
help: { type: 'boolean', short: 'h' }
}
});
// Show help
if (values.help) {
console.log(`
Usage: oauth-test [options]
Options:
-p, --provider <name> Use preset provider (github, google, microsoft)
-c, --client-id <id> OAuth client ID (required)
-s, --client-secret <sec> OAuth client secret (for token exchange)
--auth-url <url> Authorization endpoint URL
--token-url <url> Token endpoint URL
--scopes <scope> OAuth scopes (can be used multiple times)
--port <port> Callback server port (default: 3000)
--save Save tokens to file
-i, --interactive Interactive mode with prompts
--pkce Enable PKCE
--show-tokens Display full tokens (security warning!)
-h, --help Show this help message
Examples:
# Test GitHub OAuth
oauth-test -p github -c YOUR_CLIENT_ID --scopes user repo
# Interactive mode
oauth-test -i
# Custom provider with token exchange
oauth-test --auth-url https://oauth.example.com/authorize \\
--token-url https://oauth.example.com/token \\
-c CLIENT_ID -s CLIENT_SECRET \\
--pkce --save
`);
process.exit(0);
}
// Run CLI
const cli = new OAuthCLI();
cli.run(values as CLIOptions).catch(console.error);Testing the CLI
# Install globally for testing
npm link
# Test with GitHub
oauth-test -p github -c YOUR_CLIENT_ID
# Test PKCE
oauth-test -p google -c YOUR_CLIENT_ID --pkce
# Interactive mode
oauth-test -i
# Test error handling
oauth-test -c invalid_client_idSkills required
- TypeScript
- Node.js CLI development
- OAuth 2.0 protocol
- Command-line UX design
- Security best practices
Difficulty
Medium - Requires good understanding of CLI design and OAuth flows