feat(backend): Firebase ID-token verification (x509 certs, no Admin SDK) - #33
Merged
Conversation
Standalone src/cinemory/auth.py: verify_firebase_id_token() + the uid_from_authorization() header-parsing helper. RS256 JWT verification against Google's public x509 signing certs (securetoken endpoint), with no Firebase Admin SDK and no service-account secret. Checks iss/aud/exp/ iat/auth_time/sub; the accepted algorithm is hardcoded to RS256 (never taken from the token's own header) to block alg:none and RS256/HS256 algorithm-confusion forgeries. The Google-cert fetch sits behind an injectable key_source seam so tests run fully offline against a locally generated RSA keypair. Not wired into api.py or any endpoint yet — that follows in a later PR.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/cinemory/auth.py:verify_firebase_id_token()verifies a Firebase ID token (RS256 JWT) against Google's public x509 signing certs — no Firebase Admin SDK, no service-account secret.uid_from_authorization()parses aBearer <token>header (absent/empty ->Noneguest case; present-but-invalid -> raises).uid_from_authorizationinto the API so the tenant id is derived only from a cryptographically verified token, never a client-supplied field.api.pyor any endpoint in this PR — strictly additive: one new module + one new test file + two dependency-list additions (pyjwt,cryptography, added the same way existing deps are declared inrequirements.txt/pyproject.toml). No existing file's behavior changes.Verification checks (all mandatory — any failure raises
AuthError)kid.iss == "https://securetoken.google.com/<project_id>".aud == project_id.expin the future;iatandauth_timein the past (small clock-skew leeway);auth_timerequired present.subpresent and non-empty (returned as theuid).Algorithm-confusion defence
The accepted algorithm is
RS256, hardcoded — never taken from the token's ownalgheader or any caller input. The header'salgis checked against that hardcoded value before any key material is looked up, so both analg: nonetoken and an HS256 token forged using the RSA public key bytes as the HMAC secret (the classic RS256->HS256 downgrade) are rejected at that gate, before the signature is even read.Offline test seam
key_source: Callable[[], Mapping[str, str]]is injectable. The default implementation fetches + caches Google's certs over HTTPS (real-network code isolated in one pragma-excluded function, mirroringcinemory.connectors._http.RequestsTransport). Tests inject a fake backed by a locally generated 2048-bit RSA keypair wrapped in a self-signed x509 cert (same shape Google's endpoint returns) — fully offline, no network in CI.Adversarial tests (
tests/unit/test_auth.py, ~40 cases)Valid token; expired; future
iat/auth_time; missing/non-numericauth_time; clock-skew-within-leeway accepted; wrongaud; wrongiss; missingaud; tampered payload (signature no longer matches); signed by a completely different key while claiming the legitimatekid;alg: none(hand-crafted, empty signature segment); HS256-confusion (hand-crafted, HMAC-signed using the RSA public cert's PEM bytes as the secret); unsupported algorithm (ES256); missing/emptysub; missing/unknownkid; garbage/empty token; missingproject_id(and env-var default / override precedence); brokenkey_source; invalid certificate content; and the fulluid_from_authorizationmatrix (no header -> guest, case-insensitive bearer, malformed headers, invalid/expired token -> raises). Plus pure-helper coverage for theCache-Control: max-ageparser and both branches (hit/miss) of the defaultkey_sourcecache.Test plan