pingdotgg / pingdotgg/t3code

Session TTL is inverted vs credential strength: DPoP gets 1 hour, plain bearer gets 30 days

Open
#9,884 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Summary

Session lifetime is selected by whether the client presented a DPoP proof-of-possession key. The client that presents one gets 1 hour. The client that presents none — a plain bearer token — gets the default 30 days. The stronger credential is the short-lived one.

Steps to reproduce

Read apps/server/src/auth/EnvironmentAuth.ts:742-751, in exchangeBootstrapCredentialForAccessToken:

return yield* sessions
  .issue({
    method: input?.proofKeyThumbprint ? "dpop-access-token" : "bearer-access-token",
    subject: grant.subject,
    scopes: grantedScopes,
    ...(input?.proofKeyThumbprint
      ? {
          proofKeyThumbprint: input.proofKeyThumbprint,
          ttl: Duration.hours(1),
        }
      : {}),

ttl is inside the spread that only fires when proofKeyThumbprint is set. With no proof key the key is absent entirely, so SessionStore.issue falls through to its default:

apps/server/src/auth/SessionStore.ts:419

const DEFAULT_SESSION_TTL = Duration.days(30);

apps/server/src/auth/SessionStore.ts:625

milliseconds: Duration.toMillis(input?.ttl ?? DEFAULT_SESSION_TTL),

proofKeyThumbprint is populated only from a DPoP request header (apps/server/src/auth/http.ts:320-335), and the DPoP path is the cloud/relay-managed one — apps/mobile/src/connection/migration.ts:44 treats relayManaged === true || authenticationMethod === "dpop" as the same class, and the proof is built in apps/mobile/src/features/cloud/linkEnvironment.ts:531-540. A directly-paired client sends no DPoP header and therefore takes the 30-day branch.

Expected behavior

Session lifetime scales inversely with credential strength: a sender-constrained token, which cannot be replayed from another key, can afford a longer life than a bearer token, which anyone holding it can present.

Actual behavior

Exactly inverted:

client credential replayable by a holder TTL
relay / cloud-linked DPoP, sender-constrained no 1 hour
directly paired plain bearer yes 30 days
Impact

The 30-day bearer session is the one on a phone — the device most likely to be lost or stolen, and the one whose token is not bound to any key. It also carries whatever scopes the pairing link granted, which by default includes AuthTerminalOperateScope ("Create terminals and send input to running shells").

Revocation exists and works (Settings → Connections lists client sessions with a revoke action), so this is bounded by the user noticing and acting — but noticing is the whole gap that a short TTL is meant to cover.

The nearby TTL constants all carry a comment explaining the number (DESKTOP_BOOTSTRAP_TTL_HOURS, DEV_STARTUP_TTL_HOURS in PairingGrantStore.ts:245-258 are both well argued). Duration.hours(1) here has none, which makes me think the 1-hour value was chosen deliberately for DPoP and the 30-day default was simply what the other branch inherited, rather than a decision anyone made about paired phones.

Suggested fix

Set the bearer TTL explicitly rather than letting it fall through to the store default — whatever the right number is, it should be stated at this call site and not inherited. If 30 days is intentional for paired devices, a comment saying so would stop this reading as an oversight.

Version

d7cf8aaa (main).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with exchangeBootstrapCredentialForAccessToken in apps/server/src/auth/EnvironmentAuth.ts and compare its conditional TTL with DEFAULT_SESSION_TTL and the fallback in apps/server/src/auth/SessionStore.ts. Trace proofKeyThumbprint through apps/server/src/auth/http.ts and the mobile callers, then confirm the intended bearer-session lifetime and make the call site state it explicitly, with coverage for both credential paths if existing tests support it.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.