auth login should ask which scope/level to store credentials at #419

Open
opened 2026-08-17 15:49:54 +00:00 by hexajon · 2 comments
Owner

Problem: Authenticating to a second forge overwrites the first forge's stored credentials, because there is no way to choose where the token lands. Concretely: I had working credentials for one Gitea host, then ran auth login for a second forge, and it clobbered the first host's token. I was never offered a choice of scope, so a routine second-forge login silently destroyed a valid credential for a different host.

Expected: auth login should prompt for the storage level/scope before writing, for example:

  • Global (global git config / shared secure backend) - applies across all repos
  • This repo only (per-repo git config) - scoped to the current checkout

This matches madtea's own documented precedence (env, then secure backend, then per-repo git config, then global git config). The tool clearly supports both a repo level and a global level, but login does not let the user pick which one to write.

Key nuance to design for: credentials are keyed per host. Two different forges should be able to coexist, not overwrite each other. So "which level" is one axis, and "do not clobber a different host's entry" is the real bug underneath it. A per-host, per-scope store would let a global token for one forge and a repo-scoped token for another live at the same time.

Acceptance criteria:

  • auth login asks for the target level, or takes a flag, for example --scope global|repo.
  • Logging into a new host does not overwrite an existing token for a different host.
  • The non-interactive/CI path has an explicit flag, so it stays scriptable.
**Problem:** Authenticating to a second forge overwrites the first forge's stored credentials, because there is no way to choose where the token lands. Concretely: I had working credentials for one Gitea host, then ran `auth login` for a second forge, and it clobbered the first host's token. I was never offered a choice of scope, so a routine second-forge login silently destroyed a valid credential for a different host. **Expected:** `auth login` should prompt for the storage **level/scope** before writing, for example: - **Global** (global git config / shared secure backend) - applies across all repos - **This repo only** (per-repo git config) - scoped to the current checkout This matches madtea's own documented precedence (env, then secure backend, then per-repo git config, then global git config). The tool clearly supports both a repo level and a global level, but login does not let the user pick which one to write. **Key nuance to design for:** credentials are keyed per host. Two different forges should be able to coexist, not overwrite each other. So "which level" is one axis, and "do not clobber a different host's entry" is the real bug underneath it. A per-host, per-scope store would let a global token for one forge and a repo-scoped token for another live at the same time. **Acceptance criteria:** - `auth login` asks for the target level, or takes a flag, for example `--scope global|repo`. - Logging into a new host does not overwrite an existing token for a different host. - The non-interactive/CI path has an explicit flag, so it stays scriptable.
Author
Owner

There are two parts here: a clear bug and a design call.

The bug is data loss. A second-forge login clobbers the first forge's stored token. The store is already host-keyed, so login can write per host without overwriting a different host's entry. That half is straightforward, and I can land it on its own.

The design axis is the open one: how login picks the storage LEVEL, global git config / shared backend versus per repo. It touches the credential-storage model (ADR 0013) and the launch-scope credential boundary (ADR 0027). My lean is the issue's own proposal: prompt interactively for global-vs-repo, with a --scope global|repo flag for the non-interactive and CI paths. What I have not settled is the default when neither the prompt nor the flag answers, for example in a script that predates the flag. I will settle that before building, and the clobber fix does not need to wait for it.

There are two parts here: a clear bug and a design call. The bug is data loss. A second-forge login clobbers the first forge's stored token. The store is already host-keyed, so login can write per host without overwriting a different host's entry. That half is straightforward, and I can land it on its own. The design axis is the open one: how login picks the storage LEVEL, global git config / shared backend versus per repo. It touches the credential-storage model (ADR 0013) and the launch-scope credential boundary (ADR 0027). My lean is the issue's own proposal: prompt interactively for global-vs-repo, with a `--scope global|repo` flag for the non-interactive and CI paths. What I have not settled is the default when neither the prompt nor the flag answers, for example in a script that predates the flag. I will settle that before building, and the clobber fix does not need to wait for it.
hexajon self-assigned this 2026-08-17 17:45:48 +00:00
Author
Owner

Grounded this against the records. It splits into a part already decided and a part that is a real bug.

The scope-prompt ask is superseded. ADR 0025 section 2 already decided that a login is forge-wide and that there is NO storage-scope prompt. Credentials are keyed by forge host, one account per forge; --global is a deprecated no-op. So auth login does not gain a global-vs-repo scope choice. One of the proposed options, "store in global git config", is also the plaintext path that ADR 0013's 2026-07-10 hard-rule amendment bans without --insecure-storage. That half is closed by supersedence, not open design.

The clobber is real, but only on the plaintext path. The secure backends (keychain, pass, systemd-creds; the default) already key every credential by forge host, so a second-forge login cannot overwrite the first:

  • keychain account = host (internal/config/keychain.go:25)
  • pass entry = madtea/<host> (internal/config/pass.go:28)
  • systemd-creds subkey = token-enc-<host> (internal/config/systemd_creds.go:161)

Legacy compound (url,user) entries migrate to host keys on first use, forge-gated so a forge-A migration cannot destroy forge B (internal/config/config.go:254-263).

The clobber survives only on --insecure-storage, which writes single-slot madtea.token/madtea.url/madtea.user with no host in the key (internal/cmd/setup/save.go:180 to internal/config/gitconfig_writer.go:28 and internal/config/git.go:126), and a login runs global. So a second-forge --insecure-storage login silently destroys the first forge's plaintext token.

Decision (plaintext path): refuse, do not silently overwrite. When a --insecure-storage login would replace a plaintext token for a DIFFERENT forge host, fail closed with a teaching error (ADR 0023): name the existing forge host, and give the two remedies, in order:

  1. install a secure backend for host-keyed multi-forge storage;
  2. pass an explicit overwrite flag to replace the existing plaintext credential on purpose.

Keep the plaintext fallback single-slot by design. The secure backend stays the supported multi-forge model.

Acceptance criteria:

  • A second-forge --insecure-storage login with a different host's plaintext token present refuses, names the existing host, and names both remedies. No token bytes appear in the message.
  • An explicit opt-in overwrite flag (flag name is a separate naming call) replaces the plaintext credential deliberately.
  • A --insecure-storage login for the SAME host still updates in place.
  • The secure-backend paths are unchanged; they are already host-keyed.
  • The non-interactive path stays scriptable via the flag.

Related: ADR 0013, ADR 0025 section 2 and its 2026-07-19 amendment (#271), ADR 0023.

Grounded this against the records. It splits into a part already decided and a part that is a real bug. **The scope-prompt ask is superseded.** ADR 0025 section 2 already decided that a login is forge-wide and that there is NO storage-scope prompt. Credentials are keyed by forge host, one account per forge; `--global` is a deprecated no-op. So `auth login` does not gain a global-vs-repo scope choice. One of the proposed options, "store in global git config", is also the plaintext path that ADR 0013's 2026-07-10 hard-rule amendment bans without `--insecure-storage`. That half is closed by supersedence, not open design. **The clobber is real, but only on the plaintext path.** The secure backends (keychain, pass, systemd-creds; the default) already key every credential by forge host, so a second-forge login cannot overwrite the first: - keychain account = host (`internal/config/keychain.go:25`) - pass entry = `madtea/<host>` (`internal/config/pass.go:28`) - systemd-creds subkey = `token-enc-<host>` (`internal/config/systemd_creds.go:161`) Legacy compound `(url,user)` entries migrate to host keys on first use, forge-gated so a forge-A migration cannot destroy forge B (`internal/config/config.go:254-263`). The clobber survives only on `--insecure-storage`, which writes single-slot `madtea.token`/`madtea.url`/`madtea.user` with no host in the key (`internal/cmd/setup/save.go:180` to `internal/config/gitconfig_writer.go:28` and `internal/config/git.go:126`), and a login runs global. So a second-forge `--insecure-storage` login silently destroys the first forge's plaintext token. **Decision (plaintext path): refuse, do not silently overwrite.** When a `--insecure-storage` login would replace a plaintext token for a DIFFERENT forge host, fail closed with a teaching error (ADR 0023): name the existing forge host, and give the two remedies, in order: 1. install a secure backend for host-keyed multi-forge storage; 2. pass an explicit overwrite flag to replace the existing plaintext credential on purpose. Keep the plaintext fallback single-slot by design. The secure backend stays the supported multi-forge model. Acceptance criteria: - A second-forge `--insecure-storage` login with a different host's plaintext token present refuses, names the existing host, and names both remedies. No token bytes appear in the message. - An explicit opt-in overwrite flag (flag name is a separate naming call) replaces the plaintext credential deliberately. - A `--insecure-storage` login for the SAME host still updates in place. - The secure-backend paths are unchanged; they are already host-keyed. - The non-interactive path stays scriptable via the flag. Related: ADR 0013, ADR 0025 section 2 and its 2026-07-19 amendment (#271), ADR 0023.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#419
No description provided.