Skip to content

feat(webauth): honor a CLI-requested token lifetime#938

Open
tomaskir wants to merge 1 commit into
phasehq:mainfrom
tomaskir:feat/webauth-token-lifetime
Open

feat(webauth): honor a CLI-requested token lifetime#938
tomaskir wants to merge 1 commit into
phasehq:mainfrom
tomaskir:feat/webauth-token-lifetime

Conversation

@tomaskir

@tomaskir tomaskir commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Overview

PATs minted through the CLI webauth login flow always had infinite lifetime. The approval page hardcoded the token expiry to null when calling generateUserToken(...), even though per-token expiry is supported everywhere else:

  • CreateUserTokenMutation accepts expiry: BigInt (backend/backend/graphene/mutations/environment.py)
  • UserToken.expires_at exists and is nullable (backend/api/models.py)
  • Expiry is enforced on auth (backend/api/utils/rest.py, token_is_expired_or_deleted)
  • The normal "Create token" dialog already lets users pick an expiry

The webauth path was the only token-creation flow that could not set an expiry. Fixing it requires reworking the webauth request payload to be parse-safe, which is the same migration two CLI flags depend on (--token-lifetime and --token-name). This PR is the Console half; the CLI side is phasehq/cli#302 and phasehq/cli#303.

Proposed Changes

Rework the webauth payload parsing to a parse-safe format and honor a CLI-requested token lifetime. No backend change is needed.

  • New frontend/utils/webAuth.ts:
    • parseWebAuthRequest(decoded) accepts a base64(JSON) payload { port, publicKey, name, lifetime? }:
      • name is the requested token name, parsed as a string so arbitrary, user-supplied names (hyphens, spaces, etc.) survive intact - this is the format dependency for the CLI --token-name flag (cli#303).
      • lifetime is the requested lifetime in seconds (omitted / null / non-positive = never expires) - drives the CLI --token-lifetime flag (cli#302).
      • Falls back to the legacy port-pubKeyHex-patName format for older CLIs. The legacy parse now keeps everything after the second hyphen as the name (params.slice(2).join('-')) instead of params[2], which fixes the truncation bug (Webauth PAT names are truncated at the first hyphen #937).
    • expiryFromLifetime(lifetime) converts the requested lifetime (seconds) into an absolute millisecond expiry timestamp, as expected by generateUserToken / CreateUserTokenMutation.
  • frontend/utils/tokens.ts: add humanReadableExpiryTimestamp(expiry) to render an absolute-timestamp expiry, mirroring the existing humanReadableExpiry.
  • frontend/app/webauth/[requestCode]/page.tsx: use parseWebAuthRequest; compute the expiry at mint time and pass it to generateUserToken instead of hardcoded null; show the resulting expiry on the approval screen. Removed the inline WebAuthRequestParams interface and getWebAuthRequestParams helper (moved to the util).

No requested lifetime preserves the current never-expires behavior; no requested name preserves the CLI's username@hostname default.

Cross-repo contract: the base64(JSON) payload shape documented in webAuth.ts is what the CLI side (cli#302, cli#303) must emit. Adding this one format migration unblocks both CLI flags.

Screenshots or Demo

Visible change: on the CLI authentication ("in progress") screen, a line now shows the resulting token expiry, e.g. "This token will expire on 7/5/2026." or "This token will never expire." when no lifetime was requested.

Release Notes

The CLI login flow (phase auth webauth) can now request a token lifetime, and the Console honors it - the minted personal access token expires accordingly instead of always living forever. Logins that do not request a lifetime are unchanged (non-expiring). The webauth payload is also now parse-safe, which fixes PAT names being truncated at the first hyphen and lets the CLI send a custom token name. Requires a CLI version that sends the new payload (phasehq/cli#302, phasehq/cli#303); older CLIs keep working unchanged.

Open Questions

Testing

  • frontend/tests/utils/webAuth.test.ts (new): parseWebAuthRequest across JSON and legacy payloads, lifetime present/absent/non-positive, hyphenated token names (the Webauth PAT names are truncated at the first hyphen #937 case), and the non-object-JSON fallback guard; plus expiryFromLifetime (null, zero, future timestamp).
  • frontend/tests/utils/tokens.test.ts (new): humanReadableExpiryTimestamp null and timestamp branches.
  • 12 tests passing (yarn test tests/utils/webAuth.test.ts tests/utils/tokens.test.ts). Lint and tsc --noEmit clean for the changed files, and next build succeeds (/webauth/[requestCode] compiles).
  • Smoke test against a local dev stack: GET /webauth/<base64(JSON)> serves and redirects unauthenticated users to /login with the payload preserved intact.
  • Gap: the page.tsx wiring is not unit-tested - this repo has no page/component test pattern (jest.config.js roots to tests/, which holds pure-util tests), so the logic was extracted into testable utils instead. Full end-to-end verification (login -> approve -> mint) needs a running CLI + Console.

Reviewer Focus

Start at frontend/utils/webAuth.ts - parseWebAuthRequest (the payload contract, legacy fallback, and arbitrary-name handling) is the core of the change and the cross-repo interface shared by both CLI flags. Then frontend/app/webauth/[requestCode]/page.tsx for how the expiry is computed and passed to generateUserToken.

Additional Context

How to Test the Changes Locally

  1. Run the dev stack (docker compose --env-file .env.dev -f dev-docker-compose.yml up -d) and sign in.
  2. With a CLI build from feat: allow requesting a token with a specific lifetime during phase auth cli#302, run phase auth --token-lifetime 7d and complete the webauth flow; confirm the approval screen shows the expiry and the created PAT carries expires_at.
  3. With a CLI build from feat: allow setting the token name during phase auth cli#303, run phase auth --token-name "ci-prod-api" and confirm the PAT is created with that exact name (including hyphens).
  4. Without those CLI builds (legacy payload), run a normal phase auth webauth login and confirm the token is still created, never-expiring, and its username@hostname name is no longer truncated at the first hyphen.
  5. Manual payload check without the CLI: base64-encode {"port":8002,"publicKey":"<hex>","name":"user@my-host","lifetime":604800} and open /webauth/<b64>; verify the full name is used and the expiry line renders.

Did You...

  • Ensure linting passes (code style checks)?
  • Update dependencies and lockfiles (if required) - N/A (no dependency or lockfile change)
  • Update migrations (if required) - N/A (no backend change)
  • Regenerate graphql schema and types (if required) - N/A (no backend GraphQL change)
  • Verify the app builds locally? - next build succeeds; /webauth/[requestCode] compiles
  • Manually test the changes on different browsers/devices? - route smoke-tested (serves, redirects to login with payload intact); parsing/expiry logic covered by unit tests

The CLI webauth login flow always minted non-expiring PATs because the
approval page hardcoded the token expiry to null. Let the CLI request a
token lifetime in the webauth payload and mint the PAT with that expiry.

- Add parseWebAuthRequest: accept a base64(JSON) payload
  { port, publicKey, name, lifetime? } where lifetime is in seconds, with
  a fallback to the legacy port-pubKeyHex-patName format. The legacy parse
  also stops truncating token names that contain hyphens.
- Add expiryFromLifetime to convert the requested lifetime (seconds) into
  an absolute millisecond expiry passed to generateUserToken.
- Show the resulting token expiry on the approval screen.
- No requested lifetime keeps the current never-expires behavior.

The backend CreateUserTokenMutation already supports per-token expiry, so
no backend change is needed. CLI side tracked in phasehq/cli#302.

Closes phasehq#928
@tomaskir tomaskir marked this pull request as ready for review June 28, 2026 18:47
tomaskir added a commit to tomaskir/cli that referenced this pull request Jun 28, 2026
phase auth (webauth mode) always minted a non-expiring PAT named
username@hostname, with no way to override either. Add two optional flags
and send the values to the Console in the webauth request payload:

- --token-name sets the PAT name (default username@hostname). This fixes
  unhelpful names under Docker, where the default becomes root@<container-id>.
- --token-lifetime sets the PAT lifetime, e.g. 7d, 12h, 30m, 60s, 2w
  (default: never expires).

The webauth payload moves from the hyphen-joined port-pubKeyHex-patName
string to base64(JSON) { port, publicKey, name, lifetime? }, where lifetime
is in seconds. This is parse-safe for names containing hyphens or other
characters and matches the Console webauth page contract.

Add util.ParseTokenLifetime to convert a lifetime string into seconds.

Console side: phasehq/console#928, phasehq/console#937 (PR phasehq/console#938).

Closes phasehq#302
Closes phasehq#303
@tomaskir

Copy link
Copy Markdown
Contributor Author

Coordinated CLI side: phasehq/cli#304 (adds the --token-name and --token-lifetime flags and emits the base64(JSON) webauth payload this PR parses).

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.

Webauth PAT names are truncated at the first hyphen feat: honor a CLI-requested token lifetime in the webauth login flow

1 participant