Skip to content

feat: browser sign-in for pgedge starfleet - #73

Draft
AntTheLimey wants to merge 10 commits into
mainfrom
feat/browser-login
Draft

AntTheLimey wants to merge 10 commits into
mainfrom
feat/browser-login

Conversation

@AntTheLimey

Copy link
Copy Markdown
Member

What changed

  • pgedge starfleet auth login with no credential flags signs in through the browser. It uses Auth0 authorization code with PKCE (S256) and a 127.0.0.1 loopback on 8484-8486. The CLI then calls the API as the user, with the user's access token in the 0600 token cache. Nothing is created server-side.
  • API clients are unchanged: --client-id/--client-secret, the env pair, and profiles holding a client. --api-client brings back the interactive ID and secret prompt.
  • Tenants. A browser session sends X-pgEdge-Selected-Tenant on every request. Login asks which tenant when there are several (on a TTY), or takes --tenant. --tenant on any starfleet command switches tenant for that command, validated against the token's pgedge_tenant_ids. API clients never send the header, and --tenant with one is a usage error.
  • Expiry. An expired session fails with exit 5 and "run pgedge starfleet auth login", never a silent re-mint. auth status, whoami, doctor and logout understand sessions.
  • The Auth0 settings table is internal/apidefaults/auth0.go. Both client IDs are empty until saas#2040 is applied, so browser sign-in reports "not available for this API" until then.
  • Docs, llms pages, the starfleet skill and the invite text now cover both sign-in kinds.

Checks

  • make test passes, with coverage at 90.5%. make lint, make docs-check and make lint-docs are clean.
  • Each of these was mutation-checked, and every mutation was caught:
    • the state check
    • the PKCE verifier and challenge
    • the tenant header leaking to an API client
    • flags over a session overwriting its cache
    • a forged callback ending the sign-in
  • An independent review found 4 Important issues and 5 Minor ones, all fixed in this PR:
    • A one-off --client-id on a session profile overwrote the session.
    • --debug printed the ID token. It is now redacted, along with the form-encoded code and code_verifier.
    • The invite text and its tripwire are corrected.
    • A forged loopback request no longer aborts sign-in.
    • Doctor no longer calls an expired session authenticated.
    • A stale profile tenant is no longer silently replaced.
    • The --tenant checks are fixed in status and doctor.
    • The keychain is no longer touched before the save.
  • The HTTP client site count is still 4. stderrArgSites went from 200 to 211, for the new sign-in messages.

Risks

  • Draft until the real client IDs exist. Merged earlier, plain auth login fails on both APIs while the docs say it opens the browser. Nothing has run against real Auth0 yet. The dev end-to-end test comes after saas#2040 is applied to dev.
  • auth status exits 5 for an expired or missing session, where API clients exit 0. A session can't renew itself.
  • --insecure-storage without an API client is now a usage error.
  • Step 2 (refresh tokens) and enabling invite create/accept for sessions are follow-ups.

Browser sign-in needs three building blocks below the commands: the
public Auth0 settings per API (domain, audience, client ID, in one
table), a reader for the tenant claims in a user's access token, and
a profile marker for a browser session with its tenant.

A session token's cache binding covers the API URL and the user under
its own domain, so neither kind of token can pass for the other.

Both client IDs are empty until the Auth0 app is applied, which makes
browser sign-in report that it is not available yet.
internal/starfleet/signin runs the authorization-code flow with PKCE
against Auth0: a loopback listener on the first free port of 8484,
8485 and 8486, a random state checked on return, an S256 challenge
over a 32-byte verifier, and the code exchange at /oauth/token.

The browser opener, the Auth0 base URL, the port list and the wait
are injected, so the tests drive the callback with real requests and
never open a browser or reach the network.
A profile marked as a browser session resolves to the user's cached
token instead of a client-credentials exchange. Every request of a
session carries X-pgEdge-Selected-Tenant: the profile's tenant, or
the new persistent --tenant flag for one command, checked locally
against the token's pgedge_tenant_ids. An API client never sends the
header, and --tenant with one is a usage error.

A session is never re-minted. An expired, missing or other-API token
fails with exit 5 and names the login command, including when a long
--wait outlives it. doctor's Auth and Tenant rows understand a
session and report its selected tenant.

conn.Resolve now takes conn.Flags, and conn.ReadFlags replaces the
three copies of the persistent-flag reader.
auth login with no credential flags now signs the user in through
the browser and saves the profile as a session: api_url, auth and
tenant_id, with any API client ID and keychain secret removed. The
token goes to the existing cache, bound to the API URL and the user,
with its real expiry.

A user in several tenants picks one at a prompt on a terminal, or
gets the account default with a note on stderr; --tenant takes an ID
or a name. --api-client keeps the old prompt for an ID and secret,
and --client-id/--client-secret are unchanged.

status, whoami and logout understand a session. whoami reports the
user and the selected tenant without listing API clients, and status
exits 5 for an expired session, which nothing can renew.
The guides, the starfleet reference and the pgedge-starfleet skill
now present browser sign-in as the default and an API client as the
path for CI, scripts and agents, with the session's tenant, --tenant,
and the exit 5 an expired session gives. The command reference is
regenerated for the new flags.
--debug printed the ID token from the sign-in exchange's response and
the authorization code and PKCE verifier from its form-encoded
request. id_token joins the secret field names, and a form-encoded
request body now has the values of code, code_verifier and every
secret field masked.
Any web page could end a sign-in by loading the loopback callback
with a wrong state. Such a request now gets a 400 and the sign-in
keeps waiting for the real one until its timeout.

The token exchange always gets its HTTP client from the caller, so
the unreachable fallback client is gone and the starfleet client
site count returns to 4. The redirect URI is built in one place and
tested against the three registered callbacks.
- Flag credentials over a browser-session profile no longer write
  their token over the session's cache; they resolve ephemerally, as
  the env pair does.
- A profile tenant_id the token no longer reaches is exit 2, naming
  auth login --tenant, instead of silently acting in another tenant.
  whoami and doctor report the tenant requests actually carry.
- auth status and doctor validate --tenant as resource commands do,
  and refuse it with an API client.
- doctor -o json reports an expired session as unauthenticated with
  the expired message, matching auth status.
- Browser login saves the profile before deleting an API client's
  keychain secret, so a failed save keeps a working profile.

The wait-past-expiry test moves an injected clock instead of
sleeping, and new tests cover the CLI-side PKCE pairing and --debug
output during a sign-in.
The invite refusal, its help and the reference said every CLI
credential is a client ID and secret, which browser sign-in made
false. They now say an API client is refused by the API and the CLI
does not send these requests for a browser sign-in either, without
promising invite support.

TestInviteGuardPremiseStillHolds now also runs both guarded verbs
with a browser session and fails if either stops refusing, naming
the follow-up that would change it.
Keep the tests that guard what a dev-tenant run would not reveal:
state and PKCE checks, the registered callback URIs, the tenant
header for sessions only, --debug redaction, flags over a saved
session, the API URL binding, the secret kept on a failed save and
the exit 5 expiry contract. Drop the tests of output wording, prompt
flow, duplicate variants and happy paths, and fold the timeout and
cancel cases into one table.
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