go-idento

A complete, batteries-included identity framework for Go — user/role
management, password hashing, claims, lockout, two-factor and JWT, built on
pluggable stores so the persistence layer can be swapped without touching
business logic.
Status: beta (v0.6.0). User/role/sign-in managers, PBKDF2 hashing, JWT
(HS256/RS256/ES256) + JWKS, TOTP/SMS two-factor with recovery codes, external
logins, email-confirmation/password-reset tokens, and policy authorization are
implemented and tested. Four stores ship: in-memory, GORM, raw pgx, and a
sqlc-generated pgx store — with configurable schema/table names and
referential integrity (cascade delete + optimistic concurrency). The API may
still change before v1.0.
Docs: getting started ·
architecture ·
extending the user & migrations ·
customizing the schema ·
contributor/agent guide
Why
Go has JWT libraries and full identity providers (Ory, ZITADEL), but no
embedded, batteries-included identity toolkit you drop straight into your own
app and own your data. go-idento fills that gap with familiar building blocks —
UserManager / RoleManager / SignInManager over pluggable stores.
What you get
| Area |
API |
| Entities |
identity.User / identity.Role / identity.Claim |
| Users |
identity.UserManager (create, password add/change/remove, email & phone change, roles, claims, lockout, 2FA) |
| Guests |
CreateAnonymous / ConvertToRegistered (preserves ID) / PurgeAnonymousUsers GC |
| Roles |
identity.RoleManager (CRUD + role claims, optimistic concurrency) |
| Queries |
users-by-role / users-by-claim, paged ListUsers |
| Sign-in |
identity.SignInManager (password, 2FA, external, remember-this-machine, security-stamp validation) |
| API keys |
identity.APIKeyManager — opaque M2M bearer credentials (create/verify/revoke/list, non-expiring, hash-stored) |
| Two-factor |
TOTP + SMS + recovery codes, pluggable TwoFactorTokenProvider |
| Tokens |
identity.TokenService (JWT access + refresh, HS256/RS256/ES256) |
| Sessions |
multi-session refresh via WithSessionStore — one token per device, per-session revoke, MaxSessions |
| Delivery |
identity.SMSSender / identity.EmailSender (provider-agnostic) |
| Persistence |
identity.UserStore / identity.RoleStore interfaces |
| Password hashing |
identity.PasswordHasher (PBKDF2, versioned format) |
| Config |
identity.Options (password / lockout / user / sign-in policy) |
| HTTP |
auth.RequireAuth / auth.RequireRole / auth.RequirePolicy |
The default password hasher uses a versioned PBKDF2 format
(0x01 marker, PRF/iterations/salt-length header, PBKDF2-HMAC-SHA256); the
version byte lets parameters evolve while old hashes keep verifying.
Quick start
ctx := context.Background()
secret := []byte("change-me-32-byte-jwt-secret-min!")
db, _ := gorm.Open(sqlite.Open("app.db"), &gorm.Config{})
gormstore.Migrate(db)
users := identity.NewUserManager(gormstore.NewUserStore(db), identity.DefaultOptions())
signIn := identity.NewSignInManager(users)
tokens := identity.NewTokenService(users,
identity.DefaultTokenOptions(secret, "issuer", "audience"))
u := &identity.User{UserName: "jane", Email: "m@x.com"}
users.CreateWithPassword(ctx, u, "Abcdef1!")
res, signed := signIn.PasswordSignIn(ctx, "jane", "Abcdef1!", true)
if res.Succeeded {
pair, _ := tokens.IssuePair(ctx, signed) // access + refresh token
}
Run the demo server:
cd stores/gormstore && go run ./examples/httpserver
Modules
The repo is split so the core stays dependency-light: importing
github.com/terglos/go-idento pulls only golang-jwt, google/uuid and
golang.org/x/crypto — no ORM or DB driver. Each heavy store is its own module
you opt into:
| Module |
Import |
Extra deps |
| core |
github.com/terglos/go-idento (+/auth, /stores/memstore) |
jwt, uuid, x/crypto |
| GORM store |
github.com/terglos/go-idento/stores/gormstore |
gorm, drivers |
| pgx store |
github.com/terglos/go-idento/stores/pgxstore |
pgx |
| sqlc store |
github.com/terglos/go-idento/stores/pgxsqlc |
pgx |
Layout
identity/ core: entities, managers, hasher, options, JWT, signer/JWKS
auth/ HTTP middleware: Bearer + cookie, RequireAuth/RequireRole/RequirePolicy, JWKS
stores/memstore/ in-memory implementation (tests / prototyping) — in the core module
stores/gormstore/ [module] GORM (Postgres / MySQL / SQLite) + examples
stores/pgxstore/ [module] raw pgx (PostgreSQL) + demo
stores/pgxsqlc/ [module] sqlc-generated pgx (PostgreSQL)
demo/totp/ TOTP code helper (core only)
Extending the user
Four ways, smallest blast radius first (full analysis in
docs/design/extending-user-and-migrations.md):
// Option D — custom typed columns on the user row.
type AppUser struct {
identity.User // embeds -> Base()/TableName() promoted
TenantID string
}
db, _ := gorm.Open(sqlite.Open("app.db"), &gorm.Config{})
gormstore.MigrateOf[AppUser](db)
store := gormstore.NewUserStoreOf[AppUser](db)
um := identity.NewUserManagerOf[AppUser](store, identity.DefaultOptions())
um.CreateWithPassword(ctx, &AppUser{User: identity.User{UserName: "jane"}, TenantID: "acme"}, "Abcdef1!")
// Option C — schema-less JSON attributes (no custom type).
u.SetAttribute("tenant", "acme") // persisted in the attributes column
// Option B — claims (flow into the JWT). Option A — your own 1:1 extension table.
See stores/gormstore/examples/genericuser (Option D) and
stores/gormstore/examples/customfields (Options A & B).
Migrations
// No CLI: apply the canonical schema from Go.
migrations.ApplyPostgres(ctx, sqlDB)
For versioned, reviewable history use Atlas (atlas.hcl + migrations/):
atlas migrate diff <name> --env local generates SQL from schema changes — the
EF add-migration loop. goose / golang-migrate can run the same SQL.
Demos
stores/pgxstore/example/postgres — full flow
against a real PostgreSQL via the raw pgx store: docker compose up -d && go run .
then follow its README (register, login, JWT + cookie, refresh, role-gated route, TOTP 2FA).
stores/gormstore/examples/httpserver — minimal
zero-setup server on SQLite: cd stores/gormstore && go run ./examples/httpserver.
Features
- User/role management (
UserManager, RoleManager, SignInManager)
- PBKDF2 password hashing with a versioned wire format
- JWT access + refresh tokens with security-stamp revocation
- Multi-session refresh tokens (
TokenService.WithSessionStore): one
refresh session per device/browser (industry model — one row per token),
per-session rotation + sliding TTL, RevokeSession / global Revoke /
ListSessions, opportunistic GC, optional MaxSessions cap
- HS256, RS256 and ES256 signing via pluggable
Signer + RSAKeyring / ECDSAKeyring (kid rotation)
- JWKS endpoint (
auth.JWKSHandler) publishing RSA/EC public keys
- TOTP two-factor (RFC 6238) + one-time recovery codes
- Phone (SMS) two-factor via a pluggable
SMSSender
- Email confirmation, password-reset & change-email token providers
- External/OAuth login association (
AddLogin / FindByLogin / ExternalLoginSignIn)
- Opaque API keys (machine-to-machine): non-expiring, revocable,
hash-stored bearer credentials bound to a user —
CreateAPIKey /
VerifyAPIKey / RevokeAPIKey / ListAPIKeys, configurable prefix + hasher,
ImportAPIKey for zero-reissue migration, and auth.WithAPIKeys middleware
- Guest / anonymous identity:
CreateAnonymous, ConvertToRegistered
(promote in place, preserving the ID), and a PurgeAnonymousUsers GC sweep
- Account lockout
- Policy/claims authorization (
auth.Policy, RequirePolicy) beyond roles
- Stores: GORM (Postgres/MySQL/SQLite), raw
pgx, sqlc-generated pgx, in-memory
- Configurable schema: custom namespace / table prefix / table names per store
(
WithSchema/WithTablePrefix/WithTableNames) + ON DELETE CASCADE integrity (docs)
- Extensible user: custom columns via the generic
UserManagerOf[T]
(embed identity.User), a JSON Attributes bag, an extension table, or claims
- Migrations: zero-CLI
migrations.ApplyPostgres, plus Atlas config for
versioned, diff-generated migrations (goose/golang-migrate also supported)
Two-factor quick reference
key, _ := users.GetAuthenticatorKey(ctx, u) // provision (show as QR via identity.AuthenticatorURI)
users.SetTwoFactorEnabled(ctx, u, true)
res, u := signIn.PasswordSignIn(ctx, name, pw, true)
if res.RequiresTwoFactor {
res = signIn.TwoFactorAuthenticatorSignIn(ctx, u, totpCode)
}
codes, _ := users.GenerateRecoveryCodes(ctx, u, 10)
RS256 with key rotation
ring := identity.NewRSAKeyring("key-1", privKey)
tokens := identity.NewTokenService(users, identity.TokenOptions{
Signer: ring, Issuer: "issuer", Audience: "api",
AccessTokenTTL: 15*time.Minute, RefreshTokenTTL: 7*24*time.Hour,
})
ring.Add("key-2", newKey, true) // new tokens use key-2; key-1 tokens still verify
ring.Remove("key-1") // retire once no live tokens reference it
ES256 is identical — use identity.NewECDSAKeyring(kid, ecdsaKey).
Guest / anonymous identity
guest, _ := users.CreateAnonymous(ctx) // no email/password; IsAnonymous=true
pair, _ := tokens.IssuePair(ctx, guest) // issue a JWT like any user
// Later, promote in place — the ID (and any cart/claims keyed on it) is kept:
users.ConvertToRegistered(ctx, guest, "jane", "jane@x.com", "Abcdef1!")
// Reclaim abandoned guests from a cron/worker (cascades their satellite rows):
n, _ := users.PurgeAnonymousUsers(ctx, time.Now().Add(-24*time.Hour))
API keys (machine-to-machine)
Long-lived, opaque bearer credentials for callers that can't do interactive
login or token rotation (POS terminals, payment partners, webhooks). Bound to a
user, so roles/claims and the auth middleware apply unchanged.
keys := identity.NewAPIKeyManager(gormstore.NewAPIKeyStore(db), users).
WithAPIKeyPrefix("myapp-") // optional; default hasher is SHA-256
secret, key, _ := keys.CreateAPIKey(ctx, u, identity.APIKeyOptions{
Name: "POS terminal", Scopes: []string{"pay:write"}, // ExpiresAt nil = never expires
})
// `secret` is shown ONCE — only its hash + display prefix (key.Prefix) are stored.
// Authenticate requests: a Bearer value that isn't a JWT is tried as an API key.
mux := auth.Middleware(tokens, cookies, auth.WithAPIKeys(keys)) // Authorization: Bearer <secret>
owner, k, err := keys.VerifyAPIKey(ctx, secret) // err == identity.ErrInvalidAPIKey vs store error
keys.RevokeAPIKey(ctx, key.ID)
Migrating from an existing key system? Point the hasher at your current function
and ImportAPIKey(userID, name, prefix, keyHash, expiresAt, scopes...) your
already-issued hashes — every key stays valid with zero reissue.
JWKS endpoint
ring := identity.NewRSAKeyring("key-1", privKey) // or NewECDSAKeyring
mux.Handle("/.well-known/jwks.json", auth.JWKSHandler(ring)) // publishes public keys
Phone (SMS) two-factor
users := identity.NewUserManager(store, opts).WithSMSSender(mySMSSender)
users.SendPhoneToken(ctx, u) // delivers a 6-digit code
res := signIn.TwoFactorPhoneSignIn(ctx, u, code) // after PasswordSignIn returned RequiresTwoFactor
Roadmap
- WebAuthn / passkeys
- Backchannel logout / token introspection endpoint
License
Licensed under the Apache License 2.0 — permissive, with an explicit
patent grant. See NOTICE for attribution. Built by
Terglos.