Credential Provider Chain: Complete Implementation
A full, compilable credential chain for a fictional examplecloud provider.
Everything lives in one package, internal/credentials/, so it can be unit
tested without any Terraform machinery. Adapt names, file formats, and the
set of sources to your API's ecosystem — the structure is what transfers.
Contents
- Package layout
- Core types: Credentials, Provider, errors —
provider.go - The chain —
chain.go - Static provider —
static.go - Environment provider —
env.go - File provider with profiles —
file.go - Default chain constructor —
resolve.go - Unit tests
Package layout
internal/credentials/
├── provider.go # Provider interface, Credentials, ErrNoCredentials, ChainError
├── chain.go # Chain: ordered resolution, error aggregation
├── static.go # source 1: provider block values
├── env.go # source 2: environment variables
├── file.go # source 3: shared credentials file profiles
├── resolve.go # NewDefaultChain: assembles the canonical order
└── *_test.goCore types
provider.go:
package credentials
import (
"context"
"errors"
"fmt"
"strings"
)
// ErrNoCredentials signals that a source had nothing to offer and the chain
// should fall through to the next source. Any other error from Retrieve
// means the source was configured but unusable (malformed file, missing
// profile) and must be surfaced to the user, not silently skipped.
var ErrNoCredentials = errors.New("no credentials found")
// Credentials is a complete set of secrets. Secrets are resolved as a set:
// a source that supplies only one of the two fields supplies nothing.
type Credentials struct {
APIKey string
APISecret string
Source string // name of the provider that supplied them
}
func (c Credentials) Complete() bool {
return c.APIKey != "" && c.APISecret != ""
}
// String and GoString redact the secret so %v, %+v, and %#v can never leak
// it into logs, diagnostics, or wrapped errors.
func (c Credentials) String() string {
return fmt.Sprintf("Credentials{APIKey: %s, APISecret: ***REDACTED***, Source: %s}", c.APIKey, c.Source)
}
func (c Credentials) GoString() string { return c.String() }
// Provider is one source of credentials. Retrieve returns ErrNoCredentials
// (possibly wrapped) when the source has nothing to offer.
type Provider interface {
Retrieve(ctx context.Context) (Credentials, error)
Name() string
}
// ChainError aggregates the outcome of every source the chain consulted, so
// the final diagnostic can show users exactly what was tried and why each
// source was skipped.
type ChainError struct {
attempts []attempt
}
type attempt struct {
source string
err error
}
func (e *ChainError) record(source string, err error) {
e.attempts = append(e.attempts, attempt{source: source, err: err})
}
func (e *ChainError) Error() string {
if len(e.attempts) == 0 {
return ErrNoCredentials.Error()
}
var b strings.Builder
b.WriteString("no valid credential sources found. Sources tried:")
for _, a := range e.attempts {
fmt.Fprintf(&b, "\n - %s: %s", a.source, a.err)
}
return b.String()
}
// Is makes errors.Is(err, ErrNoCredentials) true only when every source
// fell through cleanly. If any source failed hard (e.g. malformed file),
// the caller should show that failure instead of the generic
// "no credentials" guidance.
func (e *ChainError) Is(target error) bool {
if target != ErrNoCredentials {
return false
}
for _, a := range e.attempts {
if !errors.Is(a.err, ErrNoCredentials) {
return false
}
}
return true
}Design notes:
- Two secret fields demonstrate set-resolution; a single-token API works the
same with
Complete()checking one field. Sourceexists purely for observability — log it, never the secrets.ChainError.Isis what letsConfigurechoose between the "here is how to supply credentials" message and the "your credentials file is broken" message with oneerrors.Iscall.
The chain
chain.go:
package credentials
import "context"
// Chain consults providers in order and returns the first complete set of
// credentials. Every skipped source is recorded so the aggregate error can
// explain the full resolution attempt.
type Chain struct {
providers []Provider
}
func NewChain(providers ...Provider) *Chain {
return &Chain{providers: providers}
}
func (c *Chain) Retrieve(ctx context.Context) (Credentials, error) {
chainErr := &ChainError{}
for _, p := range c.providers {
creds, err := p.Retrieve(ctx)
switch {
case err != nil:
// Record and continue: a broken source should not mask a
// working one later in the chain, but it must appear in the
// final error if nothing works. (Alternative: fail fast on
// non-sentinel errors. Continue-and-record is friendlier when
// e.g. a stale credentials file exists but env vars are set.)
chainErr.record(p.Name(), err)
case !creds.Complete():
chainErr.record(p.Name(), ErrNoCredentials)
default:
creds.Source = p.Name()
return creds, nil
}
}
return Credentials{}, chainErr
}
func (c *Chain) Name() string { return "Chain" }The chain itself implements Provider, so chains compose: a platform
identity source that is itself a chain of metadata endpoints slots in as one
entry.
Static provider
static.go — values from the provider block. Highest priority: explicit
configuration always wins.
package credentials
import "context"
type StaticProvider struct {
APIKey string
APISecret string
}
func (p *StaticProvider) Retrieve(_ context.Context) (Credentials, error) {
creds := Credentials{APIKey: p.APIKey, APISecret: p.APISecret}
if !creds.Complete() {
return Credentials{}, ErrNoCredentials
}
return creds, nil
}
func (p *StaticProvider) Name() string { return "provider configuration" }Environment provider
env.go — the injectable GetEnv field is what makes precedence unit
tests hermetic (no os.Setenv cross-test contamination).
package credentials
import (
"context"
"os"
)
const (
EnvAPIKey = "EXAMPLECLOUD_API_KEY"
EnvAPISecret = "EXAMPLECLOUD_API_SECRET"
EnvProfile = "EXAMPLECLOUD_PROFILE"
EnvCredsFile = "EXAMPLECLOUD_SHARED_CREDENTIALS_FILE"
)
type EnvProvider struct {
// GetEnv defaults to os.Getenv; inject a map-backed func in tests.
GetEnv func(string) string
}
func (p *EnvProvider) getenv(key string) string {
if p.GetEnv != nil {
return p.GetEnv(key)
}
return os.Getenv(key)
}
func (p *EnvProvider) Retrieve(_ context.Context) (Credentials, error) {
creds := Credentials{
APIKey: p.getenv(EnvAPIKey),
APISecret: p.getenv(EnvAPISecret),
}
if !creds.Complete() {
return Credentials{}, ErrNoCredentials
}
return creds, nil
}
func (p *EnvProvider) Name() string {
return "environment variables (" + EnvAPIKey + ", " + EnvAPISecret + ")"
}Naming the actual variables in Name() pays off directly in the aggregate
error message.
File provider with profiles
file.go. The format here is minimal INI-style parsing to avoid
dependencies; use YAML/TOML if your ecosystem prefers it. The error
semantics are the part to copy exactly:
The rule is uniform: a defaulted value that resolves to nothing falls through; an explicit value that resolves to nothing is a user mistake and errors. It applies identically to the file path and the profile name.
| Condition | Behavior | Why |
|---|---|---|
| File absent, path defaulted | ErrNoCredentials |
Most users have no file; fall through silently |
| File absent, path set explicitly | hard error | The user pointed at it; tell them it is missing |
| File unreadable or malformed | hard error | Never silently skip a file the user wrote |
| Profile missing, name set explicitly | hard error | An explicit profile that resolves to nothing is a typo |
| Profile missing, name defaulted | ErrNoCredentials |
A file holding only named profiles shouldn't break users who never asked for default |
| Profile present, fields incomplete | ErrNoCredentials |
The profile may intentionally hold only non-secret settings |
package credentials
import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
)
const DefaultProfile = "default"
func DefaultCredentialsFilePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".examplecloud", "credentials")
}
// ResolveFilePath: explicit config > env var > default location.
func ResolveFilePath(explicit string, getenv func(string) string) (path string, explicitlySet bool) {
if explicit != "" {
return explicit, true
}
if fromEnv := getenv(EnvCredsFile); fromEnv != "" {
return fromEnv, true
}
return DefaultCredentialsFilePath(), false
}
// ResolveProfile: explicit config > env var > "default". The explicitlySet
// result feeds the same defaulted-vs-explicit semantics as the file path.
func ResolveProfile(explicit string, getenv func(string) string) (profile string, explicitlySet bool) {
if explicit != "" {
return explicit, true
}
if fromEnv := getenv(EnvProfile); fromEnv != "" {
return fromEnv, true
}
return DefaultProfile, false
}
type FileProvider struct {
Path string // resolved via ResolveFilePath
PathExplicit bool // missing file: hard error if true, fall through if not
Profile string // resolved via ResolveProfile
ProfileExplicit bool // missing profile: hard error if true, fall through if not
}
func (p *FileProvider) Retrieve(_ context.Context) (Credentials, error) {
if p.Path == "" {
return Credentials{}, ErrNoCredentials
}
profiles, err := parseCredentialsFile(p.Path)
if errors.Is(err, fs.ErrNotExist) {
if p.PathExplicit {
return Credentials{}, fmt.Errorf("credentials file %q does not exist", p.Path)
}
return Credentials{}, ErrNoCredentials
}
if err != nil {
return Credentials{}, fmt.Errorf("reading credentials file %q: %w", p.Path, err)
}
profile, ok := profiles[p.Profile]
if !ok {
if p.ProfileExplicit {
return Credentials{}, fmt.Errorf("profile %q not found in %q", p.Profile, p.Path)
}
return Credentials{}, ErrNoCredentials
}
creds := Credentials{APIKey: profile["api_key"], APISecret: profile["api_secret"]}
if !creds.Complete() {
return Credentials{}, ErrNoCredentials
}
return creds, nil
}
func (p *FileProvider) Name() string {
return fmt.Sprintf("shared credentials file (%s, profile %q)", p.Path, p.Profile)
}
// PermissionsTooOpen reports whether the file is group- or world-accessible.
// Surface this as a warning diagnostic, not an error. POSIX permission bits
// are not meaningful on Windows.
func PermissionsTooOpen(path string) bool {
if runtime.GOOS == "windows" {
return false
}
info, err := os.Stat(path)
if err != nil {
return false
}
return info.Mode().Perm()&0o077 != 0
}
// parseCredentialsFile reads a minimal INI format:
//
// [default]
// api_key = abc
// api_secret = xyz
func parseCredentialsFile(path string) (map[string]map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
profiles := map[string]map[string]string{}
var current map[string]string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
switch {
case line == "" || strings.HasPrefix(line, "#"):
case strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]"):
name := strings.TrimSpace(line[1 : len(line)-1])
current = map[string]string{}
profiles[name] = current
default:
key, value, found := strings.Cut(line, "=")
if !found || current == nil {
return nil, fmt.Errorf("malformed line: %q", line)
}
current[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
return profiles, scanner.Err()
}Default chain constructor
resolve.go — one constructor owns the canonical order so Configure and
tests can never disagree about precedence.
package credentials
import "os"
type Options struct {
FilePath string // explicit credentials_file from provider config
Profile string // explicit profile from provider config
GetEnv func(string) string
// DefaultFilePath overrides the default file location when the user set
// nothing (keeps defaulted fall-through semantics). Tests use this to
// stay hermetic without hand-assembling the chain.
DefaultFilePath string
}
// NewDefaultChain assembles the canonical resolution order:
// static provider configuration > environment variables > credentials file.
// Add a platform identity provider at the end where the platform offers one.
func NewDefaultChain(staticKey, staticSecret string, opts Options) *Chain {
getenv := opts.GetEnv
if getenv == nil {
getenv = os.Getenv
}
path, pathExplicit := ResolveFilePath(opts.FilePath, getenv)
if !pathExplicit && opts.DefaultFilePath != "" {
path = opts.DefaultFilePath
}
profile, profileExplicit := ResolveProfile(opts.Profile, getenv)
return NewChain(
&StaticProvider{APIKey: staticKey, APISecret: staticSecret},
&EnvProvider{GetEnv: getenv},
&FileProvider{Path: path, PathExplicit: pathExplicit, Profile: profile, ProfileExplicit: profileExplicit},
)
}For Configure wiring — unknown-value guards, the errors.Is branch that
selects the right diagnostic, and the permission warning — see the skill
body (SKILL.md); it composes directly with this package.
Unit tests
The essential coverage, hermetic via injected env and t.TempDir():
package credentials
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func mapEnv(m map[string]string) func(string) string {
return func(key string) string { return m[key] }
}
func writeCredentialsFile(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "credentials")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return path
}
const sampleFile = "[default]\napi_key = file-key\napi_secret = file-secret\n"
func TestChain_StaticWins(t *testing.T) {
env := mapEnv(map[string]string{EnvAPIKey: "env-key", EnvAPISecret: "env-secret"})
chain := NewDefaultChain("static-key", "static-secret", Options{GetEnv: env})
creds, err := chain.Retrieve(context.Background())
if err != nil {
t.Fatal(err)
}
if creds.APIKey != "static-key" {
t.Errorf("expected static credentials to win, got source %q", creds.Source)
}
}
func TestChain_EnvBeatsFile(t *testing.T) {
path := writeCredentialsFile(t, sampleFile)
env := mapEnv(map[string]string{EnvAPIKey: "env-key", EnvAPISecret: "env-secret"})
chain := NewDefaultChain("", "", Options{FilePath: path, GetEnv: env})
creds, err := chain.Retrieve(context.Background())
if err != nil {
t.Fatal(err)
}
if creds.APIKey != "env-key" {
t.Errorf("expected env credentials to win, got %q from %q", creds.APIKey, creds.Source)
}
}
func TestChain_FallsThroughToFile(t *testing.T) {
path := writeCredentialsFile(t, sampleFile)
chain := NewDefaultChain("", "", Options{FilePath: path, GetEnv: mapEnv(nil)})
creds, err := chain.Retrieve(context.Background())
if err != nil {
t.Fatal(err)
}
if creds.APIKey != "file-key" {
t.Errorf("expected file credentials, got %q from %q", creds.APIKey, creds.Source)
}
}
func TestChain_IncompleteSourceSkipped(t *testing.T) {
// Env supplies only the key: the set is incomplete, so the whole
// source is skipped and the file supplies both values.
path := writeCredentialsFile(t, sampleFile)
env := mapEnv(map[string]string{EnvAPIKey: "env-key"})
chain := NewDefaultChain("", "", Options{FilePath: path, GetEnv: env})
creds, err := chain.Retrieve(context.Background())
if err != nil {
t.Fatal(err)
}
if creds.APIKey != "file-key" {
t.Errorf("incomplete env source must not win: got %q from %q", creds.APIKey, creds.Source)
}
}
func TestChain_AllSourcesEmpty(t *testing.T) {
chain := NewDefaultChain("", "", Options{
DefaultFilePath: filepath.Join(t.TempDir(), "missing"), // defaulted semantics, hermetic location
GetEnv: mapEnv(nil),
})
_, err := chain.Retrieve(context.Background())
if !errors.Is(err, ErrNoCredentials) {
t.Fatalf("expected ErrNoCredentials, got %v", err)
}
for _, source := range []string{"provider configuration", "environment variables", "credentials file"} {
if !strings.Contains(err.Error(), source) {
t.Errorf("aggregate error should mention %q:\n%s", source, err)
}
}
}
func TestChain_ExplicitMissingProfileIsHardError(t *testing.T) {
path := writeCredentialsFile(t, sampleFile)
chain := NewDefaultChain("", "", Options{FilePath: path, Profile: "prod", GetEnv: mapEnv(nil)})
_, err := chain.Retrieve(context.Background())
if err == nil || errors.Is(err, ErrNoCredentials) {
t.Fatalf("expected hard error for missing profile, got %v", err)
}
if !strings.Contains(err.Error(), `profile "prod" not found`) {
t.Errorf("error should name the missing profile:\n%s", err)
}
}
func TestChain_DefaultProfileMissingFallsThrough(t *testing.T) {
// The file exists but holds only a named profile; nobody asked for
// "default", so the file source falls through instead of erroring.
path := writeCredentialsFile(t, "[work]\napi_key = k\napi_secret = s\n")
chain := NewDefaultChain("", "", Options{FilePath: path, GetEnv: mapEnv(nil)})
_, err := chain.Retrieve(context.Background())
if !errors.Is(err, ErrNoCredentials) {
t.Fatalf("expected fall-through for defaulted missing profile, got %v", err)
}
}
func TestCredentials_Redaction(t *testing.T) {
creds := Credentials{APIKey: "key", APISecret: "super-secret"}
for _, formatted := range []string{
fmt.Sprintf("%v", creds), fmt.Sprintf("%+v", creds), fmt.Sprintf("%#v", creds), creds.String(),
} {
if strings.Contains(formatted, "super-secret") {
t.Errorf("secret leaked: %s", formatted)
}
}
}