Skip to content

fix(cli-generator): support Basic Auth keyring login - #17550

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787855511-cli-basic-auth-login
Open

fix(cli-generator): support Basic Auth keyring login#17550
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1787855511-cli-basic-auth-login

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: Refs

Follow-up to #17546 (Twilio CLI feedback). On a CLI whose auth scheme is HTTP Basic, auth login --with-token was a silent no-op: it stored one pasted token under (cli_name, scheme_name), but inject_keyring_sources deliberately skipped SchemeBinding::Basic, so no provider ever read that entry — auth status still reported logged_in: false immediately after ✓ Stored credential. Two adjacent papercuts from the same report are fixed here too: Basic auth status flattened username+password into one fallback chain (password-only environment ⇒ logged_in: true, username labelled shadowed), and an unknown --scheme name was accepted silently, producing the misleading "No login flow declared for scheme BasicAuth" instead of naming the schemes the binary declares.

HTTP Basic needs two independent values, so it cannot share the single keyring slot the token schemes use. It now writes two derived slots — <scheme>:username / <scheme>:password — which keeps AuthCredentialSource::Keyring and every storage backend behind it untouched. ADR-0008 is updated to document the two-entry shape.

Token and OAuth paths are unchanged: the Basic branch is only reached when the resolved scheme's binding is SchemeBinding::Basic, and env sources keep precedence over the keyring (the keyring source is appended to the existing chain, exactly as for tokens).

Changes Made

  • run_basic_paste: auth login --with-token on a Basic scheme prompts for username and password (labelled stdin reads, clear error naming the missing half) and stores each in its own keyring slot.
  • inject_keyring_sources: SchemeBinding::Basic now appends a per-part keyring source to both halves instead of being a no-op, so the Basic provider resolves the stored values at request time.
  • auth status: expand_sourcesexpand_slots, returning one CredentialSlot per required value. Basic reports logged_in only when both slots resolve; JSON entries carry a "part" field, human output prefixes username: / password:. No secret values are printed.
  • auth logout: also deletes both Basic-derived slots.
  • resolve_scheme: an explicit --scheme that isn't declared errors with Unknown auth scheme \X`. This CLI declares: …` (unchanged escape hatch for CLIs that declare no schemes).
  • Changelog: generators/cli/changes/unreleased/fix-basic-auth-login-and-status.yml.

Testing

  • Unit tests added/updated
  • Manual testing completed

Unit tests (cargo test --lib auth::: 292 passed; full cargo test: 1922 passed): Basic slot splitting, both-halves-required status, keyring injection covering both halves, logout clearing both slots, undeclared-scheme validation, plus the existing token/OAuth cases.

End-to-end on a real Twilio CLI built from this branch (scheme accountSid_authToken, no env vars set), against a local HTTP server:

  • auth login --scheme BasicAuthUnknown auth scheme \BasicAuth`. This CLI declares: accountSid_authToken.`
  • printf 'ACfakesid\nfaketoken\n' | twilio auth login --with-token --scheme accountSid_authToken → stores both; auth status --json flips to "logged_in": true with part: username/part: password each active.
  • Real request sends authorization: Basic QUNmYWtlc2lkOmZha2V0b2tlbg== (decodes to ACfakesid:faketoken).
  • With TWILIO_USERNAME/TWILIO_PASSWORD also set, the header decodes to the env values — keyring stays the lower-precedence fallback.
  • auth logout → all four sources missing, "logged_in": false.

Link to Devin session: https://app.devin.ai/sessions/f08b0a3d82234745a76bf2369d295c29
Open in Devin Desktop: https://app.devin.ai/desktop/session/f08b0a3d82234745a76bf2369d295c29?variant=devin


Devin Review

Prompt for both halves on `auth login --with-token` for HTTP Basic schemes, store them in per-part keyring slots the Basic binding resolves at request time, report Basic as two required slots in `auth status`, clear both on logout, and reject undeclared --scheme names.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

The Basic-auth login/status/logout split is coherent and the two derived keyring slots keep AuthCredentialSource untouched. A few issues: Vec::dedup() on the unsorted declared list won't remove non-adjacent duplicates, the Basic password is read with terminal echo on, the logout loop can leave one half behind on error, and run_basic_paste skips the env-shadowing warning that ADR-0008 promises for login flows.

  • 🟡 2 warning(s)
  • 🔵 3 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +606 to +607
let mut valid = declared.clone();
valid.dedup();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

dedup() only collapses consecutive duplicates. declared is bindings-then-flows, so a scheme that has both a binding and a login flow (e.g. bindings ["a","b"], flow "a") yields ["a","b","a"] and the error message lists a, b, a. Dedup order-preserving instead:

Suggested change
let mut valid = declared.clone();
valid.dedup();
let mut valid: Vec<&str> = Vec::new();
for name in &declared {
if !valid.contains(name) {
valid.push(name);
}
}

Comment on lines +279 to +281
let _ = write!(err, "Password: ");
let _ = err.flush();
let password = read_line_from_stdin("password")?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

The password is read via plain read_line, so it is echoed to the terminal in interactive use (and lands in scrollback). For the token paste path that was arguably tolerable; for an explicit Password: prompt it's a visible regression in hygiene. Consider a no-echo read when stdin is a TTY (falling back to read_line_from_stdin when piped, which the e2e test relies on).

Comment on lines +462 to +465
for part in [BASIC_USERNAME_PART, BASIC_PASSWORD_PART] {
store.delete(cli_name, &basic_keyring_account(&scheme, part))?;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

? inside the loop means a failure deleting :username leaves :password in the keyring — a half-logged-out state that is worse than either endpoint. Attempt both deletes and return the first error afterwards:

Suggested change
for part in [BASIC_USERNAME_PART, BASIC_PASSWORD_PART] {
store.delete(cli_name, &basic_keyring_account(&scheme, part))?;
}
}
let mut first_err = None;
for part in [BASIC_USERNAME_PART, BASIC_PASSWORD_PART] {
if let Err(e) = store.delete(cli_name, &basic_keyring_account(&scheme, part)) {
first_err = first_err.or(Some(e));
}
}
if let Some(e) = first_err {
return Err(e);
}
}

Comment on lines +268 to +304
fn run_basic_paste(cli_name: &str, scheme_name: &str) -> Result<(), CliError> {
let stderr = std::io::stderr();
let mut err = stderr.lock();

let _ = writeln!(
err,
"Scheme `{scheme_name}` uses HTTP Basic auth, which needs two values."
);
let _ = write!(err, "Username: ");
let _ = err.flush();
let username = read_line_from_stdin("username")?;
let _ = write!(err, "Password: ");
let _ = err.flush();
let password = read_line_from_stdin("password")?;

let store = active_store();
store.set(
cli_name,
&basic_keyring_account(scheme_name, BASIC_USERNAME_PART),
&username,
)?;
store.set(
cli_name,
&basic_keyring_account(scheme_name, BASIC_PASSWORD_PART),
&password,
)?;

let _ = writeln!(
err,
"{}",
green(&format!(
"✓ Stored username and password for {cli_name}:{scheme_name} in {}",
store.backend_label()
))
);
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

run_basic_paste skips two things the token path provides: the env-shadowing warning ADR-0008 documents for auth login ("warns at flow start when an env var would shadow the keyring entry") and the token_paste_url hint. Since env keeps precedence over these new keyring slots, a user with TWILIO_PASSWORD set will store a password that never takes effect and get no hint why. Worth emitting the same shadow warning for each half before storing.

})
.collect();
"source": describe_source(src),
"part": slot.part,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

"part": slot.part emits "part": null for every token/OAuth entry, which changes the existing --json shape for all non-Basic schemes. If any consumers do strict schema validation, consider omitting the key when part is None (build the object with serde_json::Map and insert conditionally).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment thread generators/cli/sdk/src/auth/login.rs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.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