-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_app.py
More file actions
130 lines (110 loc) · 3.69 KB
/
Copy pathgithub_app.py
File metadata and controls
130 lines (110 loc) · 3.69 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
# Copyright (c) 2026 Omar Rao
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
# Available under the GNU Affero General Public License v3.0, or under a
# separate commercial license. See LICENSE and COMMERCIAL-LICENSE.md.
"""
GitHub App authentication — JWT-based token exchange and webhook verification.
"""
import hashlib
import hmac
import time
from typing import Optional
import jwt
import requests
def generate_jwt(app_id: str, private_key_pem: str) -> str:
"""
Create a GitHub App JWT valid for 10 minutes.
Args:
app_id: GitHub App ID (from App settings page).
private_key_pem: PEM-encoded RSA private key string.
Returns:
Signed JWT string.
"""
now = int(time.time())
payload = {
"iat": now - 60, # issued 60s ago to account for clock skew
"exp": now + (10 * 60), # expires in 10 minutes
"iss": str(app_id),
}
token = jwt.encode(payload, private_key_pem, algorithm="RS256")
# PyJWT >=2.0 returns str directly; <2.0 returns bytes
if isinstance(token, bytes):
token = token.decode("utf-8")
return token
def get_installation_token(
app_id: str, private_key_pem: str, installation_id: str
) -> str:
"""
Exchange a GitHub App JWT for an installation access token.
Args:
app_id: GitHub App ID.
private_key_pem: PEM-encoded RSA private key.
installation_id: GitHub App installation ID.
Returns:
Installation access token string.
"""
jwt_token = generate_jwt(app_id, private_key_pem)
resp = requests.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers={
"Authorization": f"Bearer {jwt_token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
timeout=15,
)
resp.raise_for_status()
return resp.json()["token"]
def verify_webhook_signature(payload: bytes, secret: str, signature: str) -> bool:
"""
Verify a GitHub webhook HMAC-SHA256 signature.
Args:
payload: Raw request body bytes.
secret: Webhook secret string.
signature: Value of X-Hub-Signature-256 header.
Returns:
True if signature is valid.
"""
if not signature.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
class GitHubAppWebhookServer:
"""
Webhook server that uses GitHub App installation token auth.
Extends the core webhook server logic from webhook.py.
"""
def __init__(
self,
app_id: str,
private_key_pem: str,
installation_id: str,
port: int = 8080,
secret: str = "",
out_dir: str = "reports",
):
self.app_id = app_id
self.private_key_pem = private_key_pem
self.installation_id = installation_id
self.port = port
self.secret = secret
self.out_dir = out_dir
self._token: Optional[str] = None
def get_token(self) -> str:
"""Return a fresh installation token (re-fetches on demand)."""
self._token = get_installation_token(
self.app_id, self.private_key_pem, self.installation_id
)
return self._token
def run(self) -> None:
"""Start the webhook server using an installation token."""
token = self.get_token()
from webhook import run_webhook_server
run_webhook_server(
port=self.port,
secret=self.secret,
out_dir=self.out_dir,
github_token=token,
)