Skip to content

Commit 14b80e6

Browse files
committed
initial commit
1 parent 5ecf29b commit 14b80e6

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed

githubauthlib/github_auth.py

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
"""
3+
This module provides GitHub authentication for macOS, Windows, and Linux systems.
4+
5+
The module retrieves GitHub tokens from the system's secure storage:
6+
- macOS: Keychain Access
7+
- Windows: Credential Manager
8+
- Linux: libsecret
9+
10+
Written by: Garot Conklin
11+
"""
12+
13+
import subprocess
14+
import platform
15+
import logging
16+
import re
17+
from typing import Optional
18+
19+
# Configure logging
20+
logging.basicConfig(
21+
level=logging.INFO,
22+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23+
)
24+
logger = logging.getLogger(__name__)
25+
26+
class GitHubAuthError(Exception):
27+
"""Base exception for GitHub authentication errors."""
28+
pass
29+
30+
class CredentialHelperError(GitHubAuthError):
31+
"""Raised when there's an error with the credential helper."""
32+
pass
33+
34+
class UnsupportedPlatformError(GitHubAuthError):
35+
"""Raised when the operating system is not supported."""
36+
pass
37+
38+
def validate_token(token: str) -> bool:
39+
"""
40+
Validate the format of a GitHub token.
41+
42+
Args:
43+
token (str): The token to validate
44+
45+
Returns:
46+
bool: True if the token format is valid, False otherwise
47+
"""
48+
# GitHub tokens are 40 characters long and contain only hexadecimal characters
49+
token_pattern = re.compile(r'^gh[ps]_[A-Za-z0-9_]{36}$|^[a-f0-9]{40}$')
50+
return bool(token_pattern.match(token))
51+
52+
def get_github_token() -> Optional[str]:
53+
"""
54+
Retrieves the GitHub token from the system's secure storage.
55+
56+
Returns:
57+
str: The GitHub token if found and valid
58+
59+
Raises:
60+
CredentialHelperError: If there's an error accessing the credential helper
61+
UnsupportedPlatformError: If the operating system is not supported
62+
"""
63+
system = platform.system()
64+
65+
try:
66+
if system == "Darwin":
67+
return _get_token_macos()
68+
elif system == "Windows":
69+
return _get_token_windows()
70+
elif system == "Linux":
71+
return _get_token_linux()
72+
else:
73+
raise UnsupportedPlatformError(f"Unsupported operating system: {system}")
74+
except subprocess.CalledProcessError as e:
75+
raise CredentialHelperError(f"Error accessing credential helper: {str(e)}")
76+
77+
def _get_token_macos() -> Optional[str]:
78+
"""Retrieve token from macOS keychain."""
79+
output = subprocess.check_output(
80+
["git", "credential-osxkeychain", "get"],
81+
input="protocol=https\nhost=github.com\n",
82+
universal_newlines=True,
83+
stderr=subprocess.PIPE
84+
)
85+
86+
for line in output.split('\n'):
87+
if line.startswith('password='):
88+
token = line.split('=')[1].strip()
89+
if validate_token(token):
90+
return token
91+
else:
92+
logger.warning("Retrieved token failed validation")
93+
return None
94+
95+
logger.info("No GitHub token found in macOS keychain")
96+
return None
97+
98+
def _get_token_windows() -> Optional[str]:
99+
"""Retrieve token from Windows Credential Manager."""
100+
helper = subprocess.check_output(
101+
["git", "config", "--get", "credential.helper"],
102+
universal_newlines=True
103+
).strip()
104+
105+
if helper not in ["manager", "manager-core", "wincred"]:
106+
raise CredentialHelperError("Windows credential manager not configured")
107+
108+
output = subprocess.check_output(
109+
["git", "credential", "fill"],
110+
input="url=https://github.com\n\n",
111+
universal_newlines=True
112+
)
113+
114+
credentials = dict(
115+
line.split('=', 1)
116+
for line in output.strip().split('\n')
117+
if '=' in line
118+
)
119+
120+
token = credentials.get('password')
121+
if token and validate_token(token):
122+
return token
123+
124+
logger.info("No valid GitHub token found in Windows Credential Manager")
125+
return None
126+
127+
def _get_token_linux() -> Optional[str]:
128+
"""Retrieve token from Linux libsecret."""
129+
try:
130+
output = subprocess.check_output(
131+
["secret-tool", "lookup", "host", "github.com"],
132+
universal_newlines=True
133+
)
134+
135+
token = output.strip()
136+
if validate_token(token):
137+
return token
138+
139+
logger.warning("Retrieved token failed validation")
140+
return None
141+
142+
except FileNotFoundError:
143+
logger.error("libsecret-tools not installed. Please install using your package manager.")
144+
return None

0 commit comments

Comments
 (0)