Skip to content

Commit 8ccf9a9

Browse files
ardakuyumcusfdye
authored andcommitted
Add support for JWT authentication (#948)
APIs such as https://developer.github.com/v3/apps/#find-organization-installation use a JWT for authentication. Adding support for JWT auth.
1 parent 60a684c commit 8ccf9a9

File tree

7 files changed

+44
-6
lines changed

7 files changed

+44
-6
lines changed

CONTRIBUTING.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ from deprecated import deprecated
4545
@deprecated
4646
def rate(self):
4747
pass
48-
48+
4949
@deprecated(reason="Deprecated in favor of the new branch protection")
5050
def get_protected_branch(self):
5151
pass
@@ -65,10 +65,13 @@ You will need a `GithubCredentials.py` file at the root of the project with the
6565
login = "my_login"
6666
password = "my_password"
6767
oauth_token = "my_token" # Can be left empty if not used
68+
jwt = "my_json_web_token" # Can be left empty if not used
6869
```
6970

7071
If you use 2 factor authentication on your Github account, tests that require a login/password authentication will fail.
71-
You can use `python -m github.tests Issue139.testCompletion --record --auth_with_token` to use the `oauth_token` field specified in `GithubCredentials.py` when recording a unit test interaction. Note that the `password = ""` (empty string is ok) must still be present in `GithubCredentials.py` to run the tests even when the `--auth_with_token` arg is used. (Also note that if you record your test data with `--auth_with_token` then you also need to be in token authentication mode when running the test. A simple alternative it to replace `token private_token_removed` with `Basic login_and_password_removed` in all your newly generated ReplayData files.)
72+
You can use `python -m github.tests Issue139.testCompletion --record --auth_with_token` to use the `oauth_token` field specified in `GithubCredentials.py` when recording a unit test interaction. Note that the `password = ""` (empty string is ok) must still be present in `GithubCredentials.py` to run the tests even when the `--auth_with_token` arg is used. (Also note that if you record your test data with `--auth_with_token` then you also need to be in token authentication mode when running the test. A simple alternative is to replace `token private_token_removed` with `Basic login_and_password_removed` in all your newly generated ReplayData files.)
73+
74+
Similarly, you can use `python -m github.tests Issue139.testCompletion --record --auth_with_jwt` to use the `jwt` field specified in `GithubCredentials.py` to access endpoints that require JWT.
7275

7376
To run manual tests with external scripts that use the PyGithub package, you can install your development version with:
7477

github/MainClass.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class Github(object):
9494
This is the main class you instantiate to access the Github API v3. Optional parameters allow different authentication methods.
9595
"""
9696

97-
def __init__(self, login_or_token=None, password=None, base_url=DEFAULT_BASE_URL, timeout=DEFAULT_TIMEOUT, client_id=None, client_secret=None, user_agent='PyGithub/Python', per_page=DEFAULT_PER_PAGE, api_preview=False, verify=True):
97+
def __init__(self, login_or_token=None, password=None, jwt=None, base_url=DEFAULT_BASE_URL, timeout=DEFAULT_TIMEOUT, client_id=None, client_secret=None, user_agent='PyGithub/Python', per_page=DEFAULT_PER_PAGE, api_preview=False, verify=True):
9898
"""
9999
:param login_or_token: string
100100
:param password: string
@@ -109,13 +109,14 @@ def __init__(self, login_or_token=None, password=None, base_url=DEFAULT_BASE_URL
109109

110110
assert login_or_token is None or isinstance(login_or_token, (str, unicode)), login_or_token
111111
assert password is None or isinstance(password, (str, unicode)), password
112+
assert jwt is None or isinstance(jwt, (str, unicode)), jwt
112113
assert isinstance(base_url, (str, unicode)), base_url
113114
assert isinstance(timeout, (int, long)), timeout
114115
assert client_id is None or isinstance(client_id, (str, unicode)), client_id
115116
assert client_secret is None or isinstance(client_secret, (str, unicode)), client_secret
116117
assert user_agent is None or isinstance(user_agent, (str, unicode)), user_agent
117118
assert isinstance(api_preview, (bool))
118-
self.__requester = Requester(login_or_token, password, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify)
119+
self.__requester = Requester(login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify)
119120

120121
def __get_FIX_REPO_GET_GIT_REF(self):
121122
"""

github/Requester.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def _initializeDebugFeature(self):
214214

215215
#############################################################
216216

217-
def __init__(self, login_or_token, password, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify):
217+
def __init__(self, login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify):
218218
self._initializeDebugFeature()
219219

220220
if password is not None:
@@ -226,6 +226,8 @@ def __init__(self, login_or_token, password, base_url, timeout, client_id, clien
226226
elif login_or_token is not None:
227227
token = login_or_token
228228
self.__authorizationHeader = "token " + token
229+
elif jwt is not None:
230+
self.__authorizationHeader = "Bearer " + jwt
229231
else:
230232
self.__authorizationHeader = None
231233

@@ -469,6 +471,8 @@ def __log(self, verb, url, requestHeaders, input, status, responseHeaders, outpu
469471
requestHeaders["Authorization"] = "Basic (login and password removed)"
470472
elif requestHeaders["Authorization"].startswith("token"):
471473
requestHeaders["Authorization"] = "token (oauth token removed)"
474+
elif requestHeaders["Authorization"].startswith("Bearer"):
475+
requestHeaders["Authorization"] = "Bearer (jwt removed)"
472476
else: # pragma no cover (Cannot happen, but could if we add an authentication method => be prepared)
473477
requestHeaders["Authorization"] = "(unknown auth removed)" # pragma no cover (Cannot happen, but could if we add an authentication method => be prepared)
474478
logger.debug("%s %s://%s%s %s %s ==> %i %s %s", str(verb), self.__scheme, self.__hostname, str(url), str(requestHeaders), str(input), status, str(responseHeaders), str(output))

github/tests/Authentication.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ def testOAuthAuthentication(self):
4545
g = github.Github(self.oauth_token)
4646
self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques")
4747

48-
# Warning: I don't have a scret key, so the requests for this test are forged
48+
def testJWTAuthentication(self):
49+
g = github.Github(jwt=self.jwt)
50+
self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques")
51+
52+
# Warning: I don't have a secret key, so the requests for this test are forged
4953
def testSecretKeyAuthentication(self):
5054
g = github.Github(client_id=self.client_id, client_secret=self.client_secret)
5155
self.assertListKeyEqual(g.get_organization("BeaverSoftware").get_repos("public"), lambda r: r.name, ["FatherBeaver", "PyGithub"])

github/tests/Framework.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ def fixAuthorizationHeader(headers):
8080
headers["Authorization"] = "token private_token_removed"
8181
elif headers["Authorization"].startswith("Basic "):
8282
headers["Authorization"] = "Basic login_and_password_removed"
83+
elif headers["Authorization"].startswith("Bearer "):
84+
headers["Authorization"] = "Bearer jwt_removed"
8385

8486

8587
class RecordingConnection: # pragma no cover (Class useful only when recording new tests, not used during automated tests)
@@ -213,6 +215,7 @@ def ReplayingHttpsConnection(testCase, file, *args, **kwds):
213215
class BasicTestCase(unittest.TestCase):
214216
recordMode = False
215217
tokenAuthMode = False
218+
jwtAuthMode = False
216219
replayDataFolder = os.path.join(os.path.dirname(__file__), "ReplayData")
217220

218221
def setUp(self):
@@ -228,6 +231,7 @@ def setUp(self):
228231
self.login = GithubCredentials.login
229232
self.password = GithubCredentials.password
230233
self.oauth_token = GithubCredentials.oauth_token
234+
self.jwt = GithubCredentials.jwt
231235
# @todo Remove client_id and client_secret from ReplayData (as we already remove login, password and oauth_token)
232236
# self.client_id = GithubCredentials.client_id
233237
# self.client_secret = GithubCredentials.client_secret
@@ -241,6 +245,7 @@ def setUp(self):
241245
self.oauth_token = "oauth_token"
242246
self.client_id = "client_id"
243247
self.client_secret = "client_secret"
248+
self.jwt = "jwt"
244249

245250
def tearDown(self):
246251
unittest.TestCase.tearDown(self)
@@ -294,6 +299,8 @@ def setUp(self):
294299

295300
if self.tokenAuthMode:
296301
self.g = github.Github(self.oauth_token)
302+
elif self.jwtAuthMode:
303+
self.g = github.Github(jwt=self.jwt)
297304
else:
298305
self.g = github.Github(self.login, self.password)
299306

@@ -304,3 +311,7 @@ def activateRecordMode(): # pragma no cover (Function useful only when recordin
304311

305312
def activateTokenAuthMode(): # pragma no cover (Function useful only when recording new tests, not used during automated tests)
306313
BasicTestCase.tokenAuthMode = True
314+
315+
316+
def activateJWTAuthMode(): # pragma no cover (Function useful only when recording new tests, not used during automated tests)
317+
BasicTestCase.jwtAuthMode = True
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
https
2+
GET
3+
api.github.com
4+
None
5+
/users/jacquev6
6+
{'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python'}
7+
None
8+
200
9+
[('status', '200 OK'), ('x-ratelimit-remaining', '4994'), ('content-length', '623'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"0e3990a84c08ccd728a27dbe549d4f86"'), ('date', 'Sat, 26 May 2012 09:34:29 GMT'), ('x-oauth-scopes', ''), ('content-type', 'application/json; charset=utf-8'), ('x-accepted-oauth-scopes', 'user')]
10+
{"type":"User","company":"Criteo","location":"Paris, France","hireable":false,"gravatar_id":"b68de5ae38616c296fa345d2b9df2225","bio":"","following":24,"blog":"http://vincent-jacques.net","avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","followers":13,"html_url":"https://github.com/jacquev6","url":"https://api.github.com/users/jacquev6","name":"Vincent Jacques","login":"jacquev6","public_repos":11,"public_gists":1,"email":"vincent@vincent-jacques.net","id":327146,"created_at":"2010-07-09T06:10:06Z"}
11+

github/tests/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def main(argv):
4444
github.tests.Framework.activateTokenAuthMode()
4545
argv = [arg for arg in argv if arg != "--auth_with_token"]
4646

47+
if "--auth_with_jwt" in argv:
48+
github.tests.Framework.activateJWTAuthMode()
49+
argv = [arg for arg in argv if arg != "--auth_with_jwt"]
50+
4751
unittest.main(module=github.tests.AllTests, argv=argv)
4852

4953

0 commit comments

Comments
 (0)