Supply-Chain-Scan (Go) / govulncheck (push) Failing after 23s
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
91 lines
2.8 KiB
Go
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
|
|
}
|