Skip to content

Commit

Permalink
Implement two-factor authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
jacquev6 committed Sep 20, 2014
1 parent 2cf7266 commit 4864627
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 2 deletions.
7 changes: 7 additions & 0 deletions PyGithub/Blocking/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ def Login(self, login, password):
self.__authenticator = _ses._LoginAuthenticator(login, password)
return self

def Otp(self, login, password, otp):
"""
Use `two-factor authentication <https://developer.github.com/v3/auth/#working-with-two-factor-authentication>`__.
"""
self.__authenticator = _ses._OtpAuthenticator(login, password, otp)
return self

def OAuth(self, token):
"""
Use `OAuth authentication <http://developer.github.com/v3/oauth/>`_.
Expand Down
6 changes: 6 additions & 0 deletions PyGithub/Blocking/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class UnauthorizedException(ClientErrorException):
"""


class OtpRequiredException(UnauthorizedException):
"""
Raised by PyGithub when GitHub API v3 returns a 401 HTTP status code and x-github-otp header.
"""


class ForbiddenException(ClientErrorException):
"""
Base class for exceptions raised by PyGithub when GitHub API v3 returns a 403 HTTP status code.
Expand Down
13 changes: 13 additions & 0 deletions PyGithub/Blocking/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ def prepareSession(self, session):
session.auth = (self.__login, self.__password)


class _OtpAuthenticator(object):
def __init__(self, login, password, otp):
self.__login = login
self.__password = password
self.__otp = otp

def prepareSession(self, session):
session.auth = (self.__login, self.__password)
session.headers["X-GitHub-OTP"] = self.__otp


class _OauthAuthenticator(object):
def __init__(self, token):
self.__token = token
Expand Down Expand Up @@ -162,6 +173,8 @@ def __request(self, requestsSession, verb, url, urlArguments=None, postArguments
exceptionClass = exn.ClientErrorException
if status == 401:
exceptionClass = exn.UnauthorizedException
if "x-github-otp" in response.headers:
exceptionClass = exn.OtpRequiredException
if status == 403:
exceptionClass = exn.ForbiddenException
if self.RateLimit.remaining == 0: # @todoBeta Check rate_limiting for search queries
Expand Down
11 changes: 9 additions & 2 deletions PyGithub/Blocking/tests/topics/AuthenticationTestCases.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

from PyGithub.Blocking.tests.Framework import *

# @todoAlpha What about 2 factors authentication?


class UnauthenticatedTestCase(TestCase):
def testGetAuthenticatedUser(self):
Expand Down Expand Up @@ -61,6 +59,15 @@ def testAddAndRemoveScope(self):
u.edit(location="The Moon")


class OtpTestCase(TestCase):
def testGetAuthenticatedUser(self):
g = self.getBuilder().Login(DotComLogin, DotComPassword).Build()
with self.assertRaises(PyGithub.Blocking.OtpRequiredException):
g.get_authenticated_user()
g = self.getBuilder().Otp(DotComLogin, DotComPassword, "348483").Build()
self.assertEqual(g.get_authenticated_user().name, "Vincent Jacques")


class ApplicationAuthTestCase(TestCase):
def testGetUser(self):
g = self.getEnterpriseBuilder().Application("dfb1584c2c0674284875", "16a529070ec817d87e5b186d966fa935bfad1575").Build() # Create application manually as electra
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
[
{
"request": {
"body": null,
"headers": {
"Accept": "application/vnd.github.v3.full+json",
"Accept-Encoding": "gzip, deflate, compress",
"Authorization": "Basic removed=",
"User-Agent": "jacquev6/PyGithub/2; UnitTests recorder"
},
"is_json": false,
"url": {
"netloc": "api.github.com",
"path": "/user",
"query": {},
"scheme": "https"
},
"verb": "GET"
},
"response": {
"body": {
"documentation_url": "https://developer.github.com/v3/auth#working-with-two-factor-authentication",
"message": "Must specify two-factor authentication OTP code."
},
"headers": {
"access-control-allow-credentials": "true",
"access-control-allow-origin": "*",
"access-control-expose-headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
"content-length": "160",
"content-security-policy": "default-src 'none'",
"content-type": "application/json; charset=utf-8",
"date": "Sat, 20 Sep 2014 16:48:09 GMT",
"server": "GitHub.com",
"status": "401 Unauthorized",
"strict-transport-security": "max-age=31536000; includeSubdomains; preload",
"x-content-type-options": "nosniff",
"x-frame-options": "deny",
"x-github-media-type": "github.v3; param=full; format=json",
"x-github-otp": "required; app",
"x-github-request-id": "62E81E54:5B18:6ECD522:541DAFC9",
"x-ratelimit-limit": "60",
"x-ratelimit-remaining": "56",
"x-ratelimit-reset": "1411233998",
"x-xss-protection": "1; mode=block"
},
"is_json": true,
"status": 401
}
},
{
"request": {
"body": null,
"headers": {
"Accept": "application/vnd.github.v3.full+json",
"Accept-Encoding": "gzip, deflate, compress",
"Authorization": "Basic removed=",
"User-Agent": "jacquev6/PyGithub/2; UnitTests recorder",
"X-GitHub-OTP": "348483"
},
"is_json": false,
"url": {
"netloc": "api.github.com",
"path": "/user",
"query": {},
"scheme": "https"
},
"verb": "GET"
},
"response": {
"body": {
"avatar_url": "https://avatars.githubusercontent.com/u/327146?v=2",
"bio": null,
"blog": "http://vincent-jacques.net",
"collaborators": 2,
"company": "Amazon",
"created_at": "2010-07-09T06:10:06Z",
"disk_usage": 118665,
"email": "vincent@vincent-jacques.net",
"events_url": "https://api.github.com/users/jacquev6/events{/privacy}",
"followers": 38,
"followers_url": "https://api.github.com/users/jacquev6/followers",
"following": 51,
"following_url": "https://api.github.com/users/jacquev6/following{/other_user}",
"gists_url": "https://api.github.com/users/jacquev6/gists{/gist_id}",
"gravatar_id": "b68de5ae38616c296fa345d2b9df2225",
"hireable": false,
"html_url": "https://github.com/jacquev6",
"id": 327146,
"location": "Seattle, WA, USA",
"login": "jacquev6",
"name": "Vincent Jacques",
"organizations_url": "https://api.github.com/users/jacquev6/orgs",
"owned_private_repos": 4,
"plan": {
"collaborators": 0,
"name": "micro",
"private_repos": 5,
"space": 614400
},
"private_gists": 51,
"public_gists": 4,
"public_repos": 22,
"received_events_url": "https://api.github.com/users/jacquev6/received_events",
"repos_url": "https://api.github.com/users/jacquev6/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/jacquev6/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jacquev6/subscriptions",
"total_private_repos": 4,
"type": "User",
"updated_at": "2014-09-20T04:22:05Z",
"url": "https://api.github.com/users/jacquev6"
},
"headers": {
"access-control-allow-credentials": "true",
"access-control-allow-origin": "*",
"access-control-expose-headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
"cache-control": "private, max-age=60, s-maxage=60",
"content-encoding": "gzip",
"content-security-policy": "default-src 'none'",
"content-type": "application/json; charset=utf-8",
"date": "Sat, 20 Sep 2014 16:48:10 GMT",
"etag": "\"29d823f49e834b15bb50bf021f69a1fd\"",
"last-modified": "Sat, 20 Sep 2014 04:22:05 GMT",
"server": "GitHub.com",
"status": "200 OK",
"strict-transport-security": "max-age=31536000; includeSubdomains; preload",
"transfer-encoding": "chunked",
"vary": "Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding",
"x-content-type-options": "nosniff",
"x-frame-options": "deny",
"x-github-media-type": "github.v3; param=full; format=json",
"x-github-request-id": "62E81E54:5B14:8D6AEF5:541DAFCA",
"x-ratelimit-limit": "5000",
"x-ratelimit-remaining": "4998",
"x-ratelimit-reset": "1411234808",
"x-served-by": "971af40390ac4398fcdd45c8dab0fbe7",
"x-xss-protection": "1; mode=block"
},
"is_json": true,
"status": 200
}
}
]
6 changes: 6 additions & 0 deletions PyGithub/Blocking/tests/unit/BuilderTestCases.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ def testLogin(self):
response = s._request("GET", "http://foo.com")
self.assertEqual(response.status_code, 200)

def testOtp(self):
s = self.makeSession(bld.Builder().Otp("login", "password", "otp"))
self.adapter.expect.send.withArguments(RequestMatcher("GET", "http://foo.com/", {"Authorization": "Basic bG9naW46cGFzc3dvcmQ=", "X-GitHub-OTP": "otp", "Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": bld.Builder.defaultUserAgent}, None)).andReturn(rebuildResponse(200, dict(), ""))
response = s._request("GET", "http://foo.com")
self.assertEqual(response.status_code, 200)

def testOAuth(self):
s = self.makeSession(bld.Builder().OAuth("token"))
self.adapter.expect.send.withArguments(RequestMatcher("GET", "http://foo.com/", {"Authorization": "token token", "Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": bld.Builder.defaultUserAgent}, None)).andReturn(rebuildResponse(200, dict(), ""))
Expand Down
5 changes: 5 additions & 0 deletions PyGithub/Blocking/tests/unit/SessionTestCases.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ def test401(self):
with self.assertRaises(PyGithub.Blocking.UnauthorizedException):
self.session._request("GET", "http://foo.com")

def test401WithOtp(self):
self.adapter.expect.send.withArguments(RequestMatcher("GET", "http://foo.com/", {"Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "user-agent"}, None)).andReturn(rebuildResponse(401, {"X-GitHub-OTP": "required; app"}, "{}"))
with self.assertRaises(PyGithub.Blocking.OtpRequiredException):
self.session._request("GET", "http://foo.com")

def test403WithRateLimitRemaining(self):
self.adapter.expect.send.withArguments(RequestMatcher("GET", "http://foo.com/", {"Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "user-agent"}, None)).andReturn(rebuildResponse(403, {"x-ratelimit-limit": "42", "x-ratelimit-remaining": "1", "x-ratelimit-reset": "1408161103"}, "{}"))
with self.assertRaises(PyGithub.Blocking.ForbiddenException):
Expand Down

0 comments on commit 4864627

Please sign in to comment.