authkit initial

This commit is contained in:
juancwu 2026-04-26 01:36:53 +00:00
commit 134393fbca
43 changed files with 5188 additions and 1 deletions

121
authkit.go Normal file
View file

@ -0,0 +1,121 @@
package authkit
import (
"context"
"crypto/rand"
"io"
"net/http"
"time"
"git.juancwu.dev/juancwu/errx"
)
// Deps bundles every backing store and the password hasher the Auth service
// depends on. All fields are required; New panics on a nil dep so misuse is
// caught at boot rather than under load.
type Deps struct {
Users UserStore
Sessions SessionStore
Tokens TokenStore
APIKeys APIKeyStore
Roles RoleStore
Permissions PermissionStore
Hasher Hasher
}
// Config tunes session/JWT/token TTLs, cookie shape, JWT signing material,
// and optional hooks. Any zero-valued duration is replaced with a sane
// default in New; required fields (notably JWTSecret) cause New to panic.
type Config struct {
// Session (opaque) cookies + DB-backed lifetime
SessionIdleTTL time.Duration
SessionAbsoluteTTL time.Duration
SessionCookieName string
SessionCookieDomain string
SessionCookiePath string
SessionCookieSecure bool
SessionCookieHTTPOnly bool
SessionCookieSameSite http.SameSite
// JWT (HS256)
JWTSecret []byte
JWTIssuer string
JWTAudience string
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
// Single-use tokens
EmailVerifyTTL time.Duration
PasswordResetTTL time.Duration
MagicLinkTTL time.Duration
// Hooks (optional)
Clock func() time.Time
Random io.Reader
LoginHook func(ctx context.Context, email string, success bool) error
}
// Auth is the high-level service that composes the stores and hasher into the
// flows callers use: registration, login, sessions, JWTs, magic links, API
// keys, and authz checks. It is safe for concurrent use; method receivers
// never mutate Auth state after construction.
type Auth struct {
deps Deps
cfg Config
}
// New validates Deps and Config, fills in defaults, and returns a ready
// service. It panics on missing deps or missing JWT secret rather than
// returning an error — these are programmer errors, not runtime ones.
func New(deps Deps, cfg Config) *Auth {
if deps.Users == nil || deps.Sessions == nil || deps.Tokens == nil ||
deps.APIKeys == nil || deps.Roles == nil || deps.Permissions == nil ||
deps.Hasher == nil {
panic(errx.New("authkit.New", "all Deps fields are required"))
}
if len(cfg.JWTSecret) == 0 {
panic(errx.New("authkit.New", "Config.JWTSecret is required"))
}
if cfg.SessionIdleTTL == 0 {
cfg.SessionIdleTTL = 24 * time.Hour
}
if cfg.SessionAbsoluteTTL == 0 {
cfg.SessionAbsoluteTTL = 30 * 24 * time.Hour
}
if cfg.SessionCookieName == "" {
cfg.SessionCookieName = "authkit_session"
}
if cfg.SessionCookiePath == "" {
cfg.SessionCookiePath = "/"
}
if cfg.SessionCookieSameSite == 0 {
cfg.SessionCookieSameSite = http.SameSiteLaxMode
}
if cfg.AccessTokenTTL == 0 {
cfg.AccessTokenTTL = 15 * time.Minute
}
if cfg.RefreshTokenTTL == 0 {
cfg.RefreshTokenTTL = 30 * 24 * time.Hour
}
if cfg.EmailVerifyTTL == 0 {
cfg.EmailVerifyTTL = 48 * time.Hour
}
if cfg.PasswordResetTTL == 0 {
cfg.PasswordResetTTL = time.Hour
}
if cfg.MagicLinkTTL == 0 {
cfg.MagicLinkTTL = 15 * time.Minute
}
if cfg.Clock == nil {
cfg.Clock = func() time.Time { return time.Now().UTC() }
}
if cfg.Random == nil {
cfg.Random = rand.Reader
}
return &Auth{deps: deps, cfg: cfg}
}
// now returns the configured wall clock, defaulting to time.Now in UTC.
func (a *Auth) now() time.Time { return a.cfg.Clock() }