Skip to content

Commit

Permalink
Support oauth for enterprise (#2780)
Browse files Browse the repository at this point in the history
  • Loading branch information
EnricoMi committed Mar 21, 2024
1 parent d65fc30 commit e4106e0
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 5 deletions.
18 changes: 14 additions & 4 deletions github/ApplicationOAuth.py
Expand Up @@ -32,6 +32,7 @@

import github.AccessToken
import github.Auth
from github.Consts import DEFAULT_BASE_URL, DEFAULT_OAUTH_URL
from github.GithubException import BadCredentialsException, GithubException
from github.GithubObject import Attribute, NonCompletableGithubObject, NotSet
from github.Requester import Requester
Expand Down Expand Up @@ -82,6 +83,16 @@ def _useAttributes(self, attributes: dict[str, Any]) -> None:
if "client_secret" in attributes: # pragma no branch
self._client_secret = self._makeStringAttribute(attributes["client_secret"])

def get_oauth_url(self, path: str) -> str:
if not path.startswith("/"):
path = f"/{path}"

if self._requester.base_url == DEFAULT_BASE_URL:
base_url = DEFAULT_OAUTH_URL
else:
base_url = f"{self._requester.scheme}://{self._requester.hostname_and_port}/login/oauth"
return f"{base_url}{path}"

def get_login_url(
self,
redirect_uri: str | None = None,
Expand All @@ -104,8 +115,7 @@ def get_login_url(

query = urllib.parse.urlencode(parameters)

base_url = "https://github.com/login/oauth/authorize"
return f"{base_url}?{query}"
return self.get_oauth_url(f"/authorize?{query}")

def get_access_token(self, code: str, state: str | None = None) -> AccessToken:
"""
Expand All @@ -124,7 +134,7 @@ def get_access_token(self, code: str, state: str | None = None) -> AccessToken:
headers, data = self._checkError(
*self._requester.requestJsonAndCheck(
"POST",
"https://github.com/login/oauth/access_token",
self.get_oauth_url("/access_token"),
headers={"Accept": "application/json"},
input=post_parameters,
)
Expand Down Expand Up @@ -165,7 +175,7 @@ def refresh_access_token(self, refresh_token: str) -> AccessToken:
headers, data = self._checkError(
*self._requester.requestJsonAndCheck(
"POST",
"https://github.com/login/oauth/access_token",
self.get_oauth_url("/access_token"),
headers={"Accept": "application/json"},
input=post_parameters,
)
Expand Down
1 change: 1 addition & 0 deletions github/Consts.py
Expand Up @@ -154,6 +154,7 @@
repoVisibilityPreview = "application/vnd.github.nebula-preview+json"

DEFAULT_BASE_URL = "https://api.github.com"
DEFAULT_OAUTH_URL = "https://github.com/login/oauth"
DEFAULT_STATUS_URL = "https://status.github.com"
DEFAULT_USER_AGENT = "PyGithub/Python"
# As of 2018-05-17, Github imposes a 10s limit for completion of API requests.
Expand Down
12 changes: 11 additions & 1 deletion github/Requester.py
Expand Up @@ -506,10 +506,20 @@ def base_url(self) -> str:
def graphql_url(self) -> str:
return self.__graphql_url

@property
def scheme(self) -> str:
return self.__scheme

@property
def hostname(self) -> str:
return self.__hostname

@property
def hostname_and_port(self) -> str:
if self.__port is None:
return self.hostname
return f"{self.hostname}:{self.__port}"

@property
def auth(self) -> Optional["Auth"]:
return self.__auth
Expand Down Expand Up @@ -917,7 +927,7 @@ def __makeAbsoluteUrl(self, url: str) -> str:
"status.github.com",
"github.com",
], o.hostname
assert o.path.startswith((self.__prefix, self.__graphql_prefix, "/api/")), o.path
assert o.path.startswith((self.__prefix, self.__graphql_prefix, "/api/", "/login/oauth")), o.path
assert o.port == self.__port, o.port
url = o.path
if o.query != "":
Expand Down
41 changes: 41 additions & 0 deletions tests/ApplicationOAuth.py
Expand Up @@ -49,6 +49,8 @@ def setUp(self):
self.CLIENT_ID = "client_id_removed"
self.CLIENT_SECRET = "client_secret_removed"
self.app = self.g.get_oauth_application(self.CLIENT_ID, self.CLIENT_SECRET)
self.ent_gh = github.Github(base_url="http://my.enterprise.com/path/to/github")
self.ent_app = self.ent_gh.get_oauth_application(self.CLIENT_ID, self.CLIENT_SECRET)

def testLoginURL(self):
BASE_URL = "https://github.com/login/oauth/authorize"
Expand Down Expand Up @@ -78,6 +80,45 @@ def testGetAccessToken(self):
self.assertIsNone(access_token.refresh_expires_in)
self.assertIsNone(access_token.refresh_expires_at)

def testEnterpriseSupport(self):
requester = self.ent_gh._Github__requester
self.assertEqual(requester.scheme, "http")
self.assertEqual(requester.hostname, "my.enterprise.com")
self.assertEqual(requester.hostname_and_port, "my.enterprise.com")
self.assertEqual(self.ent_app.get_oauth_url("auth"), "http://my.enterprise.com/login/oauth/auth")
gh_w_port = github.Github(
base_url="http://my.enterprise.com:443/path/to/github"
)._Github__requester.hostname_and_port
self.assertEqual(gh_w_port, "my.enterprise.com:443")

def testEnterpriseLoginURL(self):
BASE_URL = "http://my.enterprise.com/login/oauth/authorize"
sample_uri = "https://myapp.com/some/path"
sample_uri_encoded = "https%3A%2F%2Fmyapp.com%2Fsome%2Fpath"
self.assertEqual(self.ent_app.get_login_url(), f"{BASE_URL}?client_id={self.CLIENT_ID}")
self.assertTrue(f"redirect_uri={sample_uri_encoded}" in self.ent_app.get_login_url(redirect_uri=sample_uri))
self.assertTrue(f"client_id={self.CLIENT_ID}" in self.ent_app.get_login_url(redirect_uri=sample_uri))
self.assertTrue("state=123abc" in self.ent_app.get_login_url(state="123abc", login="user"))
self.assertTrue("login=user" in self.ent_app.get_login_url(state="123abc", login="user"))
self.assertTrue(f"client_id={self.CLIENT_ID}" in self.ent_app.get_login_url(state="123abc", login="user"))

def testEnterpriseGetAccessToken(self):
access_token = self.ent_app.get_access_token("oauth_code_removed", state="state_removed")
# Test string representation
self.assertEqual(
str(access_token),
'AccessToken(type="bearer", token="acces...", scope="", '
"refresh_token_expires_in=None, refresh_token=None, expires_in=None)",
)
self.assertEqual(access_token.token, "access_token_removed")
self.assertEqual(access_token.type, "bearer")
self.assertEqual(access_token.scope, "")
self.assertIsNone(access_token.expires_in)
self.assertIsNone(access_token.expires_at)
self.assertIsNone(access_token.refresh_token)
self.assertIsNone(access_token.refresh_expires_in)
self.assertIsNone(access_token.refresh_expires_at)

def testGetAccessTokenWithExpiry(self):
with mock.patch("github.AccessToken.datetime") as dt:
dt.now = mock.Mock(return_value=datetime(2023, 6, 7, 12, 0, 0, 123, tzinfo=timezone.utc))
Expand Down
10 changes: 10 additions & 0 deletions tests/ReplayData/ApplicationOAuth.testEnterpriseGetAccessToken.txt
@@ -0,0 +1,10 @@
http
POST
my.enterprise.com
None
/login/oauth/access_token
{'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'PyGithub/Python'}
{"client_secret": "client_secret_removed", "code": "oauth_code_removed", "client_id": "client_id_removed", "state": "state_removed"}
200
[('Date', 'Fri, 25 Jan 2019 11:06:39 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Server', 'GitHub.com'), ('Status', '200 OK'), ('Vary', 'X-PJAX, Accept-Encoding'), ('ETag', 'W/"deebfe47f0039427b39ec010749014f6"'), ('Cache-Control', 'max-age=0, private, must-revalidate'), ('Set-Cookie', 'has_recent_activity=1; path=/; expires=Fri, 25 Jan 2019 12:06:38 -0000, ignored_unsupported_browser_notice=false; path=/'), ('X-Request-Id', 'ed8794eb-dc95-481f-8e52-2cd5db0494a0'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Expect-CT', 'max-age=2592000, report-uri="https://api.github.com/_private/browser/errors"'), ('Content-Security-Policy', "default-src 'none'; base-uri 'self'; block-all-mixed-content; connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; frame-src render.githubusercontent.com; img-src 'self' data: github.githubassets.com assets-cdn.github.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src 'self'; media-src 'none'; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'C8AC:1D8B2:126D746:1BF8DE4:5C4AEDBE')]
{"access_token":"access_token_removed","token_type":"bearer","scope":""}

0 comments on commit e4106e0

Please sign in to comment.