-
-
Notifications
You must be signed in to change notification settings - Fork 60
Adding utility functions for GitHub App #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7c86a5
490e288
5c332e2
7551680
75a493f
9fa267b
5b0a1c4
9f35c2f
95ea2e5
1235ae7
1ef481b
9454ebf
cce20ad
956264f
d04a4d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| :mod:`gidgethub.apps` --- Support for GitHub App | ||
| ================================================ | ||
|
|
||
| .. module:: gidgethub.apps | ||
|
|
||
| .. versionadded:: 4.1.0 | ||
|
|
||
| This module is to help provide support for `GitHub Apps <https://developer.github.com/v3/apps/>`_. | ||
|
|
||
| Example on how you would obtain the access token for authenticating as a GitHub App installation:: | ||
|
|
||
| from gidgethub.apps import get_installation_access_token | ||
|
|
||
| private_key = """-----BEGIN RSA PRIVATE KEY----- | ||
| zBgqFIin/uQEb0he006F9pNC6Kga0AMY5b0cCdZ4ge9qyFro2eVA | ||
| ... | ||
| -----END RSA PRIVATE KEY----- | ||
| """ | ||
|
|
||
| access_token_response = await get_installation_access_token( | ||
| installation_id=123, | ||
| app_id=456, | ||
| private_key=private_key | ||
| ) | ||
|
|
||
| data = gh.getitem("/rate_limit", oauth_token=access_token_response["token"]) | ||
|
|
||
| .. coroutine:: get_installation_access_token(gh, *, installation_id, app_id, private_key) | ||
|
|
||
| Obtain a GitHub App's installation access token. | ||
|
|
||
| **installation_id** is the GitHub App installation's id. | ||
|
|
||
| **app_id** is the GitHub App's identifier. | ||
|
|
||
| **private_key** is the content of the GitHub App's private key (``.PEM`` format) file. | ||
|
|
||
| It returns the response from GitHub's | ||
| `Authenticating as an installation <https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation>`_ API endpoint. | ||
|
|
||
|
|
||
| .. function:: get_jwt(*, app_id, private_key) | ||
|
|
||
| Construct the JWT (JSON Web Token), that can be used to access endpoints | ||
| that require it. | ||
|
|
||
| Example:: | ||
|
|
||
| from gidgethub.apps import get_jwt | ||
|
|
||
| private_key = """-----BEGIN RSA PRIVATE KEY----- | ||
| zBgqFIin/uQEb0he006F9pNC6Kga0AMY5b0cCdZ4ge9qyFro2eVA | ||
| ... | ||
| -----END RSA PRIVATE KEY----- | ||
| """ | ||
|
|
||
| token = get_jwt(app_id=123, private_key=private_key) | ||
| data = gh.getitem( | ||
| "/app/installations", | ||
| jwt=token, | ||
| accept="application/vnd.github.machine-man-preview+json", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,6 +130,7 @@ Contents | |
| __init__ | ||
| sansio | ||
| actions | ||
| apps | ||
| routing | ||
| abc | ||
| aiohttp | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Support for GitHub Actions.""" | ||
| from typing import cast, Any, Dict | ||
|
|
||
| import time | ||
| import jwt | ||
|
|
||
| from gidgethub.abc import GitHubAPI | ||
|
|
||
|
|
||
| def get_jwt(*, app_id: str, private_key: str) -> str: | ||
| """Construct the JWT (JSON Web Token), used for GitHub App authentication.""" | ||
| time_int = int(time.time()) | ||
| payload = {"iat": time_int, "exp": time_int + (10 * 60), "iss": app_id} | ||
| encoded = jwt.encode(payload, private_key, algorithm="RS256") | ||
| bearer_token = encoded.decode("utf-8") | ||
|
|
||
| return bearer_token | ||
|
|
||
|
|
||
| async def get_installation_access_token( | ||
| gh: GitHubAPI, *, installation_id: str, app_id: str, private_key: str | ||
| ) -> Dict[str, Any]: | ||
| """Obtain a GitHub App's installation access token. | ||
| Return a dictionary containing access token and expiration time. | ||
| (https://developer.github.com/v3/apps/#create-a-new-installation-token) | ||
| """ | ||
| access_token_url = f"/app/installations/{installation_id}/access_tokens" | ||
| token = get_jwt(app_id=app_id, private_key=private_key) | ||
| response = await gh.post( | ||
| access_token_url, | ||
| data=b"", | ||
| jwt=token, | ||
| accept="application/vnd.github.machine-man-preview+json", | ||
| ) | ||
| # example response | ||
| # { | ||
| # "token": "v1.1f699f1069f60xxx", | ||
| # "expires_at": "2016-07-11T22:14:10Z" | ||
| # } | ||
|
|
||
| return cast(Dict[str, Any], response) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,11 @@ build-backend = "flit.buildapi" | |
| module = "gidgethub" | ||
| author = "Brett Cannon" | ||
| author-email = "[email protected]" | ||
| requires = ["uritemplate>=3.0.1"] | ||
| requires = [ | ||
| "uritemplate>=3.0.1", | ||
| "PyJWT>=1.7.1", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opened #116 to track this. |
||
| "cryptography>=2.9" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not a direct dependency, why do you specify it here? |
||
| ] | ||
| requires-python = ">=3.6" | ||
| license = "Apache" | ||
| keywords = "github sans-io async" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| -----BEGIN RSA PRIVATE KEY----- | ||
| MIIEpQIBAAKCAQEA1HgzBfJv2cOjQryCwe8NEelriOTNFWKZUivevUrRhlqcmZJd | ||
| CvuCJRr+xCN+OmO8qwgJJR98feNujxVg+J9Ls3/UOA4HcF9nYH6aqVXELAE8Hk/A | ||
| Lvxi96ms1DDuAvQGaYZ+lANxlvxeQFOZSbjkz/9mh8aLeGKwqJLp3p+OhUBQpwvA | ||
| UAPg82+OUtgTW3nSljjeFr14B8qAneGSc/wl0ni++1SRZUXFSovzcqQOkla3W27r | ||
| rLfrD6LXgj/TsDs4vD1PnIm1zcVenKT7TfYI17bsG/O/Wecwz2Nl19pL7gDosNru | ||
| F3ogJWNq1Lyn/ijPQnkPLpZHyhvuiycYcI3DiQIDAQABAoIBAQCt9uzwBZ0HVGQs | ||
| lGULnUu6SsC9iXlR9TVMTpdFrij4NODb7Tc5cs0QzJWkytrjvB4Se7XhK3KnMLyp | ||
| cvu/Fc7J3fRJIVN98t+V5pOD6rGAxlIPD4Vv8z6lQcw8wQNgb6WAaZriXh93XJNf | ||
| YBO2hSj0FU5CBZLUsxmqLQBIQ6RR/OUGAvThShouE9K4N0vKB2UPOCu5U+d5zS3W | ||
| 44Q5uatxYiSHBTYIZDN4u27Nfo5WA+GTvFyeNsO6tNNWlYfRHSBtnm6SZDY/5i4J | ||
| fxP2JY0waM81KRvuHTazY571lHM/TTvFDRUX5nvHIu7GToBKahfVLf26NJuTZYXR | ||
| 5c09GAXBAoGBAO7a9M/dvS6eDhyESYyCjP6w61jD7UYJ1fudaYFrDeqnaQ857Pz4 | ||
| BcKx3KMmLFiDvuMgnVVj8RToBGfMV0zP7sDnuFRJnWYcOeU8e2sWGbZmWGWzv0SD | ||
| +AhppSZThU4mJ8aa/tgsepCHkJnfoX+3wN7S9NfGhM8GDGxTHJwBpxINAoGBAOO4 | ||
| ZVtn9QEblmCX/Q5ejInl43Y9nRsfTy9lB9Lp1cyWCJ3eep6lzT60K3OZGVOuSgKQ | ||
| vZ/aClMCMbqsAAG4fKBjREA6p7k4/qaMApHQum8APCh9WPsKLaavxko8ZDc41kZt | ||
| hgKyUs2XOhW/BLjmzqwGryidvOfszDwhH7rNVmRtAoGBALYGdvrSaRHVsbtZtRM3 | ||
| imuuOCx1Y6U0abZOx9Cw3PIukongAxLlkL5G/XX36WOrQxWkDUK930OnbXQM7ZrD | ||
| +5dW/8p8L09Zw2VHKmb5eK7gYA1hZim4yJTgrdL/Y1+jBDz+cagcfWsXZMNfAZxr | ||
| VLh628x0pVF/sof67pqVR9UhAoGBAMcQiLoQ9GJVhW1HMBYBnQVnCyJv1gjBo+0g | ||
| emhrtVQ0y6+FrtdExVjNEzboXPWD5Hq9oKY+aswJnQM8HH1kkr16SU2EeN437pQU | ||
| zKI/PtqN8AjNGp3JVgLioYp/pHOJofbLA10UGcJTMpmT9ELWsVA8P55X1a1AmYDu | ||
| y9f2bFE5AoGAdjo95mB0LVYikNPa+NgyDwLotLqrueb9IviMmn6zKHCwiOXReqXD | ||
| X9slB8RA15uv56bmN04O//NyVFcgJ2ef169GZHiRFIgIy0Pl8LYkMhCYKKhyqM7g | ||
| xN+SqGqDTKDC22j00S7jcvCaa1qadn1qbdfukZ4NXv7E2d/LO0Y2Kkc= | ||
| -----END RSA PRIVATE KEY----- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from unittest import mock | ||
|
|
||
| import importlib_resources | ||
| import jwt | ||
| import pytest | ||
|
|
||
| from gidgethub import apps | ||
| from .test_abc import MockGitHubAPI | ||
|
|
||
| from .samples import rsa_key as rsa_key_samples | ||
|
|
||
|
|
||
| class TestGitHubAppUtils: | ||
|
|
||
| """Tests for GitHub App utilities.""" | ||
|
|
||
| @mock.patch("time.time") | ||
| def test_get_jwt(self, time_mock): | ||
| app_id = 12345 | ||
|
|
||
| time_mock.return_value = 1587069751.5588422 | ||
|
|
||
| # test file copied from https://github.com/jpadilla/pyjwt/blob/master/tests/keys/testkey_rsa | ||
| private_key = importlib_resources.read_binary(rsa_key_samples, "test_rsa_key") | ||
|
|
||
| result = apps.get_jwt(app_id=app_id, private_key=private_key) | ||
| expected_payload = { | ||
| "iat": 1587069751, | ||
| "exp": 1587069751 + (10 * 60), | ||
| "iss": app_id, | ||
| } | ||
|
|
||
| assert result == jwt.encode( | ||
| expected_payload, private_key, algorithm="RS256" | ||
| ).decode("utf-8") | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_installation_access_token(self): | ||
| gh = MockGitHubAPI() | ||
| installation_id = 6789 | ||
| app_id = 12345 | ||
|
|
||
| private_key = importlib_resources.read_binary(rsa_key_samples, "test_rsa_key") | ||
|
|
||
| await apps.get_installation_access_token( | ||
| gh, installation_id=installation_id, app_id=app_id, private_key=private_key | ||
| ) | ||
|
|
||
| assert gh.url == "https://api.github.com/app/installations/6789/access_tokens" | ||
| assert gh.body == b"" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In octomachinery, I used to have it as a function but then I implemented an object that represents a private key with fingerprint pinning and it turned out that it's handy to have it as a method:
https://github.com/sanitizers/octomachinery/blob/6b41c1df510f8bbf090ea290941261396e2cb559/octomachinery/github/models/private_key.py