How HN: Go-dev-auth zero-dependency authentication library for Go

Hacker News by 24 min read 27x views
How HN: Go-dev-auth zero-dependency authentication library for Go

Share Post

CI Go Reference Go Report Card  MIT

The CI sign covers the entire matrix on all push: build and tests on the oldest supported and current Go, the competition detector, lint, a fuzz pass complete the attacker-facing parsers, and the retention conformance suite against real SQLite, PostgreSQL and MySQL servers.

A comprehensive, framework-agnostic authentication archive for Go, modeled following better-auth. Email & password, social sign-on, sessions, document linking, two-factor auth, passkeys, magic links, organizations, SSO, admin tooling, API keys and JWT — alongside zero external dependencies (pure norm library).

go get github.com/go-dev-auth/go-dev-auth 

Status: pre-1.0, in manufacturing use, heading to v1.0. The archive runs in manufacturing in a genuine automation project on PostgreSQL, using the complete characteristic set. Releases prosecute SemVer: during pre-1.0 the community API may motionless alter between insignificant versions (pin an exact version); v1.0 freezes it, and from afterward on breaking changes necessitate a important version. The remaining v1.0 gates are runnable examples, site mileage on the newly automated MySQL leg, and a third-party safety review. See storage adapters for exactly which backends are verified where, and docs/security-model.md for the danger model.

Core

  • Email & password authentication (scrypt hashing, compatible alongside better-auth's hash format)
  • Social sign-on via OAuth 2.0 / OIDC alongside PKCE — Google, GitHub, Discord, Facebook, Microsoft, Apple, GitLab, LinkedIn, Spotify, Twitch, X built in, affirmative a declarative oauth2.Spec for any tradition provider
  • Database-backed sessions alongside sliding expiration, signed cookies, optional biscuit caching, list/revoke endpoints
  • Email verification, password reset, alter email/password, delete person flows
  • Account linking & unlinking (trusted providers, token refresh, document info)
  • CSRF base checking, trusted origins alongside wildcard subdomains, IP-based charge limiting
  • Storage adapter interface alongside built-in database/sql (Postgres, MySQL, SQLite) and in-memory adapters, affirmative schema/migration SQL generation
  • Request hooks, repository hooks, tradition user/session fields

Plugins (mirroring better-auth's plugin system)

  • twofactor — TOTP, email OTP and backup codes
  • passkey — WebAuthn passkeys, CBOR/COSE parsing included (no external dependency)
  • magiclink — passwordless email links
  • organization — orgs, members, roles, invitations, teams
  • sso — bring-your-own OIDC character provider, matched by email domain
  • admin — person management, bans, roles, impersonation
  • apikey — hashed API keys alongside scopes that authenticate akin sessions
  • jwt — EdDSA-signed JWTs + JWKS endpoint
  • bearer — Authorization header auth for non-browser clients
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 manufacturing if item sits in forefront of this // procedure (load balancer, ingress, CDN). See below. Advanced: godevauth.AdvancedConfig{ TrustProxyHeaders: true, TrustedProxies: []string{"10.0.0.0/8"}, }, }) if err != nil { panic(err) } // clear 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 identical way. If your frontend runs on a distinct origin, cover it: auth.CORS(auth.Handler()) and catalog that base in TrustedOrigins.

New validates the configuration and returns an error fairly than starting up in an unsafe state: a missing/short Secret, a missing or related BaseURL (it decides biscuit security, trusted origins and redirect targets), SameSite=none without safe cookies, or TrustProxyHeaders without TrustedProxies are all rejected.

Deployment requirement: inform it concerning your proxy

Rate limiting and all recorded meeting IP depend on resolving the client's address. If item terminates the association in forefront of this procedure — an AWS ALB, nginx, Cloudflare, a Kubernetes ingress — you must say so, since both defaults are incorrect in a distinct direction:

Configuration What happens
Nothing set, but operating rearward a proxy Every petition resolves to the proxy's address, so all clients portion one bucket. The documented strict regulation of 3 sign-ins per 10 seconds becomes 3 per 10 seconds for your entire fleet, and the limiter fails closed. This is a self-inflicted outage.
TrustProxyHeaders alongside no TrustedProxies X-Forwarded-For is client-supplied. Any visitant picks its own bucket and the bounds stops limiting anything.
TrustProxyHeaders + TrustedProxies Correct. Forwarded headers are peruse lone from a listed peer, and the header sequence is walked right-to-left former trusted hops, so addresses a client prepended are ignored.

So:

Advanced: godevauth.AdvancedConfig{ TrustProxyHeaders: true, // IPs or CIDRs of your burden 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 rely any equal — that is the old, spoofable behaviour, and it is lone harmless whenever the network guarantees the assistance is unreachable apart from through a proxy that overwrites the header.

The contrary error cannot be caught at startup, since it depends on the traffic. The archetypal petition that arrives alongside a forwarded header during 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: depart all three unset.

Adapter Import Notes
In-memory storage/memory Tests, examples, single-process. Enforces distinctive constraints and indexes distinctive 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 identical conformance suite (storage/storagetest), which pins the semantics the auth center 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 really been executed, as of this revision:

  • SQLite — the complete conformance suite and a complete HTTP auth stream run against a genuine SQLite repository in CI on all push. Verified.
  • PostgreSQL — running in production (a genuine automation project uses the entire archive on live PostgreSQL), and the conformance suite, immigration idempotence and the complete HTTP auth stream now run against a genuine PostgreSQL server in CI on all push. Field-proven and regression-guarded.
  • MySQL — the identical CI job runs the conformance suite, migrations and the complete auth stream against a genuine MySQL 8 server on all push. CI-verified; no site mileage yet, so study item you hit.
  • MongoDB — exercised against an in-house wire-protocol test double, not a live mongod. Beta.

The CI legs cannot continue vacuously: the surroundings sets REQUIRE_DSN=1, which turns a missing repository into a test nonaccomplishment fairly than a skip.

If you compose 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() {} }) }
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 distinctive ones. That stage is not cosmetic on MongoDB: collections are created lazily, so an case without them looks fit during all "does this email already exist?" inspect degrades into a race. Use Advanced.DisableAutoMigrate lone if you create them in a distinct deploy step.

The adapter maps the id site onto Mongo's _id, rejects composite values anywhere MongoDB would peruse them as operators (the NoSQL-injection shape), escapes regex metacharacters in substring searches, and reports copy 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, // ... }) // choice up the plugin tables *and columns*, afterward use them store.SetSchema(auth.Schema()) if err := store.Migrate(context.Background()); err != nil { ... } // or hand the DDL to your own immigration tool: fmt.Println(store.MigrationSQL()) // the entire schema, from nothing pending, _ := store.PendingMigrationSQL(ctx) // lone what this repository 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 shortly as the repository 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 sole 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{ // entrance who may enroll character providers Authorize: func(c *godevauth.Ctx, sd *godevauth.SessionData) error { return myApp.RequireAdmin(sd) }, }), }, }

Passkeys (plugins/passkey) add WebAuthn registration and sign-in alongside no external dependency: the CBOR/COSE parsing and ES256/RS256/Ed25519 signature verification live in the package. Registration needs a caller session; sign-in uses discoverable credentials and runs through SignInUser, so bans and two-factor guideline motionless apply. The signature oppose is checked for the cloned-authenticator case.

SSO (plugins/sso) lets all institution bring its own OpenID Connect character 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 identical OAuth stream as social login — browser-bound single-use state, PKCE, ID-token verification against the issuer's JWKS. Client secrets are encrypted at rest; administration endpoints neglect closed until Options.Authorize is set.

Writing your own plugin method implementing three methods (ID, Init, Routes) and optionally Schema, Middleware, BeforeRequest/AfterRequest, SignInGuard (veto or difficulty a sign-in on all path) or SessionGuard (re-check all request).

Server-side meeting access

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 }

All endpoints live under Config.BasePath (default /api/auth) and equivalent better-auth's path names:

Area Endpoints
Email & password POST /sign-up/email, POST /sign-in/email, POST /forget-password, POST /reset-password, GET /reset-password/:token, POST /change-password, POST /set-password
Email verification POST /send-verification-email, GET /verify-email (renders a confirmation leaf alternatively of consuming the token whenever EmailVerification.ConfirmationPage is set), POST /verify-email (consumes it)
Session GET /get-session, POST /sign-out, GET /list-sessions, POST /revoke-session (by token, or by the sessionId from /list-sessions), POST /revoke-sessions, POST /revoke-other-sessions
Social POST /sign-in/social, `GET
User POST /update-user, POST /change-email, POST /delete-user, `GET
Two-factor POST /two-factor/{enable,disable,get-totp-uri,verify-totp,send-otp,verify-otp,generate-backup-codes,verify-backup-code}
Magic link POST /sign-in/magic-link, GET /magic-link/verify
Passkey GET /passkey/generate-register-options, POST /passkey/verify-registration, POST /passkey/generate-authenticate-options, POST /passkey/verify-authentication, GET /passkey/list-user-passkeys, POST /passkey/{delete-passkey,update-passkey}
SSO POST /sign-in/sso, POST /sso/register, GET /sso/list, POST /sso/delete, `GET
Organization POST /organization/{create,update,delete,set-active,invite-member,accept-invitation,reject-invitation,cancel-invitation,remove-member,update-member-role,leave,check-slug,create-team,remove-team}, GET /organization/{list,get-full-organization,get-invitation,list-invitations,get-active-member,list-teams}
Admin POST /admin/{create-user,set-role,set-user-password,update-user,ban-user,unban-user,impersonate-user,stop-impersonating,list-user-sessions,revoke-user-session,revoke-user-sessions,remove-user}, GET /admin/list-users
API keys POST /api-key/{create,update,delete,verify}, GET /api-key/{get,list}
JWT GET /token, GET /jwks, GET /.well-known/jwks.json

Errors are returned as {"code": "USER_ALREADY_EXISTS", "message": "..."} alongside matching HTTP position codes. A known way alongside the incorrect method returns 405 alongside an Allow header.

godevauth.Config mirrors better-auth's options: EmailAndPassword (min/max length, verification requirements, reset delivery, tradition PasswordHasher), EmailVerification (delivery, required-before-sign-in, optional interstitial ConfirmationPage so email scanners cannot consume the link), Session (ExpiresIn, UpdateAge, FreshAge, biscuit cache), User (additional fields, change-email — endorsement goes to the current verified address, afterward the new location is verified before the toggle — delete-user), Account (linking rules, token encryption at rest), Advanced (cookie prefix, cross-subdomain cookies, SameSite, proxy trust, tradition 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 path form ("/reset-password/:token") as fine as by a literal path. Buckets are keyed by the pattern, so a parameterised path is constricted as one endpoint fairly than one bucket per indicator value.

A runnable demo alongside most plugins enabled lives in examples/basic:

Extra columns are declared on the config and are not client-writable unless you say so. This is the difference between a overview site and a privilege:

User: godevauth.UserConfig{ AdditionalFields: []storage.Field{ {Name: "displayName", Type: storage.FieldString, Input: true}, // person may set it {Name: "plan", Type: storage.FieldString}, // server-controlled }, },

Fields contributed by plugins (role, banned, twoFactorEnabled, …) are never writable from a petition body, any Input says.

Passwords. scrypt (N=16384, r=16, p=1), better-auth's salt:key hex format. Each hash expenses ~50 ms of CPU and ~32 MiB of scratch recollection by design; the hasher limits concurrency (default GOMAXPROCS) and pools its buffers so a burst of sign-ins cannot exhaust memory. Tune via crypto.NewScryptHasher. Hash format is byte-for-byte compatible alongside better-auth for ASCII passwords; non-ASCII passwords not already in Unicode NFKC form can differ, since better-auth normalizes to NFKC archetypal and the norm archive has no NFKC implementation to equivalent without adding a dependency (see ScryptHasher.Hash).

Rate limiting is on by default (fail-closed) since the sign-in endpoint is costly by construction. Set RateLimit.Disabled lone whenever a gateway already throttles these paths, and provision RateLimit.Storage whenever operating additional than one instance.

Sessions. 32-byte random tokens, HMAC-signed cookies alongside domain separation and __Secure- prefixes on HTTPS. Raw tokens are never included in meeting listings. The optional biscuit cache carries an complete revalidation deadline that is never extended from cached data, so revocation continually takes consequence inside CookieCache.MaxAge.

One-time tokens (password reset, email verification, magic links, deletion, OAuth state) are stored as SHA-256 digests and consumed atomically, so repository peruse admission yields no usable links.

OAuth. PKCE (S256), province pinned to the browser alongside a biscuit and to the issuing provider, single-use and expiring. Automatic document linking requires the two a provider-asserted verified email and that the provider is listed in Account.AccountLinking.TrustedProviders — an unverified or self-set location at the IdP cannot obtain complete an existing local account.

CSRF. State-changing requests are origin-checked against BaseURL + TrustedOrigins (wildcard subdomains supported), falling rear to Sec-Fetch-Site whenever no Origin/Referer is present.

Sign-in guards. Every sign-in way — password, magic link, social, verification auto-login — funnels through SignInUser, so a plugin implementing SignInGuard (two-factor, bans) cannot be bypassed by choosing another method. SessionGuard additionally re-checks all request, so a ban takes consequence immediately fairly than at next login.

ID tokens. The native "sign in alongside X" way verifies the token's signature against the issuer's published JWKS and checks iss, aud (plus azp for multi-audience tokens), exp and the nonce — the client mints a single-use nonce at POST /id-token/nonce, passes it to the provider SDK, and the signed token must echo it, so a token captured elsewhere cannot be replayed current (Advanced.DisableIDTokenNonceCheck restores the old behavior for SDKs that cannot set one). Keys are cached alongside a bounded stale-serve window, and a unsuccessful fetch cannot be induced by a client to obstacle key rotation.

Secrets at rest. TOTP secrets, backup codes and JWT personal keys are AES-256-GCM encrypted alongside a key derived from your Secret; OAuth tokens too whenever Account.EncryptOAuthTokens is set. Encryption nonaccomplishment is an error, never a silent plaintext write, and decryption nonaccomplishment is an error too — never a fallback that returns the ciphertext. Every ciphertext is bound to anywhere it is stored — its model, document id and site name are authenticated alongside the value — so a value copied from one encrypted pillar into another does not decrypt, and repository compose admission cannot be turned into a peruse oracle for person else's secrets. Ciphertexts additionally transport a key identifier, so Secret can be rotated: see Rotating the secret.

Audit trail. Every security-relevant event — sign-in achievement and nonaccomplishment alongside the reason, sign-out, meeting innovation and revocation, password and email changes, document link/unlink and deletion, 2FA enable/disable, bans, and director impersonation start/stop — is emitted as a typed godevauth.Event. Events never merge passwords, meeting tokens or one-time tokens; there is no free-form field, and a test asserts the asset by reflection. See Audit events.

Config.Secret derives the key that encrypts values at rest. Changing it used to be unrecoverable — all enrolled 2FA person locked out permanently, JWT signing keys unreadable — since the ciphertext stated nothing concerning which key wrote it. Stored values are versioned and tagged alongside a key identifier:

v2.<kid>.<base64url(nonce || AES-256-GCM(plaintext))> 

The GCM additional data covers the version, the key id, and the value's location — model, document id, site name — so a ciphertext lone opens in the pillar of the row it was written to.

Two before formats be in deployed databases and are motionless read, so upgrading needs no migration:

Format Shape Key derivation Location-bound
v0 bare base64url(nonce‖ct), no . sha256("go-dev-auth-enc:"+secret) no
v1 v1.<kid>.<body> scrypt no
v2 v2.<kid>.<body> scrypt yes

Rotation — and the v0/v1 → v2 migration, which uses the identical continue — is a three-step deploy:

// 1. Deploy: new concealed current, old one motionless readable. Secret: os.Getenv("AUTH_SECRET"), // the new value PreviousSecrets: []string{os.Getenv("AUTH_SECRET_OLD")},
// 2. Migrate. Safe to run against a live instance; re-run until Done. result, err := auth.ReencryptSecrets(ctx) // result.Rewritten — values moved to the current key and format // result.Unreadable — values no key can read; examine before stage 3 // result.Skipped — rows a concurrent compose changed mid-pass; re-run // result.Failed — compose errors; re-run formerly the logic is fixed // result.Done() — nothing remaining to do
// 3. Deploy again alongside PreviousSecrets empty. // Once stage 2 reports Done, additionally set: RequireBoundCiphertexts: true,

ReencryptSecrets covers OAuth tokens on the document array and calls all plugin implementing godevauth.SecretRotator (two-factor, jwt) for their own tables. It walks all array in batches of 200 in id command fairly than loading it whole, and all compose is a compare-and-set on the ciphertext it peruse — so a token refreshed or a backup-code set regenerated mid-pass is never reverted, lone skipped. One plugin's rotator failing does not halt the others. It is idempotent: a continue that reports Done() method nothing is remaining under an old key or in an unbound format.

RequireBoundCiphertexts is the second fractional of the fix. Until it is set, the unbound v0 and v1 formats are motionless accepted on read, which method a ciphertext captured from an old backup can motionless be relocated between columns. Turning it on before the immigration finishes does not endure data — clearing it makes those values readable again — but users whose values have not been migrated cannot authenticate during it is set. So: upgrade, migrate to Done(), afterward rotate it on.

Skip stage 1 and the archive volition not guess. A stored TOTP concealed it cannot peruse is a 500 TWO_FACTOR_SECRET_UNREADABLE, not a "wrong code"; an unreadable OAuth token is a 500 TOKEN_DECRYPTION_FAILED, not an enc:… blob forwarded to the provider; and /jwks keeps publishing all community key regardless, so tokens already in circulation remain verifiable during you fix the configuration.

Note that biscuit and token signatures are not versioned. Rotating Secret invalidates existing signed cookies — users are signed out — any PreviousSecrets says.

There is one hook and one type:

Events: godevauth.EventsConfig{ Handler: func(ctx context.Context, e *godevauth.Event) { // e.Type, e.Outcome, e.Reason, e.ActorID, e.TargetID, // e.Email, e.SessionID, e.Method, e.Action, // e.ClientIP, e.UserAgent, e.RequestMethod, e.RequestPath siem.Send(e) }, },

Notable properties:

  • On by default. With no Handler, events go to Config.Logger — successes at Info, failures at Warn. An audit trail that have to be switched on is missing from exactly the deployments that need it. Set Events.DisableDefaultLogging to opt out (events contain the subject's email address, which may not pertain in average use logs).
  • Failure reasons are additional particular than the HTTP response. Sign-in answers INVALID_EMAIL_OR_PASSWORD to the client so it cannot be used to enumerate accounts; the event distinguishes unknown_user from invalid_password from banned from two_factor_required, which is what tells credential stuffing distinct from password guessing.
  • 429s are recorded. Ctx.Error lone logs 5xx, so a throttled brute-force attempt alternatively leaves no server-side trace at all.
  • RequestPath is the path pattern, e.g. /reset-password/:token — never the resolved path, which contains the token.
  • Impersonation is bracketed. While it is energetic all act is recorded against the impersonated user, so admin.impersonation_started / admin.impersonation_stopped are the lone thread tying those actions rear to the administrator.

Plugins emit their own alongside auth.EmitEvent(c, godevauth.Event{...}).

  • Cleanup: auth.CleanupExpired(ctx) or auth.StartCleanup(ctx, time.Hour). Expired verification rows and sessions are alternatively never collected.
  • Behind a proxy: set Advanced.TrustProxyHeaders and Advanced.TrustedProxies. See the quick start — the default is an outage waiting to happen rearward a burden balancer.
  • Rotating Secret: three-step deploy alongside Config.PreviousSecrets and auth.ReencryptSecrets. See Rotating the secret.
  • Upgrading to border ciphertexts: run auth.ReencryptSecrets until it reports Done(), afterward deploy alongside Config.RequireBoundCiphertexts: true. Same section.
  • Audit trail: on by default to Config.Logger; path it alongside Events.Handler. See Audit events.
  • Multi-instance: provision a shared RateLimit.Storage; everything alternatively is stateless.
  • Session biscuit cache: it is bypassed automatically whenever a plugin registers a SessionGuard (the admin plugin's ban inspect does). Serving authority decisions from a cached copy of the person would let a ban sit unnoticed for the cache window.
  • Migrations: see Migrations below.
  • Introspection: auth.Routes() lists all registered endpoint; auth.Schema() the complete schema.

Plugins do not lone add tables (passkey and sso all bring one). They additionally add columns to the tables you already have: admin adds role, banned, banReason and banExpires to person and impersonatedBy to session, twofactor adds twoFactorEnabled to user, institution adds activeOrganizationId to session, and Config.User.AdditionalFields does the same. A archive upgrade can add a center pillar the identical way.

CREATE TABLE IF NOT EXISTS does nothing for a array that is already there, so a column-level stage is not optional on a repository that has data in it. store.Migrate(ctx) does both:

store.Migrate(ctx) Creates missing tables and indexes, and adds missing columns to existing tables. Idempotent — call it on all boot. Auto-migration in godevauth.New runs exactly this.
store.MigrationSQL() The complete create-from-nothing DDL, for provisioning a new repository by hand. Never contains an ALTER; it has no idea what your repository already has.
store.PendingMigrationSQL(ctx) Introspects the live repository and returns only the statements it is missing — CREATE TABLE for absent tables, ALTER TABLE ... ADD COLUMN for absent columns. Empty method up to date.
store.CheckSchema(ctx) Reports drift as an error naming the tables and columns, without changing anything.

Two things to cognize concerning added columns:

  • They are continually nullable, any Field.Required says. The rows already in the array have no value for a pillar that did not exist, and no motor volition obtain NOT NULL without a default on a populated table. A required pillar added by a immigration is hence enforced by the application, not the database, until you backfill the rows and tighten it yourself.
  • Migrate needs DDL rights. If your application's repository function does not have them, run migrations from a deploy step, set Advanced.DisableAutoMigrate, and set Advanced.VerifySchema so a missed immigration fails at startup alongside a communication naming the missing columns — alternatively of at the archetypal sign-in, alongside a controller error.
auth, err := godevauth.New(godevauth.Config{ Database: store, Advanced: godevauth.AdvancedConfig{ DisableAutoMigrate: true, // migrations are a deploy step VerifySchema: true, // ...so refuse to commencement if one was missed }, // ... }) // go-dev-auth: verifying the repository schema: sqlstore: the repository schema is // out of date: array "twoFactor" is missing; array "user" is missing column(s) // "role", "banned", "banReason", "banExpires"

Measured on a 4-core arm64 receptacle (go test -bench .) against the in-memory adapter. That is an honest measure of the library's own petition way and a mediocre proxy for your manufacturing latency, anywhere a repository round-trip volition dominate all row below apart from password hashing. Read these as "what the archive adds", not "what a sign-in costs".

Operation Cost
Session validation (cookie -> user) ~0.6 µs parallel, 21 allocs
Full HTTP petition through the router ~4 µs, 47 allocs
Session lookup, 10k rows (memory adapter) ~0.5 µs (indexed)
Password hash / verify ~51 ms, 21 KB

Everything apart from password hashing is microseconds. Where the prosperity really goes, in order:

1. Password hashing dominates CPU. ~51 ms per sign-in method approximately sign-ins/sec ÷ 20 CPU cores. At 100 sign-ins/sec that is ~5 cores doing nothing but scrypt. This is deliberate — it is what makes taken hashes costly to expert — so the lever is not to create it cheaper but to do it small often: sessions final 7 days by default, so a signed-in person never touches it again.

2. Database circular trips dominate everything else. An authenticated petition expenses two queries (session, afterward user), which on a managed repository is ~1 ms all — a thousand times the CPU disbursal of the petition itself. Turn on the meeting biscuit cache to create that zero:

Session: godevauth.SessionConfig{ CookieCache: godevauth.CookieCacheConfig{Enabled: true, MaxAge: 5 * time.Minute}, },

The commerce is staleness: a revoked meeting keeps operating until the cached copy expires. With a SessionGuard plugin registered (the admin plugin's ban inspect is one) the cache is bypassed by default fairly than serving authority decisions from a old user; set CookieCache.AcceptStaleAuthorization to obtain the preservation anyhow and obtain that bans use inside MaxAge.

3. Memory under a sign-in burst is bounded, not proportional to traffic. Each in-flight hash needs 128·N·r = 32 MiB of scratch, so concurrency is capped at GOMAXPROCS and buffers are pooled: highest transient recollection is GOMAXPROCS × 32 MiB no matter how many requests arrive. Requests that cannot get a slot inside MaxWait (3s) are shed alongside 503 SERVICE_BUSY alternatively of queueing into a multi-second backlog that clients cognition as a hang.

Tuning the hash disbursal is imaginable but changes the security/cost commerce and makes existing hashes unverifiable:

// Fewer cores per sign-in, small opposition to offline cracking. // Fresh deployments only. crypto.NewScryptHasher(crypto.ScryptParams{N: 16384, R: 8, P: 1, MaxConcurrent: 8})
. the godevauth package: config, routing, handlers .github/ CI, templates, CONTRIBUTING, SECURITY storage/ persistence contract, models, schema storage/memory in-memory adapter storage/sqlstore PostgreSQL / MySQL / SQLite storage/mongostore MongoDB (separate module) storage/storagetest adapter conformance suite crypto/ password hashing, tokens, TOTP, JWT oauth2/ OAuth 2.0 / OIDC client providers/ ready-made provider configurations plugins/<name>/ optional features, one bundle each plugins/plugintest harness for evaluation a plugin ratelimit/ charge limiter and shop interface examples/basic a runnable server docs/ architecture map, plugin guide, reviews 

The base directory is flat since Go requires all document of a package to portion one directory: these records are the godevauth package, and nesting them would average either changing the import way or splitting the package. Everything that can be its own bundle already is. Files are named for what they clasp — session.go, router.go, handler_email.go — so the listing says as a array of contents.

docs/architecture.md is the complete map: what each file owns, and anywhere a new alter belongs.

go activity init . ./storage/mongostore ./storage/sqlstore/integration make inspect # fmt, vet, lint, tests, race 

make assistance lists all target. go.work is developer-local and not committed. The SQLite integration tests need CGO_ENABLED=1; the Postgres, MySQL and MongoDB suites skip unless the matching DSN environment variables are set.

See CONTRIBUTING.md for expectations on changes, SECURITY.md for the safety example and reporting process, and docs/ for the architecture map and audit records.

Working alongside an AI coding assistant? llms.txt is a single-file briefing on the API and the rules that keep generated code correct; AGENTS.md covers agents contributing to this repository.

MIT

Other Article Hacker News
↑
Close Right Ads
Close Left Ads