auth

Autentikáció

JWT-kibocsátás és egy transport-független Identify függvény: stateless auth, framework nélkül.

Importáld az auth-ot, hogy JWT-t bocsáss ki, és bearer tokenből identityt csinálj:

import "github.com/gp-system/auth"

Az auth egy önálló, stateless JWT-modul, semmilyen más gp-system csomagtól nem függ, még az errs-től vagy a httperr-től sem: az egyetlen függősége a golang-jwt/jwt/v5. Két dolgot ad: egy token-kibocsátót (JWT aláírás és ellenőrzés) és az Identify-t, ami egy bearer tokenből rbac.Identity-t csinál. Innen minden authorizációs döntés (a szerep- és permission-ellenőrzés, a policy-k) már a tokentől függetlenül, az Identity fölött történik.

Az auth sosem ír HTTP választ, és az Identify még csak *http.Request-et sem vár: egy fejléc-értéket vár. Ez bármilyen transportról használhatóvá teszi, nem csak net/http-ről: egy WebSocket handshake-ből, egy gRPC interceptorból, ami metaadatot olvas, egy üzenetsor-fogyasztóból, ami a tokent egy üzenetfejlécben ellenőrzi. Minden hiba egy sima Go error (sentinel vagy kis típusos érték); a hívó dönti el, hogyan jelenítse meg. Teljes, futtatható példákért lásd az Önálló használat oldalt, HTTP-vel és anélkül is. A gpsystem kit erre épít rá egy net/http middleware-t és RFC 9457 Problem-renderelést a server csomagjában.

Az autentikáció alapja itt a stateless JWT: a login végpont aláírt tokent ad, minden további kérés a tokent hordozza, és a szerver semmilyen session-tárat nem tart: a token önmagában hordozza a subjectet, a szerepeket és a jogosultságokat.

A csomag a golang-jwt/jwt/v5-re épül, és nem rejti el: a claims-típusok a jwt.Claims interfészt implementálják, a saját claims struct a jwt.RegisteredClaims-t ágyazza be. A modul itt centralizál, nem absztrahál: a JWT-ökoszisztéma tudásod egy az egyben használható.

Telepítés

go get github.com/gp-system/auth@v0.1.0   # a kit is ezt a taget használja
# vagy: go get github.com/gp-system/auth@latest

Az auth egyetlen függősége a golang-jwt/jwt/v5; az auth/rbac és az auth/policy alcsomag ugyanabban a modulban szállít, külön go get nem kell hozzájuk. Semmi nem igényli itt a gpsystem kitet vagy bármelyik más gp-system modult: lásd az Önálló használat oldalt egy olyan projekthez, ami csak a github.com/gp-system/auth-ot importálja.

Konfiguráció

Az auth.Config env-ből töltődik; a generált projekt configjában a // gpsystem:config anchor alá teszed, JWT_ prefixszel:

// internal/platform/config/config.go
type Config struct {
    Server server.Config
    DB     dbx.Config  `envPrefix:"DB_"`
    JWT    auth.Config `envPrefix:"JWT_"`
    // gpsystem:config
}
Env-változóMezőAlapértelmezés
JWT_SECRETSecretkötelező
JWT_ACCESS_TOKEN_TTLAccessTTL15m
JWT_REFRESH_TOKEN_TTLRefreshTTL168h (7 nap)
JWT_ISSUERIssuerüres (az issuer-ellenőrzés kikapcsolva)
JWT_AUDIENCEAudienceüres (az audience-ellenőrzés kikapcsolva)
Állítsd be az Issuer-t és az Audience-t, és service-enként és környezetenként különSecret-et használj: így egy másik service-nek vagy a staging környezetnek kiállított token nem játszható vissza a production API ellen. Ha az Issuer/Audience be van állítva, a parse megköveteli az egyező iss/aud claimet. A teljes env-referencia: Konfiguráció.

Token-kibocsátás: a login flow

A kit standard claims-alakja a DefaultClaims: a rbac.Identity mezőivel kompatibilis, és a jwt.RegisteredClaims-t ágyazza be:

type DefaultClaims struct {
    Username    string   `json:"username,omitempty"`
    Roles       []string `json:"roles,omitempty"`
    Permissions []string `json:"permissions,omitempty"`
    jwt.RegisteredClaims
}

A NewDefaultClaims a registered claimeket (iat, exp, iss, sub, és ha konfigurált, aud) a configból bélyegzi ki; a TokenIssuer írja alá és parseolja a tokent. Így néz ki a shop login service-e vázlatosan:

// internal/modules/shop/surfaces/admin/service/auth.go (vázlat)
type Service struct {
    jwt    auth.Config
    issuer *auth.TokenIssuer[auth.DefaultClaims]
    users  UserRepo
    tokens RefreshTokenRepo
}

func New(jwtCfg auth.Config, users UserRepo, tokens RefreshTokenRepo) *Service {
    return &Service{
        jwt:    jwtCfg,
        issuer: auth.NewTokenIssuer[auth.DefaultClaims](jwtCfg),
        users:  users,
        tokens: tokens,
    }
}

func (s *Service) Login(ctx context.Context, email, password string) (LoginResult, error) {
    user, err := s.users.FindByEmail(ctx, email)
    if err != nil || !user.CheckPassword(password) {
        return LoginResult{}, ErrInvalidCredentials
    }

    claims := auth.NewDefaultClaims(s.jwt, user.ID, user.Name,
        user.Roles, user.Permissions) // iat/exp/iss/sub kitöltve a configból
    access, err := s.issuer.Sign(claims)
    if err != nil {
        return LoginResult{}, err
    }

    refresh, err := auth.NewRefreshToken() // 32 bájt crypto/rand, hexben
    if err != nil {
        return LoginResult{}, err
    }
    expiresAt := time.Now().Add(s.jwt.RefreshTTL)
    if err := s.tokens.Store(ctx, user.ID, sha256Hex(refresh), expiresAt); err != nil {
        return LoginResult{}, err
    }

    return LoginResult{AccessToken: access, RefreshToken: refresh}, nil
}
A refresh token kezelése a te sémád: a NewRefreshToken csak egy kriptográfiailag véletlen stringet ad, a kit nem tárolja. Szerveroldalon csak a hash-ét tárold (mint a jelszavaknál), és a RefreshTTL-t használd lejáratnak (a fenti sha256Hex a te helper-ed).

Identify: tokenből identity

Az Identify azt a munkát végzi, amit egy middleware általában elrejt: kiveszi a bearer tokent egy Authorization fejléc-értékből, a TokenIssuer-rel validálja (aláírás, lejárat, issuer/audience), és a claimekből *rbac.Identity-t épít. Fejléc-értéket vár, nem requestet vagy response writert, és sikertelenség esetén sima errort ad vissza:

id, err := auth.Identify(issuer, auth.DefaultIdentity, authorizationHeader)
switch {
case err == nil:
    // id kitöltve; mehet tovább
case errors.Is(err, auth.ErrMissingToken):
    // nincs bearer token
case errors.Is(err, auth.ErrInvalidToken):
    // rossz aláírás, lejárt, rossz issuer/audience
case errors.Is(err, auth.ErrInvalidClaims):
    // a token érvényes, de a toIdentity elutasította a claimeket
}

Az auth.DefaultIdentity a DefaultClaims toIdentity függvénye; egy saját claims-típushoz saját toIdentity-t írsz (lásd Saját claims alább).

A kittel: server.AuthMiddleware

A gpsystem kit becsomagolja az Identify-t egy net/http middleware-be, és a hibáit RFC 9457 problem+json válaszként rendereli. A shop admin surface-e így kerül JWT + szerepvédelem mögé: a middleware-t a generált Register<Surface> függvénybe fűzöd be, a többi surface érintése nélkül (lásd add surface):

internal/modules/shop/register.go
func RegisterAdmin(router chi.Router, deps Dependencies) {
    adminSvc := adminservice.New()
    adminHandler := adminhttp.New(adminSvc)
    adminPolicies := adminpolicy.New()
    router.Route("/admin/shop", func(r chi.Router) {
        r.Use(server.AuthMiddleware(deps.Auth), server.RequireRole("admin"))
        shopadmingen.HandlerWithOptions(/* ... a generált strict-handler bekötés változatlan ... */)
    })
}

Az auth.Config-ot a modul Dependencies structján keresztül adod be (a // gpsystem:dependencies anchornál egy Auth auth.Config mező, a main.go-ban Auth: cfg.JWT). Így a register.go nem függ a platform-configtól. A server.AuthMiddleware/server.RequireRole/server.RequirePermission a kitben él, nem az auth-ban: a teljes server.WriteError leképzésért lásd a kit servere oldalt.

Vegyes surface-ök: publikus és védett műveletek egymás mellett

A server.AuthMiddleware minden token nélküli kérést 401-el; egy csupa-védett surface-re (mint az admin) pont ez kell. A shop api surface-e viszont vegyes: a terméklistázás anonim, a rendelés-műveletek viszont identityt igényelnek. Ilyenkor a token-parseolást tedd feltételessé, a taggelt műveletek 401-ét pedig bízd a policy-enforcerre, ami identity nélkül úgyis elutasít:

authOptional := server.AuthMiddleware(deps.Auth)
r.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            next.ServeHTTP(w, r) // anonim böngészés mehet; a taggelt műveleteket az enforcer 401-eli
            return
        }
        authOptional(next).ServeHTTP(w, r) // ha van token, legyen érvényes
    })
})

Saját claims

Ha a DefaultClaims kevés (például tenant-azonosítót hordozol a tokenben), definiáld a saját claims-típusodat, és írj saját toIdentity függvényt az Identify-hoz (vagy a server.AuthMiddlewareFor-hoz, ami ennek a kit-drótozott net/http párja):

type MyClaims struct {
    TenantID string `json:"tenant"`
    jwt.RegisteredClaims
}

toIdentity := func(c MyClaims) (*rbac.Identity, error) {
    return &rbac.Identity{
        Subject: c.Subject,
        Extra:   map[string]any{"tenant": c.TenantID},
    }, nil
}

issuer := auth.NewTokenIssuer[MyClaims](cfg.JWT)
mw := server.AuthMiddlewareFor(issuer, toIdentity) // kit net/http middleware
id, err := auth.Identify(issuer, toIdentity, authorizationHeader) // vagy közvetlenül hívva

A server.AuthMiddleware pontosan a server.AuthMiddlewareFor, DefaultClaims-szel és auth.DefaultIdentity-vel előre bekötve, így mindkettő ugyanazt a func(http.Handler) http.Handler alakot és Identity-építési kontraktust osztja; az AuthMiddlewareFor-hoz csak akkor nyúlj, ha a DefaultClaims nem elég.

HS256 és a titok kezelése

Az aláírás HS256 (megosztott titok). A parse rögzíti az algoritmust (WithValidMethods), megköveteli a lejáratot (WithExpirationRequired), és ha konfigurált, ellenőrzi az issuert és az audience-t, így az alg-confusion támadások és az aláíratlan tokenek elutasítódnak. A TokenIssuer API claims-generikus, ezért aszimmetrikus algoritmusok később törés nélkül jöhetnek.

A titok legyen hosszú és véletlen (pl. openssl rand -hex 32), és secret-kezelőből érkezzen: sose kerüljön a repóba. A .env csak lokális fejlesztésre való; lásd a konfiguráció oldalt.

Önállóan, kit nélkül

A fentiekhez semmi nem kell a server.Run-ból vagy bármelyik más kit-csomagból. Az Identify és az rbac/policy sentinel errorok minden, ami egy csupasz net/http service-hez (vagy egy nem-HTTP-shez) kell, saját hibarendereléssel: teljes, futtatható példákért lásd az Önálló használat oldalt.

A következő réteg: a tokenből lett Identity fölötti szerep- és permission-ellenőrzések, és a kérésenkénti policy-k. 3rd-party (Discord/Facebook/Apple/Google) bejelentkezéshez az add auth generált moduljának saját social login rétege tartozik, ugyanezt a token-kibocsátást hívva.

Használt patternek

Generikusok a claims-típus fölött (TokenIssuer[C jwt.Claims], Identify[C]): Design patternek.

Copyright © 2026