"""
LinkedIn MCP Authentication CLI.
Provides commands for:
- OAuth authentication (official API)
- Cookie extraction (unofficial API)
- Status checking
"""
import argparse
import sys
from datetime import datetime
from pathlib import Path
from linkedin_mcp.config.settings import get_settings
from linkedin_mcp.core.logging import configure_logging, get_logger
from linkedin_mcp.services.linkedin.official_client import LinkedInOfficialClient
from linkedin_mcp.services.storage.token_storage import (
CookieData,
TokenData,
delete_official_token,
delete_unofficial_cookies,
get_official_token,
get_unofficial_cookies,
store_official_token,
store_unofficial_cookies,
)
logger = get_logger(__name__)
def cmd_status(args: argparse.Namespace) -> int:
"""Check authentication status for both APIs."""
print("\n=== LinkedIn MCP Authentication Status ===\n")
# Check Official OAuth
print("π± Official API (OAuth 2.0):")
official_token = get_official_token()
if official_token:
if official_token.is_expired:
print(" β Token EXPIRED")
print(" Run: linkedin-mcp-auth oauth")
elif official_token.expires_soon:
print(f" β οΈ Token expires in {official_token.days_until_expiry} days")
print(" Consider re-authenticating soon")
else:
print(f" β
Authenticated ({official_token.days_until_expiry} days remaining)")
print(f" Scopes: {', '.join(official_token.scopes)}")
else:
print(" β Not authenticated")
print(" Run: linkedin-mcp-auth oauth")
print()
# Check Unofficial Cookies
print("πͺ Unofficial API (Cookies):")
cookies = get_unofficial_cookies()
if cookies:
if cookies.is_stale:
print(f" β οΈ Cookies may be stale ({cookies.hours_since_extraction}h old)")
print(" Run: linkedin-mcp-auth extract-cookies")
else:
print(f" β
Cookies loaded ({cookies.hours_since_extraction}h old)")
if cookies.browser:
print(f" Source: {cookies.browser}")
else:
print(" β No cookies stored")
print(" Run: linkedin-mcp-auth extract-cookies")
print()
return 0
def cmd_oauth(args: argparse.Namespace) -> int:
"""Start OAuth authentication flow."""
settings = get_settings()
print("\n=== LinkedIn OAuth Authentication ===\n")
# Check for client credentials
if not settings.linkedin.client_id or not settings.linkedin.client_secret:
print("β LinkedIn OAuth credentials not configured.")
print()
print("Please set the following environment variables:")
print(" LINKEDIN_CLIENT_ID=your_client_id")
print(" LINKEDIN_CLIENT_SECRET=your_client_secret")
print()
print("Get these from: https://www.linkedin.com/developers/apps")
return 1
# Check if already authenticated (and not forcing)
if not args.force:
existing_token = get_official_token()
if existing_token and not existing_token.is_expired:
print(f"β
Already authenticated ({existing_token.days_until_expiry} days remaining)")
print()
print("Use --force to re-authenticate anyway.")
return 0
# OAuth scopes for LinkedIn API
# w_member_social = posts only (Share on LinkedIn product)
# w_member_social_feed = posts, comments, reactions (Community Management API)
scopes = [
# Core OIDC scopes (required)
"openid",
"profile",
"email",
# Share on LinkedIn - create and manage posts
"w_member_social",
]
# Add Community Management scope if enabled
if args.community_management:
scopes.append("w_member_social_feed")
print("Requesting OAuth 2.0 Authorization with the following scopes:")
print()
print(" π SCOPES REQUESTED:")
print(" βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
print(" β’ openid - OpenID Connect authentication")
print(" β’ profile - Basic profile information (name, photo)")
print(" β’ email - Email address")
print(" β’ w_member_social - Create and manage posts")
if args.community_management:
print(" β’ w_member_social_feed - Comments and reactions (Community Mgmt)")
print(" βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
print()
print("A browser window will open for you to authorize these permissions.")
print()
# Initialize client
client = LinkedInOfficialClient(
client_id=settings.linkedin.client_id.get_secret_value()
if settings.linkedin.client_id
else "",
client_secret=settings.linkedin.client_secret.get_secret_value()
if settings.linkedin.client_secret
else "",
redirect_uri=settings.linkedin.redirect_uri,
scopes=scopes,
)
# Start authentication
# Pass force_consent=True when --force flag is used to show consent screen
if client.authenticate_interactive(timeout=args.timeout, force_consent=args.force):
# Store token
token_data = TokenData(
access_token=client._access_token,
expires_at=datetime.fromtimestamp(client._token_expires_at),
scopes=client.scopes,
)
store_official_token(token_data)
print()
print("β
Authentication successful!")
print(f" Token valid for {token_data.days_until_expiry} days")
print(" Token stored securely in system keychain")
# Test the token
user_info = client.get_user_info()
if user_info:
print()
print(f" Logged in as: {user_info.get('name', 'Unknown')}")
print(f" Email: {user_info.get('email', 'Unknown')}")
return 0
else:
print()
print("β Authentication failed or timed out.")
print(" Please try again.")
return 1
def cmd_extract_cookies(args: argparse.Namespace) -> int:
"""Extract cookies from browser."""
print("\n=== LinkedIn Cookie Extraction ===\n")
browser = args.browser.lower()
try:
import browser_cookie3
print(f"Extracting cookies from {browser.title()}...")
# Get cookies based on browser
if browser == "firefox":
cookiejar = browser_cookie3.firefox(domain_name=".linkedin.com")
elif browser == "chrome":
cookiejar = browser_cookie3.chrome(domain_name=".linkedin.com")
elif browser == "edge":
cookiejar = browser_cookie3.edge(domain_name=".linkedin.com")
elif browser == "opera":
cookiejar = browser_cookie3.opera(domain_name=".linkedin.com")
elif browser == "brave":
cookiejar = browser_cookie3.brave(domain_name=".linkedin.com")
else:
print(f"β Unsupported browser: {browser}")
print(" Supported: firefox, chrome, edge, opera, brave")
return 1
# Find the li_at cookie
li_at = None
jsessionid = None
for cookie in cookiejar:
if cookie.name == "li_at":
li_at = cookie.value
elif cookie.name == "JSESSIONID":
jsessionid = cookie.value
if not li_at:
print("β Could not find li_at cookie.")
print()
print("Make sure you are logged into LinkedIn in your browser:")
print(f" 1. Open {browser.title()}")
print(" 2. Go to https://www.linkedin.com")
print(" 3. Log in if not already logged in")
print(" 4. Run this command again")
return 1
# Store cookies
cookie_data = CookieData(
li_at=li_at,
jsessionid=jsessionid,
browser=browser,
)
store_unofficial_cookies(cookie_data)
print()
print("β
Cookies extracted successfully!")
print(f" Browser: {browser.title()}")
print(" Cookies stored securely in system keychain")
print()
print("Note: These cookies typically last 24-48 hours.")
print(" Run this command again if you experience auth errors.")
return 0
except Exception as e:
print(f"β Error extracting cookies: {e}")
print()
print("Common issues:")
print(" - Browser is running (try closing it)")
print(" - Profile path changed")
print(" - Permission issues")
return 1
def cmd_logout(args: argparse.Namespace) -> int:
"""Clear stored credentials."""
print("\n=== LinkedIn MCP Logout ===\n")
if args.all or args.oauth:
if delete_official_token():
print("β
Deleted official OAuth token")
else:
print("β οΈ No official token to delete")
if args.all or args.cookies:
if delete_unofficial_cookies():
print("β
Deleted unofficial cookies")
else:
print("β οΈ No cookies to delete")
print()
return 0
def main() -> int:
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
prog="linkedin-mcp-auth",
description="LinkedIn MCP Authentication CLI",
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Status command
status_parser = subparsers.add_parser("status", help="Check authentication status")
status_parser.set_defaults(func=cmd_status)
# OAuth command
oauth_parser = subparsers.add_parser("oauth", help="Authenticate via OAuth")
oauth_parser.add_argument(
"--force",
"-f",
action="store_true",
help="Force re-authentication even if already authenticated",
)
oauth_parser.add_argument(
"--timeout",
"-t",
type=int,
default=120,
help="Timeout in seconds for authentication (default: 120)",
)
oauth_parser.add_argument(
"--community-management",
"-c",
action="store_true",
help="Include Community Management API scope (w_member_social_feed) for comments/reactions",
)
oauth_parser.set_defaults(func=cmd_oauth)
# Extract cookies command
extract_parser = subparsers.add_parser(
"extract-cookies",
help="Extract cookies from browser",
)
extract_parser.add_argument(
"--browser",
"-b",
default="firefox",
choices=["firefox", "chrome", "edge", "opera", "brave"],
help="Browser to extract cookies from (default: firefox)",
)
extract_parser.set_defaults(func=cmd_extract_cookies)
# Logout command
logout_parser = subparsers.add_parser("logout", help="Clear stored credentials")
logout_parser.add_argument(
"--all",
"-a",
action="store_true",
help="Clear all credentials",
)
logout_parser.add_argument(
"--oauth",
action="store_true",
help="Clear only OAuth token",
)
logout_parser.add_argument(
"--cookies",
action="store_true",
help="Clear only cookies",
)
logout_parser.set_defaults(func=cmd_logout)
# Parse arguments
args = parser.parse_args()
# Default to status if no command
if not args.command:
args = parser.parse_args(["status"])
# Configure logging - use console format and suppress INFO logs for cleaner CLI output
settings = get_settings()
# Override to console format and WARNING level for CLI
settings.logging.format = "console"
settings.logging.level = "WARNING"
configure_logging(settings.logging)
# Run command
try:
return args.func(args)
except KeyboardInterrupt:
print("\nCancelled.")
return 130
if __name__ == "__main__":
sys.exit(main())