auth

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 23 Imported by: 0

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

View Source
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

View Source
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.

View Source
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

func AddUserToGroup(ctx context.Context, db *gorm.DB, user *User, group *Group) error

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

func HasPermission(ctx context.Context, db *gorm.DB, user User, key string) (bool, error)

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 Migrate

func Migrate(db *gorm.DB) error

Migrate creates the auth identity, token, group, and permission tables.

func Models

func Models() []any

Models returns GORM models for Atlas / AutoMigrate.

func Mount

func Mount(api huma.API, db *gorm.DB, cfg config.Config) error

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 Clock

type Clock interface {
	Now() time.Time
}

Clock is the time source used for token expiry. Tests inject a fake.

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.

func EnsureGroup

func EnsureGroup(ctx context.Context, db *gorm.DB, name string) (Group, error)

EnsureGroup returns the group with name, creating it when needed.

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

func NewService(db *gorm.DB, cfg config.Config) (*Service, error)

NewService constructs a Service. cfg.Auth.JWTSecret must be set.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, email, password string) (User, error)

Authenticate verifies email and password.

func (*Service) CreateSuperuser

func (s *Service) CreateSuperuser(ctx context.Context, email, password string) (User, error)

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

func (s *Service) IssueTokens(ctx context.Context, user User) (TokenPair, error)

IssueTokens returns a new access JWT and a rotating refresh token.

func (*Service) ListSessions added in v0.8.0

func (s *Service) ListSessions(ctx context.Context, userID uint) ([]AuthSession, error)

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

func (s *Service) ParseAccess(ctx context.Context, token string) (User, error)

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) Register

func (s *Service) Register(ctx context.Context, email, password string) (User, error)

Register creates a user. Duplicate emails return errEmailTaken.

func (*Service) RequireCookieSession

func (s *Service) RequireCookieSession() func(ctx huma.Context, next func(huma.Context))

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

func (s *Service) RevokeAllSessions(ctx context.Context, userID uint) error

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

func (s *Service) RevokeRefresh(ctx context.Context, raw string) error

RevokeRefresh invalidates one refresh token. Missing/already-revoked tokens succeed so logout is idempotent.

func (*Service) RevokeSession added in v0.8.0

func (s *Service) RevokeSession(ctx context.Context, userID uint, id string) error

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

func (s *Service) RotateRefresh(ctx context.Context, raw string) (TokenPair, error)

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.

func UserFromContext

func UserFromContext(ctx context.Context) (User, bool)

UserFromContext returns the authenticated user stored by requireBearer or RequireCookieSession.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL