-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_auth.py
More file actions
240 lines (188 loc) · 8.33 KB
/
Copy pathgithub_auth.py
File metadata and controls
240 lines (188 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env python3
"""
This module provides GitHub auth for macOS, Windows, and Linux.
Written by: Garot Conklin
"""
import logging
import platform
import subprocess
from typing import Optional
# Configure logging
logger = logging.getLogger(__name__)
class GitHubAuthError(Exception):
"""Base exception for GitHub authentication errors."""
pass
class TokenNotFoundError(GitHubAuthError):
"""Raised when no GitHub token is found in the system keychain."""
pass
class InvalidTokenError(GitHubAuthError):
"""Raised when the retrieved token is invalid or malformed."""
pass
class PlatformNotSupportedError(GitHubAuthError):
"""Raised when the current platform is not supported."""
pass
class CredentialHelperError(GitHubAuthError):
"""Raised when credential helper operations fail."""
pass
def _validate_token(token: str) -> bool:
"""
Validate GitHub token format.
Args:
token: The token to validate
Returns:
bool: True if token is valid, False otherwise
"""
if not token or not isinstance(token, str):
return False
# GitHub personal access tokens start with 'ghp_' and are 40 characters long
# GitHub organization tokens start with 'gho_' and are 40 characters long
# GitHub fine-grained tokens start with 'github_pat_' and are longer
# Allow for some flexibility in token length for testing
if (token.startswith("ghp_") or token.startswith("gho_")) and len(token) >= 40:
return True
elif token.startswith("github_pat_") and len(token) > 40:
return True
logger.warning("Invalid token format detected")
return False
def _parse_credential_output(output: str) -> Optional[str]:
"""
Parse credential helper output to extract password/token.
Args:
output: Raw output from credential helper
Returns:
Optional[str]: Extracted token or None if not found
"""
if not output:
return None
lines = output.strip().split("\n")
for line in lines:
if line.startswith("password="):
return line.split("=", 1)[1].strip()
return None
def get_github_token() -> Optional[str]:
"""
Retrieves the GitHub token from the system's keychain.
This function uses the 'git' command-line utility to interact with the
system's keychain. If the system is MacOS, it uses the 'osxkeychain'
credential helper. If the system is Windows, it uses the 'wincred'
credential helper. For Linux, it uses libsecret or git credential store.
For other systems, it raises PlatformNotSupportedError.
Returns:
Optional[str]: The GitHub token if it could be found, or None otherwise.
Raises:
PlatformNotSupportedError: If the current platform is not supported
TokenNotFoundError: If no token is found in the system keychain
InvalidTokenError: If the retrieved token is invalid
CredentialHelperError: If credential helper operations fail
"""
if platform.system() == "Darwin":
try:
logger.debug("Attempting to retrieve token from macOS keychain")
output = subprocess.check_output(
["git", "credential-osxkeychain", "get"],
input="protocol=https\nhost=github.com\n",
universal_newlines=True,
stderr=subprocess.DEVNULL,
)
token = _parse_credential_output(output)
if not token:
logger.warning("No token found in macOS keychain")
raise TokenNotFoundError("GitHub access token not found in osxkeychain")
if not _validate_token(token):
logger.error("Invalid token format retrieved from macOS keychain")
raise InvalidTokenError("Invalid token format")
logger.info("Successfully retrieved token from macOS keychain")
return token
except subprocess.CalledProcessError as e:
logger.error(f"Failed to retrieve token from macOS keychain: {e}")
raise CredentialHelperError("Failed to access macOS keychain")
elif platform.system() == "Windows":
try:
logger.debug("Attempting to retrieve token from Windows Credential Manager")
output = subprocess.check_output(
["git", "config", "--get", "credential.helper"],
universal_newlines=True,
stderr=subprocess.DEVNULL,
)
if output.strip() == "manager":
credential_output = subprocess.check_output(
["git", "credential", "fill"],
input="url=https://github.com",
universal_newlines=True,
stderr=subprocess.DEVNULL,
)
token = _parse_credential_output(credential_output)
if not token:
logger.warning("No token found in Windows Credential Manager")
raise TokenNotFoundError(
"GitHub access token not found in Windows Credential Manager"
)
if not _validate_token(token):
logger.error(
"Invalid token format retrieved from Windows Credential Manager"
)
raise InvalidTokenError("Invalid token format")
logger.info(
"Successfully retrieved token from Windows Credential Manager"
)
return token
else:
logger.warning("Windows Credential Manager not configured")
raise TokenNotFoundError(
"GitHub access token not found in Windows Credential Manager"
)
except subprocess.CalledProcessError as e:
logger.error(
f"Failed to retrieve token from Windows Credential Manager: {e}"
)
raise CredentialHelperError("Failed to access Windows Credential Manager")
elif platform.system() == "Linux":
try:
# Try using libsecret (GNOME Keyring)
logger.debug("Attempting to retrieve token from Linux libsecret")
output = subprocess.check_output(
["secret-tool", "lookup", "host", "github.com"],
universal_newlines=True,
stderr=subprocess.DEVNULL,
)
if output.strip():
token = output.strip()
if not _validate_token(token):
logger.error("Invalid token format retrieved from libsecret")
raise InvalidTokenError("Invalid token format")
logger.info("Successfully retrieved token from Linux libsecret")
return token
except FileNotFoundError:
logger.debug("secret-tool not found, falling back to git credential store")
except subprocess.CalledProcessError:
logger.debug(
"No token found in libsecret, falling back to git credential store"
)
# Fall back to git credential store
try:
logger.debug("Attempting to retrieve token from git credential store")
output = subprocess.check_output(
["git", "credential", "fill"],
input="url=https://github.com\n\n",
universal_newlines=True,
stderr=subprocess.DEVNULL,
)
token = _parse_credential_output(output)
if not token:
logger.warning("No token found in git credential store")
raise TokenNotFoundError(
"GitHub access token not found in git credential store"
)
if not _validate_token(token):
logger.error("Invalid token format retrieved from git credential store")
raise InvalidTokenError("Invalid token format")
logger.info("Successfully retrieved token from git credential store")
return token
except subprocess.CalledProcessError as e:
logger.error(f"Failed to retrieve token from git credential store: {e}")
raise CredentialHelperError("Failed to access git credential store")
else:
logger.error(f"Unsupported operating system: {platform.system()}")
raise PlatformNotSupportedError(
f"Unsupported operating system: {platform.system()}"
)