kriasoft / kriasoft/oauth-callback
Create CLI tool for testing OAuth flows
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
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
1. **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
2. **Add binary entry to package.json**:
```json
"bin": {
"oauth-test": "./dist/cli.js"
}
```
3. **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
**⚠️ Note:** This feature requires critical thinking during implementation. Consider:
1. **Security**: How do we display tokens safely? Should we mask them by default?
2. **Provider registry**: Should we maintain a registry of common providers or let users define everything?
3. **Alternative approach**: Instead of a CLI, create a web-based testing tool with a nice UI
4. **Token storage**: Where should we save tokens? Should we integrate with system keychains?
5. **Interactive vs scriptable**: Should the CLI be interactive (prompts) or fully scriptable (flags only)?
## Suggested implementation
```typescript
#!/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 {
// 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 {
// 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 Use preset provider (github, google, microsoft)
-c, --client-id OAuth client ID (required)
-s, --client-secret OAuth client secret (for token exchange)
--auth-url Authorization endpoint URL
--token-url Token endpoint URL
--scopes OAuth scopes (can be used multiple times)
--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
```bash
# 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_id
```
## Skills 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
Contributor guide
Research direction
Start with src/cli.ts, package.json, src/index, and src/storage/file; read the existing OAuth callback and file-store entry points before defining the CLI boundaries. Done means the requested provider, interactive, PKCE, token, error-handling, and registration features are implemented and exercised with the npm link commands listed in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- authentication, authorization, cli, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100