#!/usr/bin/env python3 """ GitHub OAuth Device Flow Authentication for ThrillWiki CI/CD This script implements GitHub's device flow to securely obtain access tokens. """ import sys import time import requests import argparse from pathlib import Path # GitHub OAuth App Configuration CLIENT_ID = "Iv23liOX5Hp75AxhUvIe" TOKEN_FILE = ".github-token" def parse_response(response): """Parse HTTP response and handle errors.""" if response.status_code in [200, 201]: return response.json() elif response.status_code == 401: print("You are not authorized. Run the `login` command.") sys.exit(1) else: print(f"HTTP {response.status_code}: {response.text}") sys.exit(1) def request_device_code(): """Request a device code from GitHub.""" url = "https://github.com/login/device/code" data = {"client_id": CLIENT_ID} headers = {"Accept": "application/json"} response = requests.post(url, data=data, headers=headers) return parse_response(response) def request_token(device_code): """Request an access token using the device code.""" url = "https://github.com/login/oauth/access_token" data = { "client_id": CLIENT_ID, "device_code": device_code, "grant_type": "urn:ietf:params:oauth:grant-type:device_code", } headers = {"Accept": "application/json"} response = requests.post(url, data=data, headers=headers) return parse_response(response) def poll_for_token(device_code, interval): """Poll GitHub for the access token after user authorization.""" print("Waiting for authorization...") while True: response = request_token(device_code) error = response.get("error") access_token = response.get("access_token") if error: if error == "authorization_pending": # User hasn't entered the code yet print(".", end="", flush=True) time.sleep(interval) continue elif error == "slow_down": # Polling too fast time.sleep(interval + 5) continue elif error == "expired_token": print("\nThe device code has expired. Please run `login` again.") sys.exit(1) elif error == "access_denied": print("\nLogin cancelled by user.") sys.exit(1) else: print(f"\nError: {response}") sys.exit(1) # Success! Save the token token_path = Path(TOKEN_FILE) token_path.write_text(access_token) token_path.chmod(0o600) # Read/write for owner only print(f"\nToken saved to {TOKEN_FILE}") break def login(): """Initiate the GitHub OAuth device flow login process.""" print("Starting GitHub authentication...") device_response = request_device_code() verification_uri = device_response["verification_uri"] user_code = device_response["user_code"] device_code = device_response["device_code"] interval = device_response["interval"] print(f"\nPlease visit: {verification_uri}") print(f"and enter code: {user_code}") print("\nWaiting for you to complete authorization in your browser...") poll_for_token(device_code, interval) print("Successfully authenticated!") return True def whoami(): """Display information about the authenticated user.""" token_path = Path(TOKEN_FILE) if not token_path.exists(): print("You are not authorized. Run the `login` command.") sys.exit(1) try: token = token_path.read_text().strip() except Exception as e: print(f"Error reading token: {e}") print("You may need to run the `login` command again.") sys.exit(1) url = "https://api.github.com/user" headers = { "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", } response = requests.get(url, headers=headers) user_data = parse_response(response) print(f"You are authenticated as: {user_data['login']}") print(f"Name: {user_data.get('name', 'Not set')}") print(f"Email: {user_data.get('email', 'Not public')}") return user_data def get_token(): """Get the current access token if available.""" token_path = Path(TOKEN_FILE) if not token_path.exists(): return None try: return token_path.read_text().strip() except Exception: return None def validate_token(): """Validate that the current token is still valid.""" token = get_token() if not token: return False url = "https://api.github.com/user" headers = { "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", } try: response = requests.get(url, headers=headers) return response.status_code == 200 except Exception: return False def ensure_authenticated(): """Ensure user is authenticated, prompting login if necessary.""" if validate_token(): return get_token() print("GitHub authentication required.") login() return get_token() def logout(): """Remove the stored access token.""" token_path = Path(TOKEN_FILE) if token_path.exists(): token_path.unlink() print("Successfully logged out.") else: print("You are not currently logged in.") def main(): """Main CLI interface.""" parser = argparse.ArgumentParser( description="GitHub OAuth authentication for ThrillWiki CI/CD" ) parser.add_argument( "command", choices=["login", "logout", "whoami", "token", "validate"], help="Command to execute", ) if len(sys.argv) == 1: parser.print_help() sys.exit(1) args = parser.parse_args() if args.command == "login": login() elif args.command == "logout": logout() elif args.command == "whoami": whoami() elif args.command == "token": token = get_token() if token: print(token) else: print("No token available. Run `login` first.") sys.exit(1) elif args.command == "validate": if validate_token(): print("Token is valid.") else: print("Token is invalid or missing.") sys.exit(1) if __name__ == "__main__": main()