feat(auth): store credentials in OS keyring by default
What does this MR do?
Makes glab auth login store credentials in the operating system keyring by default when a backend is available (macOS Keychain, Windows Credential Manager, libsecret/Secret Service on Linux), matching the behavior of the GitHub CLI. When no keyring is available, credentials fall back to plaintext file storage.
Behavior
- Keyring is the default at login; no flag is needed.
--insecure-storageforces plaintext file storage (following theghCLI).--use-keyringis deprecated (keyring is now the default) but still accepted for backward compatibility; the two are mutually exclusive.- The choice is decided per-host at login time and persisted via
use_keyring, so existing plaintext logins keep working until the user re-authenticates (no surprise keychain prompts, no breakage on upgrade). - If no keyring backend is present, a default login falls back to file storage with a warning.
- In CI (when
GITLAB_CIorCIis truthy) credentials are stored in the configuration file rather than the keyring. CI credentials are usually supplied via environment variables, and an OS keyring is often unavailable or not persisted across job steps. - Applies to all keyring-eligible secrets:
token,job_token, andoauth2_refresh_token. Switching a host from keyring to file (or logging out) removes any keyring copy, so a secret is never orphaned. glab auth statusnow names each host's storage backend ("operating system keyring", "configuration file (plaintext)", or "environment variable<NAME>") and nudges hosts still on plaintext to migrate.
Implementation
config.KeyringAvailable()probes for a usable keyring backend.auth logincomputes the keyring preference from the flags, CI detection, and availability, then persists it before writing credentials in every auth path (token, job-token, web/device/OAuth).- Keyring read failures (locked/denied/unavailable) are surfaced instead of being silently treated as an empty credential; a genuinely missing entry is treated as unset.
- If the OAuth2 refresh token cannot be read but a usable access token is present, the client falls through to access-token-only auth rather than failing the whole build.
config.SetStringValueclears a stale!!nullYAML tag when writing a value, so a value written onto a blank entry (e.g.token:) is no longer left in the file astoken: !!null <secret>(which leaked the secret in plaintext while decoding as empty).- The cryptic OAuth2 "no Refresh Token / Access Token required" error is replaced with an actionable "run
glab auth loginto re-authenticate" message.
How to test
Important
Test with the branch build (./bin/glab), not the glab already on your PATH.
The released glab stores tokens in the config file by default (keyring is opt-in
there), so running the wrong binary looks like "it didn't migrate".
make build
command -v glab # confirm this is NOT the binary you intend to test
# optionally, so `glab` == this build for the session:
# alias glab="$PWD/bin/glab" (or `make install`)Fresh login uses the keyring by default:
./bin/glab auth login --hostname gitlab.com --web
./bin/glab auth status --hostname gitlab.com # -> "Token found in operating system keyring"
# token is in the keyring, not the file:
security find-generic-password -s "glab:gitlab.com:token" >/dev/null 2>&1 && echo "in keychain"
grep -n 'token:' ~/.config/glab-cli/config.yml # host token line is emptyMigrating an existing plaintext host:
./bin/glab auth status --all # plaintext hosts show the migrate hint
./bin/glab auth login --hostname <plaintext-host> # re-auth moves it into the keyring
./bin/glab auth status --hostname <plaintext-host># now "operating system keyring"Opt out / no keyring available:
./bin/glab auth login --hostname gitlab.com --token <PAT> --insecure-storage
grep -n 'token:' ~/.config/glab-cli/config.yml # token stored as a quoted string, never !!nullCI defaults to the file (no keyring probe):
CI=true ./bin/glab auth login --hostname gitlab.com --token <PAT>
grep -n 'token:' ~/.config/glab-cli/config.yml # stored in the file, even where a keyring existsTests
config:Test_KeyringAvailable,Test_InCI,Test_GetWithSource_SurfacesKeyringReadError,Test_GetWithSource_KeyringEnabledButTokenMissingReturnsEmpty,Test_SetStringValue_ResetsNullTagOnBlankEntry,Test_Set_SwitchingKeyringToFileRemovesStaleEntry,Test_Set_ClearRemovesKeyringEntry.api:TestNewClientFromConfig_OAuth2NoTokenReturnsError(actionable message),TestNewClientFromConfig_SurfacesKeyringReadError.auth login:Test_defaultKeyringLogin,Test_insecureStorageLogin,Test_keyringUnavailableFallsBackToFile,Test_useKeyringDeprecatedFallsBackToFileWhenUnavailable,Test_ciLoginDefaultsToFile,Test_ciLoginHonorsExplicitUseKeyring.auth status:Test_statusRun_keyringMigrationHintplus updated golden output.- Verified end-to-end against the real macOS Keychain (stored, read back transparently, removed on logout).
Known follow-up
- Storing OAuth credentials as a single keyring entry (access token + refresh token + expiry +
is_oauth2) rather than splitting secrets across the keyring and the config file, to remove the "half-migrated" failure class entirely. Recommended as its own MR.
Closes #8327 (closed)
Edited by Stan Hu