auth-go

module
v0.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT

README

auth-go

Klarlabs shared authentication library. Implements the auth methods mandated by the Klarlabs product standard — magic link, password + TOTP, and passkeys (WebAuthn) — all converging on a single server-side session with HttpOnly-cookie semantics.

go get github.com/klarlabs-studio/auth-go

Architecture

Strict DDD / hexagonal. The domain is the center and imports nothing outward; persistence and the WebAuthn ceremony engine are injected through ports.

domain/              the auth bounded context — entities, value objects,
                     domain services, repository + authenticator ports
  values.go          UserID · TenantID · Email · Token (validating constructors)
  user.go            User aggregate + UserRepository port
  password.go        PasswordHash value object (argon2id)
  totp.go            TOTPSecret + TOTPConfig (RFC 6238) + TOTPRepository port
  session.go         Session aggregate + SessionRepository port
  magiclink.go       MagicLink aggregate + MagicLinkRepository port
  passkey.go         PasskeyCredential entity + Passkey{Repository,Authenticator}
  workload.go        WorkerID · Scope · APIKey aggregate + WorkloadStore + AtomicRotator
  services.go        SessionService · MagicLinkService · WorkloadKeyService

adapters/
  memory/            in-memory ports — tests + single-node dev
  pgstore/           Postgres ports (database/sql, no driver dep) + schema.sql
  sqlite/            SQLite ports (database/sql + modernc.org/sqlite, cgo-free)
                     + schema.sql; embedded, self-migrating via Open
  webauthn/          PasskeyAuthenticator over go-webauthn (passkey adapter)

middleware/
  basicauth.go       BasicAuthMiddleware — inbound HTTP adapter; Basic → session
                     handshake (stdlib net/http, depends only on the domain)

example/
  human/             runnable human auth walkthrough (go run ./example/human)
  workload/          runnable workload identity walkthrough (go run ./example/workload)

Value objects enforce their own invariants in constructors — no anemic models. A product wires the repository ports to its store of choice — Postgres, SQLite, or in-memory — and gets every method. Every port method takes a context.Context first, so storage I/O honors cancellation, deadlines, and trace propagation.

Methods

Area Where Notes
Users domain.UserRepository minimal User aggregate (id, tenant, email); GetUser / UpsertUser through the port
Sessions domain.SessionService opaque 256-bit token, TTL, rotate (anti-fixation), revoke + logout-everywhere
Password domain.PasswordHash argon2id, PHC encoding, OWASP-2024 defaults, constant-time verify
TOTP domain.TOTPConfig + TOTPRepository RFC 6238, verified against the spec vector, clock-skew window, otpauth:// URI; per-user secret persisted through the port
Magic link domain.MagicLinkService single-use, TTL, only the SHA-256 hash stored; Issue invalidates prior links for email+tenant
Passkeys adapters/webauthn WebAuthn; HMAC-signed ceremony state (StateKey); kept an adapter so the core carries only x/crypto
Workload keys domain.WorkloadKeyService scoped API keys for agent workers — 256-bit token (stdlib only), only the SHA-256 hash stored, resource:action scopes with tools:* wildcards, issue/validate/authorize/revoke; rotate atomic on all first-party adapters (AtomicRotator)
Basic auth middleware.BasicAuthMiddleware bootstrap-then-session handshake; Basic once, session cookie after — fits browser SPAs

Example

// Every store does I/O, so each service call takes a context.Context first — it
// carries cancellation, deadlines, and trace propagation through to the adapter
// (Postgres, SQLite, or in-memory). Pass the per-request ctx in real code.
ctx := context.Background()

repo := pgstore.NewSessionRepo(db) // or memory.NewSessionRepo() / sqlite.NewSessionRepo(db)
sm := domain.NewSessionService(repo, 24*time.Hour, nil)

uid, _ := domain.NewUserID(userID)
tid, _ := domain.NewTenantID(tenantID)
emailVO, _ := domain.NewEmail(email)
s, _ := sm.Issue(ctx, uid, tid)     // set s.Token().String() as an HttpOnly cookie

tok, _ := domain.TokenFromString(cookie)
sess, err := sm.Validate(ctx, tok)  // each request
fresh, _ := sm.Rotate(ctx, tok)     // re-issue after auth — old token invalidated (anti-fixation)
_ = fresh
sm.RevokeAll(ctx, uid)              // logout everywhere

// Users — persisted through the UserRepository port.
users := pgstore.NewUserRepo(db)
u, _ := domain.NewUser(uid, tid, emailVO, time.Now(), time.Now())
_ = users.UpsertUser(ctx, u)

h, _ := domain.HashPassword(pw, domain.DefaultArgon2idParams())
err = h.Verify(pw)

cfg := domain.DefaultTOTPConfig("Klarlabs")
secret, _ := domain.NewTOTPSecret()
uri := cfg.ProvisioningURI(secret, email)   // → QR code
cipher, _ := aesgcm.New(totpKey)            // 32-byte deployment key
_ = pgstore.NewTOTPRepo(db, cipher).SetSecret(ctx, uid, secret) // enroll; encrypted at rest
err = cfg.Validate(secret, userCode, time.Now())

ml := domain.NewMagicLinkService(pgstore.NewMagicLinkRepo(db), 15*time.Minute, nil)
raw, _ := ml.Issue(ctx, emailVO, tid)  // email raw.String(); prior links for email+tenant invalidated
link, err := ml.Peek(ctx, raw)         // is it good? does NOT spend it
link, err = ml.Consume(ctx, raw)       // single-use; spend it when they commit

// Workload keys: scoped, time-boxed API keys for agent workers.
wk := domain.NewWorkloadKeyService(pgstore.NewWorkloadKeyRepo(db), nil)
worker, _ := domain.NewWorkerID("agent-7")
scope, _ := domain.NewScope("tools:*", "memory:read")
key, token, _ := wk.IssueKey(ctx, domain.KeyRequest{
    WorkerID: worker, Scope: scope, ExpiresAt: time.Now().Add(24 * time.Hour),
})                                   // hand token.String() to the worker once — never stored
err = wk.Authorize(ctx, token, "tools:write")     // validate + scope match (wildcard)
_, newToken, _ := wk.RotateKey(ctx, key.ID())     // atomic on memory/sqlite/pgstore (AtomicRotator)
wk.RevokeAllKeys(ctx, worker)                     // kill-switch
_ = newToken

// Basic-auth handshake: Authorization: Basic once, session cookie after.
mw, _ := middleware.NewBasicAuthMiddleware(middleware.BasicAuthConfig{
    Verifier: middleware.AuthenticatorFunc(func(ctx context.Context, user, pass string) (domain.UserID, domain.TenantID, error) {
        // look up the user, verify with PasswordHash.Verify, return the identity
        return uid, tid, nil // or middleware.ErrInvalidCredentials
    }),
    Sessions:   sm,
    Realm:      "rollops",
    CookieName: "rollops_ui",
})
http.Handle("/ui/", mw.Middleware(uiHandler))
// downstream: sess, _ := middleware.SessionFromContext(r.Context())
// logout: mw.Logout(w, r)  // revokes from cookie; do not Revoke(sess.Token()) after Validate

Security posture

What the library guarantees, and where the deployment must meet it halfway:

  • Secrets at rest. Session tokens, magic links, and workload keys are stored as SHA-256 hashes; passwords as argon2id. The TOTP shared secret is the one recoverable credential — sqlite/pgstore NewTOTPRepo(db, cipher) requires an aesgcm (or other) SecretCipher so it is encrypted at rest. Use NewPlaintextTOTPRepo only for tests or legacy migration.
  • Session cookie vs Session.Token(). Issue / Rotate return a Session whose Token() is the raw cookie value. After Validate, Token() is empty — the raw bearer is not recoverable from storage. Revoke with the cookie string (or BasicAuthMiddleware.Logout), never Revoke(sess.Token()) from a validated session.
  • TOTP replay. Use TOTPService (requires an AtomicTOTPConsumer repo — all first-party adapters). TOTPConfig.Validate remains available and is intentionally replay-prone within the skew window.
  • Magic links. Issue invalidates prior unconsumed links for the same email+tenant so only the newly emailed token remains redeemable.
  • Tenant isolation. UserRepository.GetUser is tenant-scoped: a UserID belonging to another tenant reads as ErrNotFound even though IDs are globally unique. This is defense in depth alongside, not instead of, database Row-Level Security — enable RLS on the Postgres tables (tenant_id derived server-side, never from the client).
  • CSRF. The session cookie defaults to SameSite=Lax + HttpOnly + Secure. Lax blocks cross-site state-changing requests but is not a complete defense; pair state-changing endpoints with an app-layer anti-CSRF token or an Origin/Sec-Fetch-Site check. Use SameSite=Strict where no cross-site authenticated navigation is needed.
  • Brute-force lockout. Per-account lockout means an attacker who knows an email can lock that account on purpose; the lock expires (Window) so the victim self-recovers. Also throttle on a network identity (client IP) at the edge so one source can't drive another account's counter. Prefer LockoutKeyFromEmail so authgo_login_attempts does not store plaintext email.
  • Input bounds. Value-object constructors cap length (email 254, id/worker 255, token 4096, password 1024, lockout key 255) so an unbounded attacker-controlled field can't be hashed or stored.
  • Passkey ceremony state. WebAuthn ceremony state is HMAC-SHA256-signed with a required Config.StateKey (≥32 bytes), so a client cannot swap the UserID. Still treat the blob as a secret single-use challenge; prefer server-side storage. See adapters/webauthn package docs.

Engineering bar

Per the Klarlabs default: TDD, gofmt, golangci-lint (gocritic + gosec), nox security scan, coverctl coverage gate, strict DDD. CI is the shared klarlabs-studio/.github reusable Go workflow. Postgres adapter tests are integration-gated on TEST_DATABASE_URL. MIT.

Directories

Path Synopsis
adapters
memory
Package memory provides in-memory implementations of the auth domain repository ports, for tests and single-node development.
Package memory provides in-memory implementations of the auth domain repository ports, for tests and single-node development.
pgstore
Package pgstore provides Postgres implementations of the auth domain repository ports, built on the stdlib database/sql.
Package pgstore provides Postgres implementations of the auth domain repository ports, built on the stdlib database/sql.
sqlite
Package sqlite provides SQLite implementations of the auth domain repository ports, built on the stdlib database/sql and the pure-Go (cgo-free) driver modernc.org/sqlite — the same driver the other Klarlabs services use, so a product can vendor one SQLite stack across the stack.
Package sqlite provides SQLite implementations of the auth domain repository ports, built on the stdlib database/sql and the pure-Go (cgo-free) driver modernc.org/sqlite — the same driver the other Klarlabs services use, so a product can vendor one SQLite stack across the stack.
webauthn
Package webauthn is the passkey adapter — it implements domain.PasskeyAuthenticator over github.com/go-webauthn/webauthn.
Package webauthn is the passkey adapter — it implements domain.PasskeyAuthenticator over github.com/go-webauthn/webauthn.
Package aesgcm is a ready AES-256-GCM implementation of domain.SecretCipher, for encrypting a recoverable secret (the TOTP shared secret) at rest.
Package aesgcm is a ready AES-256-GCM implementation of domain.SecretCipher, for encrypting a recoverable secret (the TOTP shared secret) at rest.
Package domain is the auth bounded context.
Package domain is the auth bounded context.
example
human command
Command human is a minimal, self-contained walkthrough of the human auth flows: users, sessions (issue / validate / rotate / revoke), password hashing, replay-safe TOTP, single-use magic links, and a WebAuthn registration begin (HMAC-sealed state).
Command human is a minimal, self-contained walkthrough of the human auth flows: users, sessions (issue / validate / rotate / revoke), password hashing, replay-safe TOTP, single-use magic links, and a WebAuthn registration begin (HMAC-sealed state).
workload command
Command workload is a minimal, self-contained walkthrough of workload identity: issuing a scoped API key for an agent worker, validating it, authorizing a concrete action against its scope, rotating it, and revoking.
Command workload is a minimal, self-contained walkthrough of workload identity: issuing a scoped API key for an agent worker, validating it, authorizing a concrete action against its scope, rotating it, and revoking.
Package middleware holds inbound HTTP adapters for the auth bounded context.
Package middleware holds inbound HTTP adapters for the auth bounded context.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL