cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
root / app / gh_oidc.py

gh_oidc.py (3126B)


      1 """
      2 Verifies GitHub Actions OIDC ID tokens so the backend can trust *who* is
      3 pushing without any shared secret living in contributor repos.
      4 
      5 Why this instead of a PAT/App token per contributor:
      6 - The token is minted per-workflow-run by GitHub itself and is only valid
      7   for ~5 minutes.
      8 - Its `repository_owner` / `repository` / `ref` claims are set by GitHub's
      9   own infrastructure from the run's real context - a workflow author cannot
     10   edit their own workflow YAML to make GitHub sign a token claiming to be a
     11   different repo or a different owner.
     12 - We never see (or need) a long-lived credential from the contributor.
     13 
     14 Docs: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
     15 """
     16 
     17 import os
     18 import time
     19 import httpx
     20 import jwt
     21 from jwt import PyJWKClient
     22 from fastapi import HTTPException
     23 
     24 GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com"
     25 JWKS_URL = f"{GITHUB_OIDC_ISSUER}/.well-known/jwks"
     26 # The `audience` your workflows request in core.getIDToken(...). Self-hosters: set OIDC_AUDIENCE
     27 # to your own CDN hostname. The default keeps the original deployment working unchanged.
     28 EXPECTED_AUDIENCE = os.getenv("OIDC_AUDIENCE", "cdn.amit.is-a.dev")
     29 
     30 _jwks_client = None
     31 _jwks_client_ts = 0
     32 _JWKS_CLIENT_TTL = 3600
     33 
     34 
     35 def _get_jwks_client() -> PyJWKClient:
     36     global _jwks_client, _jwks_client_ts
     37     now = time.time()
     38     if _jwks_client is None or now - _jwks_client_ts > _JWKS_CLIENT_TTL:
     39         _jwks_client = PyJWKClient(JWKS_URL)
     40         _jwks_client_ts = now
     41     return _jwks_client
     42 
     43 
     44 def verify_actions_token(token: str) -> dict:
     45     """Verify a GitHub Actions OIDC token and return its trusted claims.
     46 
     47     Raises HTTPException(401) if the token is missing, expired, wrongly
     48     scoped, or fails signature verification.
     49     """
     50     if not token:
     51         raise HTTPException(status_code=401, detail="Missing GitHub Actions OIDC token.")
     52 
     53     try:
     54         signing_key = _get_jwks_client().get_signing_key_from_jwt(token)
     55         claims = jwt.decode(
     56             token,
     57             signing_key.key,
     58             algorithms=["RS256"],
     59             audience=EXPECTED_AUDIENCE,
     60             issuer=GITHUB_OIDC_ISSUER,
     61             options={"require": ["exp", "iat", "repository", "repository_owner"]},
     62         )
     63     except jwt.PyJWTError as e:
     64         raise HTTPException(status_code=401, detail=f"Invalid GitHub Actions token: {e}")
     65 
     66     repository = claims.get("repository", "")
     67     owner = claims.get("repository_owner", "")
     68     if "/" not in repository:
     69         raise HTTPException(status_code=401, detail="Malformed token claims.")
     70 
     71     repo_owner_from_full, repo_name = repository.split("/", 1)
     72     if repo_owner_from_full != owner or not owner or not repo_name:
     73         raise HTTPException(status_code=401, detail="Token owner/repository mismatch.")
     74 
     75     return {
     76         "owner": owner,
     77         "owner_id": str(claims.get("repository_owner_id", "")),
     78         "repo": repo_name,
     79         "actor": claims.get("actor", ""),
     80         "ref": claims.get("ref", ""),
     81         "workflow_ref": claims.get("job_workflow_ref", ""),
     82     }