Documentation
¶
Overview ¶
Package auth is Gombit's runtime Bearer JWT surface (C3 / M5-2).
Behavior lives here, not in generated apps. framework.New mounts the Huma routes when config.Auth.JWTSecret is set and a database is attached.
Access tokens are short-lived JWTs. Refresh tokens are opaque, stored hashed, and rotated on each successful refresh. The generated SPA keeps both in memory — never localStorage or sessionStorage. Cookie/session + CSRF (--auth cookie) is a first-class alternative (M5-3).
Users can receive stable permission keys directly or through groups. IsSuperuser bypasses every permission check.
Index ¶
- Constants
- Variables
- func AddUserToGroup(ctx context.Context, db *gorm.DB, user *User, group *Group) error
- func CSRFMiddleware(cfg config.Config, exemptPaths ...string) gin.HandlerFunc
- func GrantPermissionToGroup(ctx context.Context, db *gorm.DB, group *Group, permission *Permission) error
- func GrantPermissionToUser(ctx context.Context, db *gorm.DB, user *User, permission *Permission) error
- func HasPermission(ctx context.Context, db *gorm.DB, user User, key string) (bool, error)
- func HasPermissions(ctx context.Context, db *gorm.DB, user User, keys ...string) (map[string]bool, error)
- func Migrate(db *gorm.DB) error
- func Models() []any
- func Mount(api huma.API, db *gorm.DB, cfg config.Config) error
- type AuthSession
- type Clock
- type Group
- type Hasher
- type Permission
- type PublicUser
- type RefreshToken
- type Service
- func (s *Service) Authenticate(ctx context.Context, email, password string) (User, error)
- func (s *Service) CreateSuperuser(ctx context.Context, email, password string) (User, error)
- func (s *Service) IssueTokens(ctx context.Context, user User) (TokenPair, error)
- func (s *Service) ListSessions(ctx context.Context, userID uint) ([]AuthSession, error)
- func (s *Service) ParseAccess(ctx context.Context, token string) (User, error)
- func (s *Service) Register(ctx context.Context, email, password string) (User, error)
- func (s *Service) RequireCookieSession() func(ctx huma.Context, next func(huma.Context))
- func (s *Service) RevokeAllSessions(ctx context.Context, userID uint) error
- func (s *Service) RevokeRefresh(ctx context.Context, raw string) error
- func (s *Service) RevokeSession(ctx context.Context, userID uint, id string) error
- func (s *Service) RotateRefresh(ctx context.Context, raw string) (TokenPair, error)
- type TokenPair
- type User
Constants ¶
const ( // AccessCookieName holds the short-lived access JWT in cookie mode. AccessCookieName = "gombit_access" // RefreshCookieName holds the rotating refresh token in cookie mode. RefreshCookieName = "gombit_refresh" // CSRFCookieName holds the signed double-submit CSRF token. CSRFCookieName = "gombit_csrf" // CSRFHeaderName is the header clients must echo the CSRF cookie value // into for state-changing (POST/PUT/PATCH/DELETE) requests. CSRFHeaderName = "X-CSRF-Token" )
Cookie and CSRF header names for cookie-mode session auth (M5-3). See docs/auth-cookie.md for the threat model these pair with.
Variables ¶
var ErrEmailTaken = errEmailTaken
ErrEmailTaken is the exported form of errEmailTaken. Register and CreateSuperuser return it (wrapped) so callers such as gombit createsuperuser can detect duplicates with errors.Is without a second uniqueness check.
var ErrSessionNotFound = errSessionNotFound
ErrSessionNotFound is returned by RevokeSession when the id names no active session of the user, including a session that has refreshed since its id was listed or that a logout ended while the revocation ran, and by RevokeSession and RevokeAllSessions when the user does not exist (a user that is not there has no sessions).
Functions ¶
func AddUserToGroup ¶
AddUserToGroup adds a persisted user to a persisted group.
func CSRFMiddleware ¶
func CSRFMiddleware(cfg config.Config, exemptPaths ...string) gin.HandlerFunc
CSRFMiddleware enforces the double-submit CSRF defense documented in docs/auth-cookie.md for cookie-mode session auth (M5-3). It is a Gin middleware (not scoped to Huma auth routes) because CSRF must cover every state-changing request in the app, including future feature routes, not just the auth surface.
Safe methods (GET/HEAD/OPTIONS) bootstrap a signed CSRF cookie when one is missing. Other methods require a X-CSRF-Token header that matches the gombit_csrf cookie and verifies against cfg.Auth.JWTSecret; otherwise the request is rejected with a D10 403 authorization error.
framework.New wires this into the runtime middleware stack automatically when cfg.Auth.Enabled() and cfg.Auth.EffectiveMode() == AuthModeCookie.
exemptPaths are exact request paths that opt out of CSRF enforcement on unsafe methods — for non-browser endpoints (webhooks, server-to-server callbacks) that cannot participate in the double-submit defense and must authenticate themselves by other means (e.g. HMAC signature verification). Safe methods still bootstrap the cookie on those paths. See framework.WithCSRFExemptPaths and docs/auth-cookie.md.
func GrantPermissionToGroup ¶
func GrantPermissionToGroup(ctx context.Context, db *gorm.DB, group *Group, permission *Permission) error
GrantPermissionToGroup grants a persisted permission to a persisted group.
func GrantPermissionToUser ¶
func GrantPermissionToUser(ctx context.Context, db *gorm.DB, user *User, permission *Permission) error
GrantPermissionToUser grants a persisted permission directly to a user.
func HasPermission ¶
HasPermission reports whether user has key directly, through a group, or through the Django-style superuser bypass.
func HasPermissions ¶
func HasPermissions(ctx context.Context, db *gorm.DB, user User, keys ...string) (map[string]bool, error)
HasPermissions checks several keys while loading the user's associations once. Missing users and unassigned keys are reported as false.
func Mount ¶
Mount registers framework-owned auth Huma routes on api. The route shapes differ by cfg.Auth.EffectiveMode(): AuthModeJWT (default) mounts the Bearer token surface (register/login/refresh/logout/me exchanging JSON tokens); AuthModeCookie mounts the same operation IDs and paths but exchanges HttpOnly session cookies instead, plus GET /auth/csrf. The two modes are mutually exclusive per app — see docs/auth-cookie.md for the cookie/CSRF threat model.
Types ¶
type AuthSession ¶ added in v0.8.0
type AuthSession struct {
ID string `` /* 161-byte string literal not displayed */
LastRefreshedAt time.Time `` /* 157-byte string literal not displayed */
ExpiresAt time.Time `json:"expires_at" doc:"When the session ends unless it refreshes first"`
Current bool `json:"current" example:"true" doc:"True for the session that made this request"`
}
AuthSession is one active sign-in as the sessions API lists it. Huma names a schema after its Go type, in the one namespace every operation of the app shares, and two types with the same name panic at boot: the Auth prefix keeps it clear of an application's own Session type.
type Group ¶
type Group struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex;not null;size:100"`
Permissions []Permission `gorm:"many2many:auth_group_permissions;"`
CreatedAt time.Time
UpdatedAt time.Time
}
Group collects permissions that can be assigned to users.
type Hasher ¶
type Hasher interface {
Hash(password string) (string, error)
Compare(hash, password string) error
}
Hasher hashes and compares passwords.
type Permission ¶
type Permission struct {
ID uint `gorm:"primaryKey"`
Key string `gorm:"column:permission_key;uniqueIndex;not null;size:120"` // not "key": reserved in MySQL
Description string `gorm:"size:255"`
CreatedAt time.Time
UpdatedAt time.Time
}
Permission is a stable authorization key, such as admin.widgets.view.
func EnsurePermission ¶
func EnsurePermission(ctx context.Context, db *gorm.DB, key, description string) (Permission, error)
EnsurePermission returns the permission for key, creating it when needed.
type PublicUser ¶
type PublicUser struct {
ID uint `json:"id" example:"1" doc:"User identifier"`
Email string `json:"email" example:"ada@example.com" format:"email" doc:"Normalized email"`
}
PublicUser is the safe user representation returned by register and /me.
type RefreshToken ¶
type RefreshToken struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index;not null"`
TokenHash string `gorm:"uniqueIndex;size:64;not null"`
ExpiresAt time.Time `gorm:"index;not null"`
RevokedAt *time.Time `gorm:"index"`
ReplacedBy *uint
CreatedAt time.Time
}
RefreshToken is a hashed, rotating refresh credential.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the runtime auth implementation: users, password hashes, access JWTs, and rotating refresh tokens.
func NewService ¶
NewService constructs a Service. cfg.Auth.JWTSecret must be set.
func (*Service) Authenticate ¶
Authenticate verifies email and password.
func (*Service) CreateSuperuser ¶
CreateSuperuser creates a user with IsSuperuser set, for gombit createsuperuser (M4-6). It shares Register's hasher and uniqueness path; duplicate emails return errEmailTaken.
func (*Service) IssueTokens ¶
IssueTokens returns a new access JWT and a rotating refresh token.
func (*Service) ListSessions ¶ added in v0.8.0
ListSessions returns the user's active sessions, most recently refreshed first. When ctx comes from a request the auth middlewares authenticated, the request's own session has Current set, also when it refreshed while the request ran; otherwise none does.
func (*Service) ParseAccess ¶
ParseAccess validates an access JWT, from the Bearer header or the session cookie, and loads its user. It fails when the token's session has ended: its refresh token row was revoked or has expired.
func (*Service) RequireCookieSession ¶
RequireCookieSession is the exported cookie-session gate used by runtime packages such as admin. It authenticates the gombit_access cookie and stores the User on the request context for UserFromContext. It does not itself enforce CSRF; CSRFMiddleware (csrf.go) is wired as global Gin middleware ahead of it for state-changing requests.
func (*Service) RevokeAllSessions ¶ added in v0.8.0
RevokeAllSessions ends every active session of the user. It returns ErrSessionNotFound when the user does not exist. Like RevokeSession, it does not look at the caller's own session; the route checks the request's (revokeAllSessionsAs).
func (*Service) RevokeRefresh ¶
RevokeRefresh invalidates one refresh token. Missing/already-revoked tokens succeed so logout is idempotent.
func (*Service) RevokeSession ¶ added in v0.8.0
RevokeSession ends one of the user's active sessions. It returns ErrSessionNotFound when id names no active session of that user; a session that refreshed after id was listed has a new id, so the caller lists again rather than having the revoke silently miss. It also returns ErrSessionNotFound when the session's row turns out revoked by the time it is updated (a logout, which does not take the user's lock, ended it first), so a success always means this call ended the session. A user that does not exist also yields ErrSessionNotFound. It does not look at the caller's own session, which application code (an admin action revoking another user's session, say) does not have; the route checks the request's (revokeSessionAs).
func (*Service) RotateRefresh ¶
RotateRefresh validates the current refresh token, revokes it, and issues a new pair. Reuse of an already-rotated token revokes the user's remaining tokens; a token revoked by logout or session revocation is just invalid. Concurrent callers with the same still-valid token share one rotation so a lost race cannot family-revoke the winner's new session.
type TokenPair ¶
type TokenPair struct {
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiJ9..." doc:"Short-lived Bearer access JWT. Hold in memory only."`
RefreshToken string `json:"refresh_token" example:"aa11..." doc:"Opaque rotating refresh token. Hold in memory only; never web storage."`
TokenType string `json:"token_type" example:"Bearer" doc:"Token type; always Bearer"`
ExpiresIn int `json:"expires_in" example:"900" doc:"Access token lifetime in seconds"`
}
TokenPair is the login/refresh success payload (inside D10 data).
type User ¶
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;size:255;not null"`
PasswordHash string `gorm:"not null"`
IsSuperuser bool `gorm:"not null;default:false"`
Groups []Group `gorm:"many2many:auth_user_groups;"`
Permissions []Permission `gorm:"many2many:auth_user_permissions;"`
CreatedAt time.Time
UpdatedAt time.Time
}
User is the auth identity. IsSuperuser bypasses every permission check; regular users receive permissions directly or through groups.