Skip to content

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.

type CredentialSource func(ctx context.Context) (string, error)

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:

forge.FirstCredential(vaultSource, forge.EnvCredential("GITHUB_TOKEN"))
  • Vault down, GITHUB_TOKEN set → 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:

settings.Credential = forge.StaticCredential(tokenFromMyOwnLoginFlow)

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.envauth.keychainauth.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.