go-dev-auth/go-dev-auth: go-dev-auth The complete authentication library for Go. One config struct. Batteries included. Personal your customers. · GitHub

The CI badge covers the entire matrix on each push: construct and checks on
the oldest supported and present Go, the race detector, lint, a fuzz
cross over the attacker-facing parsers, and the storage conformance
suite towards actual SQLite, PostgreSQL and MySQL servers.
A complete, framework-agnostic authentication library for Go, modeled after better-auth. Email & password, social sign-on, periods, account linking, two-factor auth, passkeys, magic hyperlinks, organizations, SSO, admin tooling, API keys and JWT — with zero exterior dependencies (pure normal library).
go get github.com/go-dev-auth/go-dev-auth
Status: pre-1.0, in manufacturing use, heading to v1.0. The library runs in manufacturing in an actual automation mission on PostgreSQL, utilizing the complete characteristic set. Releases comply with SemVer: whereas pre-1.0 the general public API should still change between minor variations (pin an actual model); v1.0 freezes it, and from then on breaking modifications require a serious model. The remaining v1.0 gates are runnable examples, discipline mileage on the newly automated MySQL leg, and a third-party safety evaluate. See storage adapters for precisely which backends are verified the place, and docs/security-model.md for the risk mannequin.
Core
- Email & password authentication (scrypt hashing, suitable with better-auth’s hash format)
- Social sign-on by way of OAuth 2.0 / OIDC with PKCE — Google, GitHub, Discord, Facebook, Microsoft, Apple, GitLab, LinkedIn, Spotify, Twitch, X in-built, plus a declarative
oauth2.Specfor any customized supplier - Database-backed periods with sliding expiration, signed cookies, optionally available cookie caching, record/revoke endpoints
- Email verification, password reset, change electronic mail/password, delete person flows
- Account linking & unlinking (trusted suppliers, token refresh, account data)
- CSRF origin checking, trusted origins with wildcard subdomains, IP-based price limiting
- Storage adapter interface with built-in
database/sql(Postgres, MySQL, SQLite) and in-memory adapters, plus schema/migration SQL era - Request hooks, database hooks, customized person/session fields
Plugins (mirroring better-auth’s plugin system)
twofactor— TOTP, electronic mail OTP and backup codespasskey— WebAuthn passkeys, CBOR/COSE parsing included (no exterior dependency)magiclink— passwordless electronic mail hyperlinksgroup— orgs, members, roles, invites, groupssso— bring-your-own OIDC identification supplier, matched by electronic mail areaadmin— person administration, bans, roles, impersonationapikey— hashed API keys with scopes that authenticate like periodsjwt— EdDSA-signed JWTs + JWKS endpointbearer— Authorization header auth for non-browser shoppers
package deal most important
import (
"context"
"web/http"
"os"
"time"
godevauth "github.com/go-dev-auth/go-dev-auth"
"github.com/go-dev-auth/go-dev-auth/storage/reminiscence"
)
func most important() {
auth, err := godevauth.New(godevauth.Config{
BaseURL: "http://localhost:8080", // required
Secret: os.Getenv("AUTH_SECRET"), // required, 32+ random chars
Database: reminiscence.New(), // required
EmailAndPassword: godevauth.EmailPasswordConfig{Enabled: true},
// Required in manufacturing if something sits in entrance of this
// course of (load balancer, ingress, CDN). See beneath.
Advanced: godevauth.AdvancedConfig{
TrustProxyHeaders: true,
TrustedProxies: []string{"10.0.0.0/8"},
},
})
if err != nil {
panic(err)
}
// sweep expired one-time tokens and periods
defer auth.BeginCleanup(context.Background(), time.Hour)()
mux := http.NewServeMux()
mux.Handle("/api/auth/", auth.Handler())
http.Pay attentionAndServe(":8080", mux)
}
The handler is a plain http.Handler, so it mounts on chi, echo, gorilla, or gin (by way of gin.WrapH) the identical means. If your frontend runs on a distinct origin, wrap it: auth.CORS(auth.Handler()) and record that origin in TrustedOrigins.
New validates the configuration and returns an error relatively than beginning up in an unsafe state: a lacking/quick Secret, a lacking or relative BaseURL (it decides cookie safety, trusted origins and redirect targets), SameSite=none with out safe cookies, or TrustProxyHeaders with out TrustedProxies are all rejected.
Rate limiting and each recorded session IP rely on resolving the shopper’s tackle. If something terminates the connection in entrance of this course of — an AWS ALB, nginx, Cloudflare, a Kubernetes ingress — you should say so, as a result of each defaults are flawed in a distinct route:
| Configuration | What occurs |
|---|---|
| Nothing set, however working behind a proxy | Every request resolves to the proxy’s tackle, so all shoppers share one bucket. The documented strict rule of three sign-ins per 10 seconds turns into 3 per 10 seconds to your complete fleet, and the limiter fails closed. This is a self-inflicted outage. |
TrustProxyHeaders with no TrustedProxies |
X-Forwarded-For is client-supplied. Any caller picks its personal bucket and the restrict stops limiting something. |
TrustProxyHeaders + TrustedProxies |
Correct. Forwarded headers are learn solely from a listed peer, and the header chain is walked right-to-left previous trusted hops, so addresses a shopper prepended are ignored. |
So:
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 out if TrustProxyHeaders (or IPAddressHeaders) is ready with out TrustedProxies. Use TrustedProxies: []string{"*"} to belief any peer — that’s the outdated, spoofable behaviour, and it is just secure when the community ensures the service is unreachable besides by a proxy that overwrites the header.
The reverse mistake can’t be caught at startup, as a result of it is dependent upon the visitors. The first request that arrives with a forwarded header whereas proxy headers are untrusted logs an error naming the header and the peer. If you see it in manufacturing, you might be in row one in every of that desk.
Direct-to-internet deployments want none of this: depart all three unset.
| Adapter | Import | Notes |
|---|---|---|
| In-memory | storage/reminiscence |
Tests, examples, single-process. Enforces distinctive constraints and indexes distinctive fields. |
| SQL | storage/sqlstore |
PostgreSQL, MySQL, SQLite by way of database/sql. Bring your individual driver. |
| MongoDB | storage/mongostore |
Official mongo-go-driver. Separate Go module. |
All adapters are written towards the identical conformance suite (storage/storagetest), which pins the semantics the auth core is dependent upon: unique-violation reporting, NULL vs zero, chronological sorting, clause folding, literal case-insensitive substring matching and compare-and-set replace counts.
What has really been executed, as of this revision:
- SQLite — the complete conformance suite and a whole HTTP auth circulate run towards an actual SQLite database in CI on each push. Verified.
- PostgreSQL — working in manufacturing (an actual automation mission makes use of the entire library on dwell PostgreSQL), and the conformance suite, migration idempotence and the complete HTTP auth circulate now run towards an actual PostgreSQL server in CI on each push. Field-proven and regression-guarded.
- MySQL — the identical CI job runs the conformance suite, migrations and the complete auth circulate towards an actual MySQL 8 server on each push. CI-verified; no discipline mileage but, so report something you hit.
- MongoDB — exercised towards an in-house wire-protocol take a look at double, not a dwell
mongod. Beta.
The CI legs can’t cross vacuously: the surroundings units REQUIRE_DSN=1, which turns a lacking database right into a take a look at failure relatively than a skip.
If you write your individual adapter, run that suite towards 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/choices"
"github.com/go-dev-auth/go-dev-auth/storage/mongostore"
)
shopper, err := mongo.Connect(choices.Client().ApplyURI(os.Getenv("MONGODB_URI")))
retailer := mongostore.New(shopper.Database("myapp"))
auth, err := godevauth.New(godevauth.Config{Database: retailer, /* ... */})
New creates the indexes the schema implies, together with the distinctive ones. That step just isn’t beauty on MongoDB: collections are created lazily, so an occasion with out them seems wholesome whereas each “does this electronic mail exist already?” examine degrades right into a race. Use Advanced.DisableAutoMigrate provided that you create them in a separate deploy step.
The adapter maps the id discipline onto Mongo’s _id, rejects composite values the place MongoDB would learn them as operators (the NoSQL-injection form), escapes regex metacharacters in substring searches, and stories duplicate keys as storage.ErrUniqueViolation. Transaction wants a duplicate 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" // convey your individual driver
)
db, _ := sql.Open("pgx", os.Getenv("DATABASE_URL"))
retailer := sqlstore.New(db, sqlstore.Postgres, nil)
auth, _ := godevauth.New(godevauth.Config{
Secret: os.Getenv("AUTH_SECRET"),
Database: retailer,
// ...
})
// choose up the plugin tables *and columns*, then apply them
retailer.SetSchema(auth.Schema())
if err := retailer.Migrate(context.Background()); err != nil { ... }
// or hand the DDL to your individual migration instrument:
fmt.Println(retailer.MigrationSQL()) // the entire schema, from nothing
pending, _ := retailer.PendingMigrationSQL(ctx) // solely what this database lacks
Migrate is idempotent and does two issues: it creates lacking tables and indexes, and it provides lacking columns to tables that exist already. The second issues as quickly because the database has knowledge in it — see Migrations.
import (
"github.com/go-dev-auth/go-dev-auth/oauth2"
"github.com/go-dev-auth/go-dev-auth/suppliers"
)
godevauth.Config{
SocialProviders: []oauth2.Provider{
suppliers.Google(suppliers.Credentials{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
}),
suppliers.GitHub(suppliers.Credentials{
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
}),
},
}
Set the supplier redirect URI to {BaseURL}/api/auth/callback/{supplier}. Custom suppliers are a single oauth2.New(oauth2.Spec{...}) name.
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/group"
"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(),
group.New(group.Options{Teams: true}),
admin.New(),
jwt.New(),
magiclink.New(magiclink.Options{
SendMagicHyperlink: func(ctx context.Context, electronic mail, url, token string) error {
return mailer.Send(electronic mail, "Sign in", url)
},
}),
passkey.New(),
sso.New(sso.Options{
// gate who might register identification suppliers
Authorize: func(c *godevauth.Ctx, sd *godevauth.SessionKnowledge) error {
return myApp.RequireAdmin(sd)
},
}),
},
}
Passkeys (plugins/passkey) add WebAuthn registration and sign-in with no exterior dependency: the CBOR/COSE parsing and ES256/RS256/Ed25519 signature verification dwell within the package deal. Registration wants a recent session; sign-in makes use of discoverable credentials and runs by SignInUser, so bans and two-factor coverage nonetheless apply. The signature counter is checked for the cloned-authenticator case.
SSO (plugins/sso) lets every group convey its personal OpenID Connect identification supplier (Okta, Microsoft Entra, Google Workspace, Keycloak). Providers are registered at runtime, matched to customers by electronic mail area, and the sign-in runs by the identical OAuth circulate as social login — browser-bound single-use state, PKCE, ID-token verification towards the issuer’s JWKS. Client secrets and techniques are encrypted at relaxation; administration endpoints fail closed till Options.Authorize is ready.
Writing your individual plugin means implementing three strategies (ID, Init, Routes) and optionally Schema, Middleware, Earlier thanRequest/AfterRequest, SignInGuard (veto or problem a sign-in on each path) or SessionGuard (re-check each 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
}
All endpoints dwell underneath Config.BasePath (default /api/auth) and match better-auth’s route names:
| Area | Endpoints |
|---|---|
| Email & password | POST /sign-up/electronic mail, POST /sign-in/electronic mail, 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 affirmation web page as a substitute of consuming the token when EmailVerification.ConfirmationPage is ready), 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/{allow,disable,get-totp-uri,verify-totp,send-otp,verify-otp,generate-backup-codes,verify-backup-code} |
| Magic hyperlink | POST /sign-in/magic-link, GET /magic-link/confirm |
| 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/record, POST /sso/delete, `GET |
| Organization | POST /group/{create,replace,delete,set-active,invite-member,accept-invitation,reject-invitation,cancel-invitation,remove-member,update-member-role,depart,check-slug,create-team,remove-team}, GET /group/{record,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,replace,delete,confirm}, GET /api-key/{get,record} |
| JWT | GET /token, GET /jwks, GET /.well-known/jwks.json |
Errors are returned as {"code": "USER_ALREADY_EXISTS", "message": "..."} with matching HTTP standing codes. A recognized path with the flawed methodology returns 405 with an Allow header.
godevauth.Config mirrors better-auth’s choices: EmailAndPassword (min/max size, verification necessities, reset supply, customized PasswordHasher), EmailVerification (supply, required-before-sign-in, optionally available interstitial ConfirmationPage so electronic mail scanners can’t eat the hyperlink), Session (ExpiresIn, UpdateAge, FreshAge, cookie cache), User (further fields, change-email — approval goes to the present verified tackle, then the brand new tackle is verified earlier than the change — delete-user), Account (linking guidelines, token encryption at relaxation), Advanced (cookie prefix, cross-subdomain cookies, SameSite, proxy belief, customized ID era, CSRF exemptions), TrustedOrigins, RateRestrict (home windows, per-path guidelines, pluggable retailer), Events (the audit hook), EarlierSecrets (secret rotation), Hooks and DatabaseHooks.
Rate-limit guidelines in RateRestrict.CustomGuidelines could also be keyed by the route sample ("/reset-password/:token") in addition to by a literal path. Buckets are keyed by the sample, so a parameterised route is proscribed as one endpoint relatively than one bucket per parameter worth.
A runnable demo with most plugins enabled lives in examples/basic:
Extra columns are declared on the config and are not client-writable until you say so. This is the distinction between a profile discipline and a privilege:
User: godevauth.UserConfig{
ExtraFields: []storage.Field{
{Name: "showName", Type: storage.FieldString, Input: true}, // person might set it
{Name: "plan", Type: storage.FieldString}, // server-controlled
},
},
Fields contributed by plugins (position, banned, twoFactorEnabled, …) are by no means writable from a request physique, no matter Input says.
Passwords. scrypt (N=16384, r=16, p=1), better-auth’s salt:key hex format. Each hash prices ~50 ms of CPU and ~32 MiB of scratch reminiscence by design; the hasher bounds concurrency (default GOMAXPROCS) and swimming pools its buffers so a burst of sign-ins can’t exhaust reminiscence. Tune by way of crypto.NewScryptHasher. Hash format is byte-for-byte suitable with better-auth for ASCII passwords; non-ASCII passwords not already in Unicode NFKC type can differ, as a result of better-auth normalizes to NFKC first and the usual library has no NFKC implementation to match with out including a dependency (see ScryptHasher.Hash).
Rate limiting is on by default (fail-closed) as a result of the sign-in endpoint is pricey by building. Set RateRestrict.Disabled solely when a gateway already throttles these paths, and provide RateRestrict.Storage when working multiple occasion.
Sessions. 32-byte random tokens, HMAC-signed cookies with area separation and __Secure- prefixes on HTTPS. Raw tokens are by no means included in session listings. The optionally available cookie cache carries an absolute revalidation deadline that’s by no means prolonged from cached knowledge, so revocation all the time takes impact inside CookieCache.MaxAge.
One-time tokens (password reset, electronic mail verification, magic hyperlinks, deletion, OAuth state) are saved as SHA-256 digests and consumed atomically, so database learn entry yields no usable hyperlinks.
OAuth. PKCE (S256), state pinned to the browser with a cookie and to the issuing supplier, single-use and expiring. Automatic account linking requires each a provider-asserted verified electronic mail and that the supplier is listed in Account.AccountLinking.TrustedProviders — an unverified or self-set tackle on the IdP can’t take over an current native account.
CSRF. State-changing requests are origin-checked towards BaseURL + TrustedOrigins (wildcard subdomains supported), falling again to Sec-Fetch-Site when no Origin/Referer is current.
Sign-in guards. Every sign-in path — password, magic hyperlink, social, verification auto-login — funnels by SignInUser, so a plugin implementing SignInGuard (two-factor, bans) can’t be bypassed by selecting one other methodology. SessionGuard moreover re-checks each request, so a ban takes impact instantly relatively than at subsequent login.
ID tokens. The native “register with X” path verifies the token’s signature towards the issuer’s printed JWKS and checks iss, aud (plus azp for multi-audience tokens), exp and the nonce — the shopper mints a single-use nonce at POST /id-token/nonce, passes it to the supplier SDK, and the signed token should echo it, so a token captured elsewhere can’t be replayed right here (Advanced.DisableIDTokenNonceCheck restores the outdated behaviour for SDKs that can’t set one). Keys are cached with a bounded stale-serve window, and a failed fetch can’t be induced by a shopper to dam key rotation.
Secrets at relaxation. TOTP secrets and techniques, backup codes and JWT non-public keys are AES-256-GCM encrypted with a key derived out of your Secret; OAuth tokens too when Account.EncryptOAuthTokens is ready. Encryption failure is an error, by no means a silent plaintext write, and decryption failure is an error too — by no means a fallback that returns the ciphertext. Every ciphertext is sure to the place it’s saved — its mannequin, report id and discipline identify are authenticated alongside the worth — so a worth copied from one encrypted column into one other doesn’t decrypt, and database write entry can’t be become a learn oracle for anyone else’s secrets and techniques. Ciphertexts additionally carry a key identifier, so Secret could be rotated: see Rotating the secret.
Audit path. Every security-relevant occasion — sign-in success and failure with the rationale, sign-out, session creation and revocation, password and electronic mail modifications, account hyperlink/unlink and deletion, 2FA allow/disable, bans, and administrator impersonation begin/cease — is emitted as a typed godevauth.Event. Events by no means comprise passwords, session tokens or one-time tokens; there isn’t a free-form discipline, and a take a look at asserts the property by reflection. See Audit events.
Config.Secret derives the important thing that encrypts values at relaxation. Changing it was once unrecoverable — each enrolled 2FA person locked out completely, JWT signing keys unreadable — as a result of the ciphertext stated nothing about which key wrote it. Stored values are versioned and tagged with a key identifier:
The GCM further knowledge covers the model, the important thing id, and the worth’s location — mannequin, report id, discipline identify — so a ciphertext solely opens within the column of the row it was written to.
Two earlier codecs exist in deployed databases and are nonetheless learn, so upgrading wants no migration:
| Format | Shape | Key derivation | Location-bound |
|---|---|---|---|
v0 |
naked base64url(nonce‖ct), no . |
sha256("go-dev-auth-enc:"+secret) |
no |
v1 |
v1. |
scrypt | no |
v2 |
v2. |
scrypt | sure |
Rotation — and the v0/v1 → v2 migration, which makes use of the identical cross — is a three-step deploy:
// 1. Deploy: new secret present, outdated one nonetheless readable.
Secret: os.Getenv("AUTH_SECRET"), // the brand new worth
EarlierSecrets: []string{os.Getenv("AUTH_SECRET_OLD")},
// 2. Migrate. Safe to run towards a dwell occasion; re-run till Done.
end result, err := auth.ReencryptSecrets(ctx)
// end result.Rewritten — values moved to the present key and format
// end result.Unreadable — values no key can learn; examine earlier than step 3
// end result.Skipped — rows a concurrent write modified mid-pass; re-run
// end result.Failed — write errors; re-run as soon as the trigger is mounted
// end result.Done() — nothing left to do
// 3. Deploy once more with EarlierSecrets empty.
// Once step 2 stories Done, additionally set:
RequireBoundCiphertexts: true,
ReencryptSecrets covers OAuth tokens on the account desk and calls each plugin implementing godevauth.SecretRotator (two-factor, jwt) for their very own tables. It walks every desk in batches of 200 in id order relatively than loading it complete, and each write is a compare-and-set on the ciphertext it learn — so a token refreshed or a backup-code set regenerated mid-pass is rarely reverted, solely skipped. One plugin’s rotator failing doesn’t cease the others. It is idempotent: a cross that stories Done() means nothing is left underneath an outdated key or in an unbound format.
RequireBoundCiphertexts is the second half of the repair. Until it’s set, the unbound v0 and v1 codecs are nonetheless accepted on learn, which implies a ciphertext captured from an outdated backup can nonetheless be relocated between columns. Turning it on earlier than the migration finishes doesn’t lose knowledge — clearing it makes these values readable once more — however customers whose values haven’t been migrated can’t authenticate whereas it’s set. So: improve, migrate to Done(), then flip it on.
Skip step 1 and the library is not going to guess. A saved TOTP secret it can’t learn is a 500 TWO_FACTOR_SECRET_UNREADABLE, not a “flawed code”; an unreadable OAuth token is a 500 TOKEN_DECRYPTION_FAILED, not an enc:… blob forwarded to the supplier; and /jwks retains publishing each public key regardless, so tokens already in circulation keep verifiable when you repair the configuration.
Note that cookie and token signatures are usually not versioned. Rotating Secret invalidates current signed cookies — customers are signed out — no matter EarlierSecrets says.
There is one hook and one sort:
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, occasions go toConfig.Logger— successes atInfo, failures atWarn. An audit path that needs to be switched on is lacking from precisely the deployments that want it. SetEvents.DisableDefaultLoggingto choose out (occasions embody the topic’s electronic mail tackle, which can not belong in abnormal utility logs). - Failure causes are extra particular than the HTTP response. Sign-in solutions
INVALID_EMAIL_OR_PASSWORDto the shopper so it can’t be used to enumerate accounts; the occasion distinguishesunknown_userfrominvalid_passwordfrombannedfromtwo_factor_required, which is what tells credential stuffing other than password guessing. - 429s are recorded.
Ctx.Errorsolely logs 5xx, so a throttled brute-force try in any other case leaves no server-side hint in any respect. RequestPathis the route sample, e.g./reset-password/:token— by no means the resolved path, which comprises the token.- Impersonation is bracketed. While it’s energetic each motion is recorded towards the impersonated person, so
admin.impersonation_started/admin.impersonation_stoppedare the one thread tying these actions again to the administrator.
Plugins emit their very own with auth.EmitEvent(c, godevauth.Event{...}).
- Cleanup:
auth.CleanupExpired(ctx)orauth.BeginCleanup(ctx, time.Hour). Expired verification rows and periods are in any other case by no means collected. - Behind a proxy: set
Advanced.TrustProxyHeadersandAdvanced.TrustedProxies. See the quick start — the default is an outage ready to occur behind a load balancer. - Rotating
Secret: three-step deploy withConfig.EarlierSecretsandauth.ReencryptSecrets. See Rotating the secret. - Upgrading to sure ciphertexts: run
auth.ReencryptSecretstill it storiesDone(), then deploy withConfig.RequireBoundCiphertexts: true. Same part. - Audit path: on by default to
Config.Logger; route it withEvents.Handler. See Audit events. - Multi-instance: provide a shared
RateRestrict.Storage; all the pieces else is stateless. - Session cookie cache: it’s bypassed routinely when a plugin registers a
SessionGuard(the admin plugin’s ban examine does). Serving authorization choices from a cached copy of the person would let a ban sit unnoticed for the cache window. - Migrations: see Migrations beneath.
- Introspection:
auth.Routes()lists each registered endpoint;auth.Schema()the complete schema.
Plugins don’t solely add tables (passkey and sso every convey one). They additionally add columns to the tables you have already got: admin provides position, banned, banReason and banExpires to person and impersonatedBy to session, twofactor provides twoFactorEnabled to person, group provides energeticOrganizationId to session, and Config.User.ExtraFields does the identical. A library improve can add a core column the identical means.
CREATE TABLE IF NOT EXISTS does nothing for a desk that’s already there, so a column-level step just isn’t optionally available on a database that has knowledge in it. retailer.Migrate(ctx) does each:
retailer.Migrate(ctx) |
Creates lacking tables and indexes, and provides lacking columns to current tables. Idempotent — name it on each boot. Auto-migration in godevauth.New runs precisely this. |
retailer.MigrationSQL() |
The full create-from-nothing DDL, for provisioning a brand new database by hand. Never comprises an ALTER; it has no concept what your database already has. |
retailer.PendingMigrationSQL(ctx) |
Introspects the dwell database and returns solely the statements it’s lacking — CREATE TABLE for absent tables, ALTER TABLE ... ADD COLUMN for absent columns. Empty means updated. |
retailer.TestSchema(ctx) |
Reports drift as an error naming the tables and columns, with out altering something. |
Two issues to find out about added columns:
- They are all the time nullable, no matter
Field.Requiredsays. The rows already within the desk don’t have any worth for a column that didn’t exist, and no engine will settle forNOT NULLwith out a default on a populated desk. A required column added by a migration is subsequently enforced by the applying, not the database, till you backfill the rows and tighten it your self. - Migrate wants DDL rights. If your utility’s database position doesn’t have them, run migrations from a deploy step, set
Advanced.DisableAutoMigrate, and setAdvanced.ConfirmSchemaso a missed migration fails at startup with a message naming the lacking columns — as a substitute of on the first sign-in, with a driver error.
auth, err := godevauth.New(godevauth.Config{
Database: retailer,
Advanced: godevauth.AdvancedConfig{
DisableAutoMigrate: true, // migrations are a deploy step
ConfirmSchema: true, // ...so refuse to start out if one was missed
},
// ...
})
// go-dev-auth: verifying the database schema: sqlstore: the database schema is
// outdated: desk "twoFactor" is lacking; desk "person" is lacking column(s)
// "position", "banned", "banReason", "banExpires"
Measured on a 4-core arm64 container (go take a look at -bench .) towards the in-memory adapter. That is an trustworthy measure of the library’s personal request path and a poor proxy to your manufacturing latency, the place a database round-trip will dominate each row beneath besides password hashing. Read these as “what the library provides”, not “what a sign-in prices”.
| Operation | Cost |
|---|---|
| Session validation (cookie -> person) | ~0.6 µs parallel, 21 allocs |
| Full HTTP request by the router | ~4 µs, 47 allocs |
| Session lookup, 10k rows (reminiscence adapter) | ~0.5 µs (listed) |
| Password hash / confirm | ~51 ms, 21 KB |
Everything besides password hashing is microseconds. Where the cash really goes, so as:
1. Password hashing dominates CPU. ~51 ms per sign-in means roughly sign-ins/sec ÷ 20 CPU cores. At 100 sign-ins/sec that’s ~5 cores doing nothing however scrypt. This is deliberate — it’s what makes stolen hashes costly to crack — so the lever is to not make it cheaper however to do it much less typically: periods final 7 days by default, so a signed-in person by no means touches it once more.
2. Database spherical journeys dominate all the pieces else. An authenticated request prices two queries (session, then person), which on a managed database is ~1 ms every — a thousand occasions the CPU price of the request itself. Turn on the session cookie cache to make that zero:
Session: godevauth.SessionConfig{
CookieCache: godevauth.CookieCacheConfig{Enabled: true, MaxAge: 5 * time.Minute},
},
The commerce is staleness: a revoked session retains working till the cached copy expires. With a SessionGuard plugin registered (the admin plugin’s ban examine is one) the cache is bypassed by default relatively than serving authorization choices from a stale person; set CookieCache.AcceptStaleAuthorization to take the saving anyway and settle for that bans apply inside MaxAge.
3. Memory underneath a sign-in burst is bounded, not proportional to visitors. Each in-flight hash wants 128·N·r = 32 MiB of scratch, so concurrency is capped at GOMAXPROCS and buffers are pooled: peak transient reminiscence is GOMAXPROCS × 32 MiB irrespective of what number of requests arrive. Requests that can’t get a slot inside MaxWait (3s) are shed with 503 SERVICE_BUSY as a substitute of queueing right into a multi-second backlog that shoppers expertise as a cling.
Tuning the hash price is feasible however modifications the safety/price commerce and makes current hashes unverifiable:
// Fewer cores per sign-in, much less resistance to offline cracking.
// Fresh deployments solely.
crypto.NewScryptHasher(crypto.ScryptParams{N: 16384, R: 8, P: 1, MaxConcurrent: 8})
. the godevauth package deal: config, routing, handlers
.github/ CI, templates, CONTRIBUTING, SECURITY
storage/ persistence contract, fashions, schema
storage/reminiscence 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 shopper
suppliers/ ready-made supplier configurations
plugins// optionally available options, one package deal every
plugins/plugintest harness for testing a plugin
ratelimit/ price limiter and retailer interface
examples/fundamental a runnable server
docs/ structure map, plugin information, evaluations
The root listing is flat as a result of Go requires each file of a package deal
to share one listing: these recordsdata are the godevauth package deal, and
nesting them would imply both altering the import path or splitting the
package deal. Everything that may be its personal package deal already is. Files are
named for what they maintain — session.go, router.go, handler_email.go
— so the itemizing reads as a desk of contents.
docs/architecture.md is the complete map: what every
file owns, and the place a brand new change belongs.
go work init . ./storage/mongostore ./storage/sqlstore/integration
make examine # fmt, vet, lint, checks, race
make assist lists each goal. go.work is developer-local and never
dedicated. The SQLite integration checks want CGO_ENABLED=1; the
Postgres, MySQL and MongoDB suites skip until the matching DSN
surroundings variables are set.
See CONTRIBUTING.md for expectations on modifications,
SECURITY.md for the safety mannequin and reporting course of,
and docs/ for the structure map and audit information.
Working with an AI coding assistant? llms.txt is a
single-file briefing on the API and the foundations that preserve generated code
appropriate; AGENTS.md covers brokers contributing to this
repository.
MIT
