-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_app.py
More file actions
125 lines (106 loc) · 3.46 KB
/
Copy pathgithub_app.py
File metadata and controls
125 lines (106 loc) · 3.46 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
"""
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,
)