Skip to content

Authenticate to the Certora cloud without a browser - #166

Draft
shellygr wants to merge 3 commits into
shelly/codegen-multi-specfrom
shelly/prover-auth-noninteractive
Draft

Authenticate to the Certora cloud without a browser#166
shellygr wants to merge 3 commits into
shelly/codegen-multi-specfrom
shelly/prover-auth-noninteractive

Conversation

@shellygr

Copy link
Copy Markdown
Contributor

Stacked on #155#148. Retarget to master as those merge.

Problem

A headless run that needed to refresh its cloud session spent five minutes waiting for a login nobody could complete, then failed:

Running prover with args: ... --server production --wait_for_results none
AuthenticationError: PKCE login deadline of 300.0s expired before a callback completed.

certora_login.login completes a missing or stale session with the PKCE browser flow. In a container that can never succeed — the link is never opened and the callback server waits out its deadline. CERTORA_LOGIN_NO_BROWSER, which the compose file already set, only suppresses the webbrowser.open call; the wait is unaffected.

Fix

login takes no_pkce, which removes only the fallback — the refresh path is untouched:

credentials = get_credentials()
if credentials:
    credentials = _who_am_i(credentials, ...)      # refresh — unchanged
if not credentials and not resolved_no_pkce:
    credentials = pkce_login(...)                  # the 300s wait
if not credentials:
    raise CertoraLoginRefreshError(...)            # what we get instead

New composer/prover/auth.py exposes ensure_prover_login() and a prover_output_api() factory — the same shape PreAudit uses (prover_api_factory.py) — and turns a failure into a message naming the command a human has to run, since retrying never fixes an expired token.

It is applied as an environment default rather than an argument because ProverOutputAPI logs in on its own: its constructor authenticates, and it authenticates again after a 401. Neither call is ours to pass arguments to, and both read the same variable. setdefault leaves an operator who exports CERTORA_LOGIN_NO_PKCE=0 in charge, so a host run can still use the browser.

Both construction sites go through the factory — composer/prover/cloud.py and composer/spec/source/report_prover.py, the latter of which would have hung independently — and the cloud path in run_prover refreshes before submitting, so a dead session costs no prover minutes. The compose service sets the variable too, for subprocesses that never import composer.

CI is deliberately exempt. ProverOutputUtility does not log in there — get_auth_cookies returns an empty jar under CI and the API uses AWS SigV4 instead — and the nightly integration job has AWS OIDC credentials with no credentials file. ensure_prover_login mirrors that precondition rather than insisting on a login that would never have happened.

Verification

In the container, against real credentials:

scenario before after
valid session 300 s, fails 2.2 s, OK — refreshed token written back through the mount
no credentials 300 s, fails 0.3 s, fails with run 'certora-cloud login' on the host

tests/test_prover_auth.py — 6 tests: refresh-only, operator override, CI no-op, once-per-process, message content, login-before-construct. Green both normally and with CI=true (the fixture clears CI, or every assertion here would silently pass under GitHub Actions while testing nothing).

The second commit fixes a regression the first one introduced; it was the one finding of seventeen that survived adversarial review.

shellygr and others added 3 commits August 14, 2026 15:03
A headless run that needed to refresh its cloud session spent five minutes
waiting for a login nobody could complete:

    AuthenticationError: PKCE login deadline of 300.0s expired before a
    callback completed.

`certora_login.login` completes a missing or stale session with the PKCE browser
flow. In a container that cannot succeed: the link is never opened and the
callback server waits out its deadline. `CERTORA_LOGIN_NO_BROWSER`, which the
compose file already set, only suppresses the `webbrowser.open` call -- the wait
is unaffected.

`login` takes `no_pkce`, which removes only the fallback: credentials are still
read and refreshed, and an unusable session raises instead of waiting. New
`composer/prover/auth.py` sets it and turns the failure into a message naming the
command a human has to run, since no amount of retrying fixes an expired token.

It is applied as an environment default rather than an argument because
`ProverOutputAPI` logs in on its own -- its constructor authenticates, and it
authenticates again after a 401 -- and neither call is ours to pass arguments to.
`setdefault` leaves an operator who exports `CERTORA_LOGIN_NO_PKCE=0` in charge,
so a host run can still use the browser. The compose service sets it too, for
subprocesses that never import composer.

Both construction sites now go through the factory, and the cloud path refreshes
before submitting a job rather than after it has run, so a dead session costs no
prover time.

Verified in the container against real credentials: refresh succeeds in 2.2s;
with no credentials present it fails in 0.3s with the hint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProverOutputUtility deliberately does not log in under CI. `get_auth_cookies`
returns an empty jar (`if os.getenv("CI"): return cookies`) and `ProverOutputAPI`
authenticates to Lambda with SigV4 instead of cookies. The nightly integration
job matches that shape exactly: AWS OIDC credentials and CERTORAKEY, no
CERTORA_USER/TOKEN/REFRESH_TOKEN and no ~/.certora/credentials.json.

So the eager login added in the previous commit would have failed that job at the
gate -- `get_credentials()` finds nothing, NO_PKCE removes the fallback, and
`login` raises -- before a prover job was ever submitted, in a run that
previously fetched its results through AWS and never touched certora_login.
`test_autoprove_integration.py` runs with cloud=True, so the nightly would have
broken while the fast suite stayed green.

Mirror the precondition instead: no-op under CI. Where AWS credentials are
absent, the run fails later on empty cookies exactly as it did before.

The test fixture now clears CI too. GitHub Actions sets it for the fast suite as
well, and without that every login assertion in this file would quietly become a
no-op that passes while testing nothing.

Found by adversarial review of the previous commit; it was the one finding of
seventeen that survived refutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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