From bab1750c829d226d564f5a46b3ddf0e43820a289 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 31 Mar 2026 05:56:57 -0600 Subject: [PATCH] doc(csvauth): add programmatic usage section for tests and embedded apps 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 --- auth/csvauth/README.md | 98 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/auth/csvauth/README.md b/auth/csvauth/README.md index a2820d3..9650e7b 100644 --- a/auth/csvauth/README.md +++ b/auth/csvauth/README.md @@ -135,6 +135,104 @@ req.SetBasicAuth(account.Name, account.Secret()) } ``` +## 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: + +```go +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"}` | + +```go +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 + +```go +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 1. Use `csvauth store --purpose [options] ` to store API credentials