Files
nexarch/internal/auth/token.go
T
patrickandClaude Sonnet 5 cc0dc21fe7
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 23s
IAM-17: session-token-widerruf-bei-logout-und-kompromittierung
Fix: Massenwiderrufs-Stichtagsvergleich nutzt jetzt IssuedAtNano statt
claims.IssuedAt, da RegisteredClaims.IssuedAt beim JWT-Roundtrip auf volle
Sekunden rundet und sonst ein Login in derselben Sekunde wie ein
RevokeAllForUser-Aufruf faelschlich als widerrufen gelten koennte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
2026-09-17 10:38:29 +02:00

91 lines
2.8 KiB
Go

package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
// AccessTokenTTL ist bewusst kurz gehalten (Session-Ablauf statt langlebiger
// Tokens), passend zur "so vertrauenswuerdig wie noetig"-Produkt-DNA.
const AccessTokenTTL = 30 * time.Minute
var ErrInvalidToken = errors.New("auth: ungueltiges oder abgelaufenes token")
type Claims struct {
UserID string `json:"uid"`
TenantSlug string `json:"tenant"`
// IssuedAtNano haelt den Ausstellungszeitpunkt in Nanosekunden-Praezision
// (IAM-17) — RegisteredClaims.IssuedAt rundet beim JSON-Roundtrip auf
// volle Sekunden (jwt.TimePrecision), was fuer den Massenwiderrufs-
// Stichtagsvergleich zu falschen Treffern fuehren wuerde, wenn ein neuer
// Login in derselben Sekunde wie ein RevokeAllForUser-Aufruf stattfindet.
IssuedAtNano int64 `json:"iat_ns"`
jwt.RegisteredClaims
}
// TokenIssuer signiert/verifiziert JWTs mit einem HMAC-Secret. Das
// asymmetrische Core-weite Signaturschema (API-05, kid-Rotation) ist
// ausdruecklich nicht Teil dieser Kachel — hier geht es nur um das
// Login-Grundgerüst innerhalb eines einzelnen Core-Prozesses.
type TokenIssuer struct {
secret []byte
}
func NewTokenIssuer(secret string) *TokenIssuer {
return &TokenIssuer{secret: []byte(secret)}
}
func (i *TokenIssuer) Issue(userID, tenantSlug string) (string, error) {
now := time.Now()
jti, err := newJTI()
if err != nil {
return "", fmt.Errorf("token-id erzeugen: %w", err)
}
claims := Claims{
UserID: userID,
TenantSlug: tenantSlug,
IssuedAtNano: now.UnixNano(),
RegisteredClaims: jwt.RegisteredClaims{
ID: jti,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(AccessTokenTTL)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(i.secret)
}
// newJTI erzeugt eine zufaellige, eindeutige Token-ID (RegisteredClaims.ID).
// IAM-17 braucht sie, um ein EINZELNES Token gezielt widerrufen zu koennen,
// ohne andere Sitzungen desselben Benutzers zu beeinflussen (siehe
// internal/auth/revocation.go).
func newJTI() (string, error) {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
// Verify prueft Signatur UND Ablauf (jwt.ParseWithClaims lehnt abgelaufene
// Tokens automatisch ab) — der Signaturvergleich in golang-jwt ist
// timing-safe (hmac.Equal).
func (i *TokenIssuer) Verify(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrInvalidToken
}
return i.secret, nil
})
if err != nil || !token.Valid {
return nil, ErrInvalidToken
}
return claims, nil
}