Skip to content

feat(auth): --org-uuid hint, mismatch handling, and richer callback page#480

Merged
platinummonkey merged 11 commits intoDataDog:mainfrom
srosenthal-dd:stephen.rosenthal/org-uuid-hint
May 8, 2026
Merged

feat(auth): --org-uuid hint, mismatch handling, and richer callback page#480
platinummonkey merged 11 commits intoDataDog:mainfrom
srosenthal-dd:stephen.rosenthal/org-uuid-hint

Conversation

@srosenthal-dd
Copy link
Copy Markdown
Member

@srosenthal-dd srosenthal-dd commented May 7, 2026

Summary

Adds --org-uuid <UUID> to pup auth login, sent as dd_oid in the OAuth authorize URL. Lets the authorize endpoint skip the org switcher when the existing browser session matches and pre-route SAML/SSO routing for first-time logins. The UUID is persisted on the session record so subsequent logins re-emit it automatically.

Behavior

$ pup auth login --org datadog-hq --org-uuid 8dee7c38-00cb-11ea-a77b-8b5a08d3b091
🎯 Hinting org UUID (dd_oid): 8dee7c38-00cb-11ea-a77b-8b5a08d3b091
✅ Login successful (org: datadog-hq)!

$ pup auth login --org datadog-hq          # no flag this time, UUID recalled from storage
🎯 Hinting org UUID (dd_oid): 8dee7c38-00cb-11ea-a77b-8b5a08d3b091

$ pup auth login --org foo --org-uuid <A>  # but server returned <B>
⚠️  Requested org UUID <A> but OAuth returned <B> (Other Org). Saving token under "Other Org [bbbbbbbb]" instead of the requested label.
   Your existing 'foo' session is unchanged. Run `pup auth logout --org foo` to clear it if it's stale.

Changes

  • SessionEntry gains an optional org_uuid field with #[serde(default)] + skip_serializing_if, keeping existing sessions.json byte-stable.
  • build_authorization_url appends dd_oid when the caller supplies it (empty-string coercion happens up at the CLI layer in main.rs).
  • CallbackResult parses dd_oid and dd_org_name from both the HTTP listener and the stdin-paste path.
  • commands::auth::login resolves the effective UUID as CLI flag → stored hint → None, builds the URL, then on callback compares hinted vs returned dd_oid.
  • On mismatch with --org set, relabels the saved token as "{dd_org_name} [{uuid_prefix}]" (always embeds a UUID prefix so the relabel can't collide with an unrelated existing session of the same display name) and warns on stderr with a pup auth logout --org X cleanup hint. With no --org, default-session reauth stays idempotent and updates in place.
  • Callback dd_oid is shape-validated as 8-4-4-4-12 hex before being persisted, so a tampered stdin-paste URL can't seed garbage into sessions.json.
  • Saved org_uuid falls back only to the user's explicit --org-uuid flag if the callback didn't echo one — the recalled-from-storage value is used to build the in-flight URL but never re-persisted unconfirmed.
  • Localhost callback success page now identifies the OAuth provider, links the site, and shows the consented org name; UUID renders as fine-print mono.

Test plan

  • cargo test --bin pup — 86 auth-related tests pass, including 7 cases for resolve_save_target and 4 for success_page rendering / HTML escaping. Pre-existing parallel-run flakes in unrelated modules are unaffected.
  • cargo clippy --bin pup --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • First-time auth with explicit UUID — dd_oid in authorize URL, saved cleanly under requested label
  • Re-auth with no flag — UUID recalled from storage, emitted in URL
  • Hint that matches the active browser session — switcher skipped, fast pass-through
  • True mismatch (cross-org hint) — warning + relabel under Datadog HQ [8dee7c38] + cleanup hint + exit 0

Out of scope

  • Org switcher pre-select / highlight (a separate web-ui change).
  • Org switcher skipped on dd_oid match — rejected; would clobber unrelated browser tabs since sessions are one-per-domain. Also a separate web-ui change.
  • Rekeying the session registry by UUID instead of display label — known limitation flagged in review; would require a broader storage refactor.

Jira: https://datadoghq.atlassian.net/browse/DAL-670

…compat

Carries the authoritative `dd_oid` returned by the OAuth issuer alongside
the existing free-form `org` label. `#[serde(default)]` lets sessions.json
files written before this field existed deserialize cleanly, and
`skip_serializing_if = "Option::is_none"` keeps the on-disk shape
byte-stable for sessions that were never tagged with a UUID.

Adds `find_session(site, org)` so login can recall a previously-stored
UUID without searching the registry by hand. Existing callers of
`save_session` pass `None` for the new third arg; the upcoming login-path
change will populate it from the callback.
Adds a `--org-uuid <UUID>` flag on `pup auth login` that emits the value
as `dd_oid` in the OAuth authorize URL. The hint lets the authorize
endpoint skip the org switcher when the existing browser session already
matches, and pre-routes SAML/SSO routing for first-time logins.

Resolution order for the effective UUID:

  1. CLI flag `--org-uuid` if provided
  2. Stored `SessionEntry.org_uuid` from the session registry
  3. None (no hint)

After token exchange, the callback's authoritative `dd_oid` is persisted
on the session record so subsequent invocations re-emit it without the
flag. The callback parser now also captures `dd_org_name` for use in the
mismatch path (next commit).

Empty UUID strings are coerced to "no hint" (mirrors the empty-subdomain
behaviour) so an empty `--org-uuid ""` falls back rather than emitting a
malformed `dd_oid=`.
When the user hints a UUID via --org-uuid (or via a stored session) but
the OAuth server returns a different `dd_oid` (the user picked a
different org in the switcher, or hit an SSO routing override), saving
the resulting token under the user-supplied --org label would mislabel
it. Refusing to save would also throw away the user's click since
authorization codes are single-use.

The chosen middle ground: warn on stderr and switch the save target to
the actual org's name (`dd_org_name` from the callback, falling back to
the first 8 chars of the UUID). The user-supplied --org entry is left
alone, so a future re-auth against the requested org still gets a clean
slot.

The selection logic lives in a pure helper (`resolve_save_target`) so
the unit tests can pin the four cases (no hint, match, mismatch with
name, mismatch without name) without touching env state or storage.
@platinummonkey platinummonkey added authentication enhancement New feature or request labels May 8, 2026
The local listener's success page used to say only "Authentication
Successful" — handy as a "you can close this tab" signal but offered no
way to confirm the user consented for the org they intended. With the
dd_oid hint flow, the consented org can diverge silently (logged-out
login redirect drops the hint, SSO override, switcher pick), so showing
the org name + UUID alongside the success message lets users catch the
divergence at the moment of consent rather than later.

Tests stub `success_page` directly with synthetic inputs (hostile
strings included to verify HTML escaping). Real UUIDs that snuck into
unit tests during the prior commits are swapped for placeholder values.
Iterating on the success page based on live preview feedback:

- Lead line is now "Connected to Datadog (datadoghq.com)" — names the
  OAuth provider explicitly and links the site URL extracted from the
  callback's `site=` param. Avoids the dual "Authenticated"/"Authenticated
  to" repetition the prior copy had.
- Org line reads "in org <Name>" instead of "as <Name>" — the org-as-
  context framing parses better than person-as-org.
- UUID is now always-visible fine-print on its own line, wrapped in
  parens so it reads as supplementary annotation. Fine-print styling
  (small, gray, monospaced) demotes it visually but keeps it selectable
  and copy-pasteable. The earlier <details>/disclosure variant felt
  cluttered; hover-tooltip alternatives weren't copyable.
- Defensive: the link href is only emitted when `site` is an https URL,
  and the displayed text uses the bare `domain` to avoid surfacing a
  full URL into the rendered text.

Test fixtures use Star Wars placeholders (Mos Eisley Cantina,
tatooine.example, Rebel Alliance, deathstar.tatooine.example) so no
real Datadog org names or hosts leak into the codebase.
- Extract `org_suffix(Option<&str>) -> String` helper to replace the
  `org.map(|o| format!(" (org: {o})")).unwrap_or_default()` boilerplate
  that was duplicated across `login`, `logout`, `status`, `refresh`, and
  the new mismatch path.
- Replace the single-field `SaveTarget { org }` struct with an enum
  carrying both branches and pull the eprintln! out of the helper into
  the caller, so `resolve_save_target` becomes genuinely pure (matching
  the docstring) and the warning text stays testable separately if
  needed.
- `save_session(&SessionEntry)` instead of three loose Option-shaped
  args: a slip between `org` and `org_uuid` would have been silent
  before; the struct field names make the call sites clearer.
- Move the empty-`--org-uuid`-string coercion from
  `dcr::build_authorization_url` up to `main.rs` where the raw clap
  value lives. Drops the `.filter(|u| !u.is_empty())` from the
  URL-builder API and the now-redundant dcr test.
- Trim narration comments that restated what the code did or what was
  changing in this PR. Kept genuine WHY comments (security defense,
  OAuth-code-single-use rationale).
…smatch

Codex review flagged two real risks in the previous mismatch handling:

1. **Relabel could overwrite an unrelated existing session**. Saving
   under just `dd_org_name` meant a fresh login that resolved to "Foo
   HQ" would silently replace any existing `(site, "Foo HQ")` session
   on that site, even if that session belonged to a different
   underlying org UUID. Org display names are not stable identifiers,
   so the relabel now always embeds the actual UUID's 8-char prefix:
   `"Foo HQ [11111111]"`. Collisions become impossible.

2. **The user's pre-existing `--org foo` session was left untouched
   silently**. The warning told users their token was being saved
   somewhere else but didn't mention that the requested-label session
   was preserved as-is and may serve stale tokens on future commands.
   The warning now suggests `pup auth logout --org foo` so users can
   actively clear the prior session if it's stale.

Also tightens `SaveTarget::Reassigned.label` from `Option<String>` to
`String` since the resolution path is now total, and drops a dead
`code{...}` CSS rule on the success page that wasn't matching any
rendered element.
…d_oid

Two more issues caught in the iter-2 review pass:

- **Default-org reauth was silently leaving the unnamed session stale**
  on dd_oid mismatch. `pup auth login` (no `--org`) is supposed to be
  idempotent — refresh the default session. Previously, a recalled
  stored UUID that no longer matched the issuer's response would route
  through `Reassigned`, save under a phantom labeled slot, and leave
  the unnamed session pointing at old credentials. Future commands
  without `--org` would still load the stale token.

  `resolve_save_target` now only relabels when `--org` was explicitly
  set; when it wasn't, the default session is overwritten in place
  with the new UUID, so the user gets the idempotent refresh they
  expected.

- **Callback `dd_oid` is now shape-validated** before being persisted.
  In the stdin-paste fallback path the user pastes a URL whose query
  parameters could carry an arbitrary string. Anything that doesn't
  match the canonical `8-4-4-4-12` hex-with-dashes UUID shape is
  treated as missing, so a tampered URL can't seed garbage into
  `sessions.json` or rendered hints.

Also filters empty `dd_org_name` out of the warning text so we don't
render `OAuth returned <uuid> ()` with a dangling empty paren.
Codex flagged that on a successful login where the issuer omitted (or
sent a malformed) `dd_oid`, the previous code carried the
already-recalled UUID forward into the saved session. That meant a user
could authenticate into a different org but `sessions.json` would still
be tagged with the old org UUID, and subsequent logins would re-emit
that stale hint.

The fallback chain is now: callback-validated `dd_oid` → user's
explicit `--org-uuid` flag → None. The recalled-from-storage value is
only used to construct the in-flight authorize URL; it is no longer
written back unless the callback confirms it.
@srosenthal-dd srosenthal-dd changed the title feat(auth): --org-uuid hint for OAuth login feat(auth): --org-uuid hint, mismatch handling, and richer callback page May 8, 2026
@srosenthal-dd srosenthal-dd marked this pull request as ready for review May 8, 2026 18:12
@srosenthal-dd srosenthal-dd requested a review from a team as a code owner May 8, 2026 18:12
@platinummonkey platinummonkey merged commit b496c52 into DataDog:main May 8, 2026
6 checks passed
@srosenthal-dd srosenthal-dd deleted the stephen.rosenthal/org-uuid-hint branch May 8, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authentication enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants