Authentication, honestly
fulfillmenttools does not authenticate you. Google Identity Platform (Firebase) does, and fulfillmenttools accepts the resulting token. The swagger has no login endpoint at all, which is why this is worth spelling out:
fftsigns in againstidentitytoolkit.googleapis.comwith your username and password, and receives an ID token and a refresh token.- It sends
Authorization: Bearer <idToken>tohttps://<tenant>.api.fulfillmenttools.com. - When the token nears expiry it refreshes against
securetoken.googleapis.com, transparently. You will not notice.
The API key confers no authorization. It is the Firebase Web API key: it identifies the Firebase project and grants nothing on its own. It is sent only as ?key= on those two Google URLs and is never sent to fulfillmenttools — the token source owns a separate HTTP client with a hardcoded allowlist of the two Google hosts, so the key is structurally incapable of reaching your tenant. It is nonetheless treated as sensitive and kept in the keychain, not the config file.
Your username is not your email. fulfillmenttools derives a synthetic one: {username}@ocff-{projectId}-{env}.com. fft builds it for you; project add asks for the parts.
Secrets (API key, password, refresh token, ID token) live in the OS keychain — Keychain on macOS, Credential Manager on Windows, Secret Service on Linux. Each gets its own entry. Non-secret project data lives in ~/.config/fft/config.yaml, mode 0600. An older config that still holds the API key in cleartext is migrated into the keychain on the next run.
On a Linux box with no Secret Service (a headless server, a bare container), pass --no-keyring or set FFT_NO_KEYRING=1 to fall back to a 0600 file.
On Windows, --no-keyring protects less than 0600 suggests
The default on Windows is the Credential Manager, and it is the right choice: the OS holds each secret per-user, and config.yaml never contains a secret. None of what follows applies to the default path.
--no-keyring is different. It writes %USERPROFILE%\.local\state\fft\credentials.json, and that file holds your password and refresh token in cleartext, exactly as on Linux. What is not the same is the protection around it:
- Windows has no POSIX mode bits.
fftasks for mode0600, but Go'sos.Chmodon Windows only toggles the read-only attribute and discards the rest. File security on Windows is an ACL, andfftsets no ACL of its own. - The file is therefore protected by exactly one thing: the ACL it inherits from its parent directory. Under the default
%USERPROFILE%that inheritance is sound — a stock Windows install grants your profile directory to you,SYSTEMandAdministrators, and to no other standard user. There, the file is about as private as0600on Linux, whererootcan read it anyway. - The weakness is that the protection is inherited rather than asserted. Point
XDG_STATE_HOME(whichffthonours on Windows too) at a shared directory, a second volume with default permissions, a network share, or a redirected/roaming profile, and the file inherits that ACL — which may let every user on the machine read it. On Linux,0600would still protect you in all of those places. On Windows, nothing does.
So: on Windows, prefer the Credential Manager. If you genuinely need --no-keyring — a Windows CI container, say — leave XDG_STATE_HOME unset so the file stays inside your user profile, and assume anyone with local Administrator can read your tenant password.
The specs that assert 0600 are skipped on Windows, with that reason printed in the CI output, rather than deleted — so this gap stays visible instead of rotting quietly.