Skip to content

Authenticate to the Certora cloud without a browser - #166

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

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

Conversation

@shellygr

@shellygr shellygr commented Aug 14, 2026

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.

Also here: mounting the work directory at its host path

Every argument to a containerized run had to be rewritten from the path you see in your shell to the path the container sees, so a command that works on the host fails in the container and a copied path is silently wrong rather than missing.

The work directory is now mounted twice, at /work and at the path it has on the host:

- ${HOST_WORK_DIR:-${PWD}}:/work
- ${HOST_WORK_DIR:-${PWD}}:${HOST_WORK_DIR:-${PWD}}

A bind mount is a view of a directory rather than a copy, so both spellings address the same files and outputs land in the same place either way. /work still works, so existing invocations and scripts are unaffected. Set HOST_WORK_DIR to a directory containing your projects and host paths beneath it can go on the command line unchanged.

It rides along here because it is the other half of making a containerized run work without hand-translation.

shellygr and others added 6 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>
Every argument to a containerized run has to be rewritten from the path you see
in your shell to the path the container sees, so a command that works on the host
fails in the container and a copied path is silently wrong rather than missing.

Mounting the same directory twice, once at /work and once at the path it has on
the host, makes both spellings work. A bind mount is a view of a directory rather
than a copy, so the two paths address the same files and outputs land in the same
place either way.

Set HOST_WORK_DIR to a directory containing the projects you work on and host
paths beneath it can go on the command line unchanged. /work keeps working, so
existing invocations and scripts are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port 5432 is the first thing anything postgres-shaped grabs, so on a machine that
already runs one the container never starts. Compose reports the collision once
and then leaves it in Created, which reads as "nothing happened" rather than as
an error, and the next command fails against a database that was never up.

Publish on 5454 instead, overridable with COMPOSER_DB_HOST_PORT. The in-network
port is untouched at 5432, so the autoprove service and everything inside the
container are unaffected. Host tooling talking to the database directly needs
CERTORA_AI_COMPOSER_PGPORT=5454.

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