
How HN: Go-dev-auth zero-dependency authentication library for Go
The CI badge covers the whole matrix on every push: build and tests on
以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。
The CI badge covers the whole matrix on every push: build and tests on the oldest supported and current Go, the race detector, lint, a fuzz pass over the attacker-facing parsers, and the storage conformance suite against real SQLite, PostgreSQL and MySQL servers.
A comprehensive, framework-agnostic authentication library for Go, modeled after better-auth. Email & password, social sign-on, sessions, account linking, two-factor auth, passkeys, magic links, organizations, SSO, admin tooling, API keys and JWT — with zero external dependencies (pure standard library).
go get github.com/go-dev-auth/go-dev-auth
Status: pre-1.0, in production use, heading to v1.0. The library runs in production in a real automation project on PostgreSQL, using the full feature set. Releases follow SemVer: while pre-1.0 the public API may still change between minor versions (pin an exact version); v1.0 freezes it, and from then on breaking changes require a major version. The remaining v1.0 gates are runnable examples, field mileage on the newly automated MySQL leg, and a third-party security review. See storage adapters for exactly which backends are verified where, and docs/security-model.md for the threat model.
Plugins (mirroring better-auth's plugin system)
package main
import ( "context" "net/http" "os" "time"
godevauth "github.com/go-dev-auth/go-dev-auth" "github.com/go-dev-auth/go-dev-auth/storage/memory" )
func main() { auth, err := godevauth.New(godevauth.Config{ BaseURL: "http://localhost:8080", // required Secret: os.Getenv("AUTH_SECRET"), // required, 32+ random chars Database: memory.New(), // required EmailAndPassword: godevauth.EmailPasswordConfig{Enabled: true},
// Required in production if anything sits in front of this // process (load balancer, ingress, CDN). See below. Advanced: godevauth.AdvancedConfig{ TrustProxyHeaders: true, TrustedProxies: []string{"10.0.0.0/8"}, }, }) if err != nil { panic(err) }
// sweep expired one-time tokens and sessions defer auth.StartCleanup(context.Background(), time.Hour)()
mux := http.NewServeMux() mux.Handle("/api/auth/", auth.Handler()) http.ListenAndServe(":8080", mux) }
The handler is a plain http.Handler, so it mounts on chi, echo, gorilla, or gin (via gin.WrapH) the same way. If your frontend runs on a different origin, wrap it: auth.CORS(auth.Handler()) and list that origin in TrustedOrigins.
New validates the configuration and returns an error rather than starting up in an unsafe state: a missing/short Secret, a missing or relative BaseURL (it decides cookie security, trusted origins and redirect targets), SameSite=none without secure cookies, or TrustProxyHeaders without TrustedProxies are all rejected.
Rate limiting and every recorded session IP depend on resolving the client's address. If anything terminates the connection in front of this process — an AWS ALB, nginx, Cloudflare, a Kubernetes ingress — you must say so, because both defaults are wrong in a different direction:
Advanced: godevauth.AdvancedConfig{ TrustProxyHeaders: true, // IPs or CIDRs of your load balancers / ingress pods. TrustedProxies: []string{"10.0.0.0/8", "192.168.0.0/16"}, // Or, for a provider-specific header: // IPAddressHeaders: []string{"CF-Connecting-IP"}, },
New refuses to start if TrustProxyHeaders (or IPAddressHeaders) is set without TrustedProxies. Use TrustedProxies: []string{"*"} to trust any peer — that is the old, spoofable behaviour, and it is only safe when the network guarantees the service is unreachable except through a proxy that overwrites the header.
The opposite mistake cannot be caught at startup, because it depends on the traffic. The first request that arrives with a forwarded header while proxy headers are untrusted logs an error naming the header and the peer. If you see it in production, you are in row one of that table.
Direct-to-internet deployments need none of this: leave all three unset.
Adapter Import Notes
In-memory storage/memory Tests, examples, single-process. Enforces unique constraints and indexes unique fields.
SQL storage/sqlstore PostgreSQL, MySQL, SQLite via database/sql. Bring your own driver.
MongoDB storage/mongostore Official mongo-go-driver. Separate Go module.
All adapters are written against the same conformance suite (storage/storagetest), which pins the semantics the auth core depends on: unique-violation reporting, NULL vs zero, chronological sorting, clause folding, literal case-insensitive substring matching and compare-and-set update counts.
What has actually been executed, as of this revision:
The CI legs cannot pass vacuously: the environment sets REQUIRE_DSN=1, which turns a missing database into a test failure rather than a skip.
If you write your own adapter, run that suite against it:
func TestConformance(t *testing.T) { storagetest.Run(t, func(t *testing.T) (storage.Adapter, func()) { return myAdapter(storagetest.Schema()), func() {} }) }
MongoDB
import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "github.com/go-dev-auth/go-dev-auth/storage/mongostore" )
client, err := mongo.Connect(options.Client().ApplyURI(os.Getenv("MONGODB_URI"))) store := mongostore.New(client.Database("myapp"))
auth, err := godevauth.New(godevauth.Config{Database: store, /* ... */})
New creates the indexes the schema implies, including the unique ones. That step is not cosmetic on MongoDB: collections are created lazily, so an instance without them looks healthy while every "does this email already exist?" check degrades into a race. Use Advanced.DisableAutoMigrate only if you create them in a separate deploy step.
The adapter maps the id field onto Mongo's _id, rejects composite values where MongoDB would read them as operators (the NoSQL-injection shape), escapes regex metacharacters in substring searches, and reports duplicate keys as storage.ErrUniqueViolation. Transaction needs a replica set or mongos, as MongoDB itself does.
import ( "database/sql"
"github.com/go-dev-auth/go-dev-auth/storage/sqlstore" _ "github.com/jackc/pgx/v5/stdlib" // bring your own driver )
db, _ := sql.Open("pgx", os.Getenv("DATABASE_URL")) store := sqlstore.New(db, sqlstore.Postgres, nil)
auth, _ := godevauth.New(godevauth.Config{ Secret: os.Getenv("AUTH_SECRET"), Database: store, // ... })
// pick up the plugin tables and columns, then apply them store.SetSchema(auth.Schema()) if err := store.Migrate(context.Background()); err != nil { ... }
// or hand the DDL to your own migration tool: fmt.Println(store.MigrationSQL()) // the whole schema, from nothing pending, _ := store.PendingMigrationSQL(ctx) // only what this database lacks
Migrate is idempotent and does two things: it creates missing tables and indexes, and it adds missing columns to tables that already exist. The second matters as soon as the database has data in it — see Migrations.
import ( "github.com/go-dev-auth/go-dev-auth/oauth2" "github.com/go-dev-auth/go-dev-auth/providers" )
godevauth.Config{ SocialProviders: []oauth2.Provider{ providers.Google(providers.Credentials{ ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), }), providers.GitHub(providers.Credentials{ ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), }), }, }
Set the provider redirect URI to {BaseURL}/api/auth/callback/{provider}. Custom providers are a single oauth2.New(oauth2.Spec{...}) call.
import ( "github.com/go-dev-auth/go-dev-auth/plugins/admin" "github.com/go-dev-auth/go-dev-auth/plugins/jwt" "github.com/go-dev-auth/go-dev-auth/plugins/magiclink" "github.com/go-dev-auth/go-dev-auth/plugins/organization" "github.com/go-dev-auth/go-dev-auth/plugins/passkey" "github.com/go-dev-auth/go-dev-auth/plugins/sso" "github.com/go-dev-auth/go-dev-auth/plugins/twofactor" )
godevauth.Config{ Plugins: []godevauth.Plugin{ twofactor.New(), organization.New(organization.Options{Teams: true}), admin.New(), jwt.New(), magiclink.New(magiclink.Options{ SendMagicLink: func(ctx context.Context, email, url, token string) error { return mailer.Send(email, "Sign in", url) }, }), passkey.New(), sso.New(sso.Options{ // gate who may register identity providers Authorize: func(c *godevauth.Ctx, sd *godevauth.SessionData) error { return myApp.RequireAdmin(sd) }, }), }, }
Passkeys (plugins/passkey) add WebAuthn registration and sign-in with no external dependency: the CBOR/COSE parsing and ES256/RS256/Ed25519 signature verification live in the package. Registration needs a fresh session; sign-in uses discoverable credentials and runs through SignInUser, so bans and two-factor policy still apply. The signature counter is checked for the cloned-authenticator case.
SSO (plugins/sso) lets each organization bring its own OpenID Connect identity provider (Okta, Microsoft Entra, Google Workspace, Keycloak). Providers are registered at runtime, matched to users by email domain, and the sign-in runs through the same OAuth flow as social login — browser-bound single-use state, PKCE, ID-token verification against the issuer's JWKS. Client secrets are encrypted at rest; management endpoints fail closed until Options.Authorize is set.
Writing your own plugin means implementing three methods (ID, Init, Routes) and optionally Schema, Middleware, BeforeRequest/AfterRequest, SignInGuard (veto or challenge a sign-in on every path) or SessionGuard (re-check every request).
func handler(w http.ResponseWriter, r *http.Request) { sd, err := auth.GetSession(r) if err != nil { // errors.Is(err, godevauth.ErrNoSession) → unauthenticated } _ = sd.User // *storage.User _ = sd.Session // *storage.Session }
API endpoints
All endpoints live under Config.BasePath (default /api/auth) and match better-auth's route names:
Errors are returned as {"code": "USER_ALREADY_EXISTS", "message": "..."} with matching HTTP status codes. A known path with the wrong method returns 405 with an Allow header.
godevauth.Config mirrors better-auth's options: EmailAndPassword (min/max length, verification requirements, reset delivery, custom PasswordHasher), EmailVerification (delivery, required-before-sign-in, optional interstitial ConfirmationPage so email scanners cannot consume the link), Session (ExpiresIn, UpdateAge, FreshAge, cookie cache), User (additional fields, change-email — approval goes to the current verified address, then the new address is verified before the switch — delete-user), Account (linking rules, token encryption at rest), Advanced (cookie prefix, cross-subdomain cookies, SameSite, proxy trust, custom ID generation, CSRF exemptions), TrustedOrigins, RateLimit (windows, per-path rules, pluggable store), Events (the audit hook), PreviousSecrets (secret rotation), Hooks and DatabaseHooks.
Rate-limit rules in RateLimit.CustomRules may be keyed by the route pattern ("/reset-password/:token") as well as by a literal path. Buckets are keyed by the pattern, so a parameterised route is limited as one endpoint rather than one bucket per parameter value.
A runnable demo with most plugins enabled lives in examples/basic:
go run ./examples/basic
Custom user fields
Extra columns are declared on the config and are not client-writable unless you say so. This is the difference between a profile field and a privilege:
User: godevauth.UserConfig{ AdditionalFields: []storage.Field{ {Name: "displayName", Type: storage.FieldString, Input: true}, // user may set it {Name: "plan", Type: storage.FieldString}, // server-controlled }, },
Fields contributed by plugins (role, banned, twoFactorEnabled, …) are never writable from a request body, whatever Input says.
Passwords. scrypt (N=16384, r=16, p=1), better-auth's salt:key hex format. Each hash costs ~50 ms of CPU and ~32 MiB of scratch memory by design; the hasher bounds concurrency (default GOMAXPROCS) and pools its buffers so a burst of sign-ins cannot exhaust memory. Tune vi
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。