#!/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 os import sys import json import time import requests import argparse from pathlib import Path from urllib.parse import urlencode # 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()