mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
* feat: TTL and revocation handling for cached GitHub OAuth tokens [closes AB-2322] Persist github_token_expires_at alongside github_token_encrypted, treat expired cache entries as missing so we re-resolve before kicking off runs, and invalidate the cached ciphertext on a downstream 401 so the next invocation gets a fresh token instead of replaying a revoked one. Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> * webapp: forward installation-token expiry to reviewer cache writes The three reviewer-thread persist sites in webapp.py were calling get_github_app_installation_token() (no expiry) and persist_encrypted_github_token without expires_at, so cached App tokens were treated as never-expiring even though they actually expire in ~1 hour. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
67 lines
2.4 KiB
Python
67 lines
2.4 KiB
Python
"""GitHub App installation token generation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
|
|
import httpx
|
|
import jwt
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
GITHUB_APP_ID = os.environ.get("GITHUB_APP_ID", "")
|
|
GITHUB_APP_PRIVATE_KEY = os.environ.get("GITHUB_APP_PRIVATE_KEY", "")
|
|
GITHUB_APP_INSTALLATION_ID = os.environ.get("GITHUB_APP_INSTALLATION_ID", "")
|
|
|
|
|
|
def _generate_app_jwt() -> str:
|
|
"""Generate a short-lived JWT signed with the GitHub App private key."""
|
|
now = int(time.time())
|
|
payload = {
|
|
"iat": now - 60, # issued 60s ago to account for clock skew
|
|
"exp": now + 540, # expires in 9 minutes (max is 10)
|
|
"iss": GITHUB_APP_ID,
|
|
}
|
|
private_key = GITHUB_APP_PRIVATE_KEY.replace("\\n", "\n")
|
|
return jwt.encode(payload, private_key, algorithm="RS256")
|
|
|
|
|
|
async def get_github_app_installation_token() -> str | None:
|
|
"""Exchange the GitHub App JWT for an installation access token.
|
|
|
|
Returns:
|
|
Installation access token string, or None if unavailable.
|
|
"""
|
|
token, _ = await get_github_app_installation_token_with_expiry()
|
|
return token
|
|
|
|
|
|
async def get_github_app_installation_token_with_expiry() -> tuple[str | None, str | None]:
|
|
"""Exchange the GitHub App JWT for an installation access token and its expiry.
|
|
|
|
Returns ``(token, expires_at)`` where ``expires_at`` is the ISO-8601 string
|
|
returned by GitHub (typically 1 hour out). Either value may be ``None``.
|
|
"""
|
|
if not GITHUB_APP_ID or not GITHUB_APP_PRIVATE_KEY or not GITHUB_APP_INSTALLATION_ID:
|
|
logger.debug("GitHub App env vars not fully configured, skipping app token")
|
|
return None, None
|
|
|
|
try:
|
|
app_jwt = _generate_app_jwt()
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
f"https://api.github.com/app/installations/{GITHUB_APP_INSTALLATION_ID}/access_tokens",
|
|
headers={
|
|
"Authorization": f"Bearer {app_jwt}",
|
|
"Accept": "application/vnd.github+json",
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return data.get("token"), data.get("expires_at")
|
|
except Exception:
|
|
logger.exception("Failed to get GitHub App installation token")
|
|
return None, None
|