Document the API for creating credentials in code without a CSV file: - NewCredential params requirement (nil panics) - PurposeToken vs PurposeDefault lookup behavior - CacheCredential vs LoadCSV - Full test example for Bearer token auth
7.7 KiB
csvauth
Simple, non-scalable credentials stored in a tab-separated file.
(logical successor to envauth)
- Login Credentials
- Save recoverable (aes or plain) or salted hashed passwords (pbkdf2 or bcrypt)
- Great in http middleware, authorizing login or api requests
- Stored by username (or token hash)
- Service Accounts
- Store API keys for services like SMTP and S3
- Great for contacting other services
- Stored by purpose
Also useful for generating pbkdf2 or bcrypt hashes for manual entry in a real database.
Can be adapted to pull from a Google Sheets URL (CSV format).
# create login credentials
csvauth store 'bot@example.com'
# create login token
csvauth store --token 'bot@example.com'
# store service account
csvauth store --purpose 'postmark_smtp_notifier' 'admin@example.com'
credentials.tsv:
purpose name algo salt derived roles extra
ntfy_sh mytopic-1234 plain mytopic-1234
s3_files account1 aes xxxxxxxxxxxx xxxxxxxxxxxxxxxx
login johndoe pbkdf2 1000 16 SHA-256 5cLjzprCHP3WmMbzfqVaew k-elXFa4B_P4-iZ-Rr9GnA admin
login janedoe bcrypt $2a$12$Xbe3OnIapGXUv9eF3k3cSu7sazeZSJquUwGzaovJxb9XQcN54/rte {"foo": "bar"}
f, err := os.Open("./credentials.tsv")
defer func() { _ = f.Close() }()
auth, err := csvauth.Load(f)
// ...
credential, err := auth.Authenticate(usernameOrEmpty, passwordOrToken)
if err != nil {
return err
}
// ...
account := auth.LoadServiceAccount("account-mailer")
req.SetBasicAuth(account.Name, account.Secret())
Login Credentials: Basic Auth & Bearer Token
-
Use
csvauth store [options] <username>to create new login credentials.go run ./cmd/csvauth/ store --helpgo run ./cmd/csvauth/ store 'john.doe@example.com' # choose your own algorithm go run ./cmd/csvauth/ store --algorithm aes-128-gcm 'johndoe' go run ./cmd/csvauth/ store --algorithm plain 'johndoe' go run ./cmd/csvauth/ store --algorithm 'pbkdf2 1000 16 SHA-256' 'johndoe' go run ./cmd/csvauth/ store --algorithm 'bcrypt 12' 'john.doe@example.com' # choose your own password go run ./cmd/csvauth/ store --ask-password 'john.doe@example.com' go run ./cmd/csvauth/ store --password-file ./password.txt 'johndoe' # add extra credential data go run ./cmd/csvauth/ store --roles 'admin' --extra '{"foo":"bar"}' 'jimbob' -
Use
github.com/therootcompany/golib/auth/csvauthto verify credentialspackage main import ( "net/http" "os" "github.com/therootcompany/golib/auth/csvauth" ) var auth csvauth.Auth func main() { f, _ := os.Open("./credentials.tsv") defer func() { _ = f.Close() }() auth, _ = csvauth.Load(f) // ... } // Example of checking for checking username (or token signifier) and password // (or token) in just about every common way func handleRequest(w http.ResponseWriter, r *http.Request) { name, secret, ok := r.BasicAuth() if !ok { secret, ok = strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok { secret = r.Header.Get("X-API-Key") if secret == "" { secret = r.URL.Query().Get("access_token") if secret == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } } } credential, err := auth.Authenticate(name, secret); if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // ... }
Programmatic Usage (Tests, Embedded Apps)
You can set up credentials entirely in code, without a CSV file. This is useful for tests, embedded apps, or anywhere you want to avoid file I/O.
Creating an Auth instance
New takes a 16-byte AES key. For tests, any 16 bytes will do:
key := make([]byte, 16) // all zeros is fine for tests
auth := csvauth.New(key)
Creating credentials with NewCredential
NewCredential creates a credential with a derived (hashed or encrypted) secret.
The params argument is required and must specify the algorithm. Passing
nil will panic with an index-out-of-range error. Valid values:
| Algorithm | Params |
|---|---|
| Plaintext | []string{"plain"} |
| AES-128-GCM (reversible) | []string{"aes-128-gcm"} |
| PBKDF2 (defaults) | []string{"pbkdf2"} |
| PBKDF2 (explicit) | []string{"pbkdf2", "1000", "16", "SHA-256"} |
| bcrypt (defaults) | []string{"bcrypt"} |
| bcrypt (explicit cost) | []string{"bcrypt", "12"} |
cred := auth.NewCredential(
csvauth.PurposeToken, // purpose: "token" or "login"
"bot@example.com", // name (username or label)
"my-secret-token", // plaintext secret
[]string{"plain"}, // algorithm — REQUIRED, must not be nil
[]string{"admin"}, // roles (nil for none)
"", // extra JSON (empty string for none)
)
Token auth vs Login auth
The purpose field controls how a credential is stored and looked up:
-
"login"(PurposeDefault) — The credential is cached by name (username).Authenticate("username", "password")looks it up by name. -
"token"(PurposeToken) — The credential is cached by a hash of the secret in a separate tokens map.Authenticate("", "the-token")looks it up by hashing the provided secret and searching the tokens map.
If you use "login" as the purpose for a Bearer token credential, it will
never be found by Authenticate("", secret) because login credentials are
only searched by name, not by token hash. Token credentials must use
PurposeToken ("token").
CacheCredential vs LoadCSV
Both populate the same internal maps — they differ only in how credentials are provided:
CacheCredential(c)— Add a single credential programmatically. Use this in tests or when building credentials in code.LoadCSV(f, '\t')— Parse a TSV (or CSV) file of credentials. Use this in production when credentials live in a file.
Full test example
func TestBearerAuth(t *testing.T) {
key := make([]byte, 16)
auth := csvauth.New(key)
secret := "test-api-token-abc123"
// Purpose MUST be "token" for Bearer-style auth
cred := auth.NewCredential(
csvauth.PurposeToken,
"test-bot",
secret,
[]string{"plain"},
nil,
"",
)
auth.CacheCredential(*cred)
// Authenticate with empty name (Bearer token style)
principal, err := auth.Authenticate("", secret)
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
if principal.ID() != "test-bot" {
t.Fatalf("expected 'test-bot', got %q", principal.ID())
}
}
Service Account
-
Use
csvauth store --purpose <account> [options] <username>to store API credentialsgo run ./cmd/csvauth/ store --helpgo run ./cmd/csvauth/ store --purpose ntfy_sh_admins 'acme-admins-1234abcd' -
Use
github.com/therootcompany/golib/auth/csvauthto verify credentialspackage main import ( "bytes" "net/http" "os" "github.com/therootcompany/golib/auth/csvauth" ) func main() { f, _ := os.Open("./credentials.tsv") defer func() { _ = f.Close() }() auth, _ := csvauth.Load(f) // ... credential := auth.LoadServiceAccount("ntfy_sh_admins") req, _ := http.NewRequest("POST", "https://ntfy.sh/"+credential.Secret(), bytes.NewBuffer(message)) // ... }