auth-go

module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 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
Passkeys adapters/webauthn WebAuthn; 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 the sql 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
_ = pgstore.NewTOTPRepo(db).SetSecret(ctx, uid, secret) // enroll; secret persisted via the port
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(); never stored
link, err := ml.Consume(ctx, raw)      // single-use

// 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 the sql adapters (single-tx swap); memory falls back to overlap-not-gap
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(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())

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. Only the TOTP shared secret is stored recoverable (HOTP needs the raw secret to verify a code) — protect that column with database column encryption or a protected schema.
  • 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.
  • Input bounds. Value-object constructors cap length (email 254, id 255, token 4096) so an unbounded attacker-controlled field can't be hashed or stored.

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, TOTP enrollment + verification, and single-use magic links.
Command human is a minimal, self-contained walkthrough of the human auth flows: users, sessions (issue / validate / rotate / revoke), password hashing, TOTP enrollment + verification, and single-use magic links.
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