AJ ONeal 02fef67e53
fix(auth/csvauth): ID() returns Name only, not Name~hashID for tokens
Principal identity is the subject (who), not the credential instance
(which token). The hashID suffix was an internal cache fingerprint that
leaked into the public ID. Callers that need to distinguish individual
token instances must use a separate mechanism.

TSV serialization in ToRecord() still writes Name~hashID when hashID is
set so the credential file round-trips correctly.
2026-04-13 22:57:21 -06:00
..
2025-10-06 00:42:20 -06:00
2026-02-26 02:23:31 -07:00

csvauth

Go Reference

Simple, non-scalable credentials stored in a tab-separated file.
(logical successor to envauth)

  1. 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)
  2. 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

  1. Use csvauth store [options] <username> to create new login credentials.

    go run ./cmd/csvauth/ store --help
    
    go 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'
    
  2. Use github.com/therootcompany/golib/auth/csvauth to verify credentials

    package 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
       }
    
       // ...
    }
    

Service Account

  1. Use csvauth store --purpose <account> [options] <username> to store API credentials

    go run ./cmd/csvauth/ store --help
    
    go run ./cmd/csvauth/ store --purpose ntfy_sh_admins 'acme-admins-1234abcd'
    
  2. Use github.com/therootcompany/golib/auth/csvauth to verify credentials

    package 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))
    
       // ...
    }