Skip to content

feat(backend): Firebase ID-token verification (x509 certs, no Admin SDK) - #33

Merged
upgradedev merged 1 commit into
mainfrom
feat/firebase-auth-verify
Jul 27, 2026
Merged

feat(backend): Firebase ID-token verification (x509 certs, no Admin SDK)#33
upgradedev merged 1 commit into
mainfrom
feat/firebase-auth-verify

Conversation

@upgradedev

Copy link
Copy Markdown
Owner

Summary

  • Standalone 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 a Bearer <token> header (absent/empty -> None guest case; present-but-invalid -> raises).
  • This is the security root for Cinemory's coming multitenancy: a later PR will wire uid_from_authorization into the API so the tenant id is derived only from a cryptographically verified token, never a client-supplied field.
  • Not wired into api.py or 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 in requirements.txt / pyproject.toml). No existing file's behavior changes.

Verification checks (all mandatory — any failure raises AuthError)

  • RS256 signature against Google's public cert for the token's kid.
  • iss == "https://securetoken.google.com/<project_id>".
  • aud == project_id.
  • exp in the future; iat and auth_time in the past (small clock-skew leeway); auth_time required present.
  • sub present and non-empty (returned as the uid).

Algorithm-confusion defence

The accepted algorithm is RS256, hardcoded — never taken from the token's own alg header or any caller input. The header's alg is checked against that hardcoded value before any key material is looked up, so both an alg: none token 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, mirroring cinemory.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-numeric auth_time; clock-skew-within-leeway accepted; wrong aud; wrong iss; missing aud; tampered payload (signature no longer matches); signed by a completely different key while claiming the legitimate kid; 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/empty sub; missing/unknown kid; garbage/empty token; missing project_id (and env-var default / override precedence); broken key_source; invalid certificate content; and the full uid_from_authorization matrix (no header -> guest, case-insensitive bearer, malformed headers, invalid/expired token -> raises). Plus pure-helper coverage for the Cache-Control: max-age parser and both branches (hit/miss) of the default key_source cache.

Test plan

  • CI green on this PR (lint, unit+integration+e2e with coverage gate, pen-test suite, dependency audit, readiness gate, secret scan)
  • Reviewer sign-off before merge (security-critical — not merging without explicit approval)

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.
@upgradedev
upgradedev merged commit a8ea121 into main Jul 27, 2026
12 checks passed
@upgradedev
upgradedev deleted the feat/firebase-auth-verify branch July 27, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant