Authenticate to a forge¶
A provider asks a CredentialSource for a token. Where that token comes from is
your composition. This module resolves no precedence of its own.
The four sources¶
forge.StaticCredential(token) // one you already hold
forge.ConfigCredential(cfg, "auth.value") // one config key — you name it
forge.EnvCredential("GITHUB_TOKEN") // a named environment variable
forge.FirstCredential(sources...) // the first non-empty one
A provider composes a default so zero-config still works:
forge.FirstCredential(
forge.ConfigCredential(cfg, forge.DefaultAuthKey),
forge.EnvCredential(github.DefaultTokenEnv),
)
Set Settings.Credential to take over completely.
Name your own key¶
ConfigCredential reads the key you give it, verbatim, against whatever
forge.Config you hand in. There is no scoping rule to learn, and both shapes
work:
forge.ConfigCredential(sub, "") // gitlab.auth.value, via Sub
forge.ConfigCredential(root, "platforms.gitlab.token") // your own layout
forge.DefaultAuthKey (auth.value) is what an empty key falls back to. It is a
default rather than a convention imposed on you, which matters when your keychain layer
already declares the credential under a different path.
Absent is not broken¶
("", nil) means not configured, which is legitimate: a public repository
needs no token. An error means broken.
FirstCredential treats them differently. A source that errors is skipped like
an empty one, but its error is retained and returned, joined with any
others, only if no source produced a value:
- Vault down,
GITHUB_TOKENset → the token, no error. The outage was immaterial. - Vault down, nothing else → the Vault error, so you know why rather than merely that.
Neither extreme is good enough. Aborting on the first error would sacrifice a working fallback to a transient outage; discarding errors would make a misconfigured source indistinguishable from an absent one, which is the silent absence that surfaces later as an unexplained 401.
Compose the layers in go/config¶
Ordering belongs in your configuration stack, stated once. In
go/config later backends win:
store, err := config.NewStore(ctx,
config.WithFiles(config.OS(), "/etc/mytool/config.yaml"),
config.WithBackend(configkeychain.New( // secrets above files
configkeychain.Registered(), "mytool", map[string]string{
"gitlab.auth.value": "gitlab-token",
})),
config.WithEnv("MYTOOL"), // overrides on top
)
Two things fall out that a resolution chain cannot give you: store.Explain
names which layer a value came from, and a refreshed token is written back to the
highest-precedence writable layer (the keychain) rather than down into the
file beneath it.
forge.Config is a two-method interface, so bridging takes six lines:
type configAdapter struct{ v *config.View }
func (c configAdapter) GetString(key string) string { return c.v.GetString(key) }
func (c configAdapter) Sub(key string) forge.Config { return configAdapter{v: c.v.Sub(key)} }
Every declared keychain entry is read at NewStore
config-keychain cannot enumerate a keychain, so it declares its key map up
front and Store.loadAll reads all of them eagerly, before any GetString.
Six declared accounts means six keychain reads at start-up whether or not
your process uses them, which on a locked keychain means six unlock prompts.
Declare only what this process needs, or split the stores.
Not using go/config?¶
Nothing here requires it. Pass the token straight in:
Or write a source. It is a function, so a closure is enough:
settings.Credential = func(ctx context.Context) (string, error) {
secret, err := myVault.Read(ctx, "secret/data/gitlab")
if err != nil {
return "", err
}
return secret.Token, nil
}
The context is honoured: a caller that gives up is not waited on.
Migrating from the credential chain¶
Earlier versions walked auth.env → auth.keychain → auth.value → a
well-known variable, inside this module.
Do not port the ladder into your consumer
Rebuilding the four rungs as a private resolveToken helper is the most
common migration mistake, and it reintroduces exactly what the removal
fixed. Each adapter already composes the ladder for its own section and its
own well-known variable, and you pass the root config to the factory.
Migrate from ResolveToken has the complete
multi-forge example.
| Was | Becomes |
|---|---|
auth.value: <token> |
unchanged, still the default key |
auth.env: FOO |
an env layer in your config stack, or forge.EnvCredential("FOO") |
auth.keychain: svc/acct |
a config-keychain layer, or a CredentialSource of your own |
fallbackEnv argument |
composed by the constructor; override with Settings.Credential |
Configuration still carrying auth.env or auth.keychain reports
forge.ErrStaleAuthKeys, but only when nothing else supplied a credential, so a
stale key next to a working environment variable stays quiet rather than failing
a deployment that works.
Pin the credential before you send it¶
Resolving a token is half the job. Attaching it only to the host you authenticated against is the other half:
if token != "" && forge.HostTrusted(assetURL, p.baseURL) {
req.Header.Set("Authorization", "token "+token)
}
Asset URLs come from release metadata, which a release author controls. See credential pinning for the threat model. This is the single easiest way to leak a token from an otherwise-correct provider.
No credential is often fine¶
A missing token is not automatically an error. Public releases download unauthenticated, and rate limits are usually the only difference. Decide the policy at the layer that knows whether the repository is private, and fail fast there with a message naming the variable to set, rather than letting an unauthenticated request fail later with an opaque 404.
Related¶
- Log what a provider does
- Providers: the config keys and fallback variable each provider uses
- Author a provider
- Credential pinning