Files
netbird/management/server/auth/session.go
T

76 lines
1.7 KiB
Go

package auth
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
)
const (
usedTokenKeyPrefix = "jwt-used:"
usedTokenMarker = "1"
)
var (
ErrTokenAlreadyUsed = errors.New("JWT already used")
ErrTokenExpired = errors.New("JWT expired")
)
// TokenCache atomically records used JWTs until their expiration.
type TokenCache interface {
SetNX(ctx context.Context, key, value string, ttl time.Duration) (bool, error)
}
type SessionStore struct {
cache TokenCache
}
func NewSessionStore(cacheStore TokenCache) *SessionStore {
return &SessionStore{cache: cacheStore}
}
// RegisterToken records a JWT until its exp time and rejects reuse.
func (s *SessionStore) RegisterToken(ctx context.Context, token string, expiresAt time.Time) error {
ttl := time.Until(expiresAt)
if ttl <= 0 {
return ErrTokenExpired
}
key := usedTokenKeyPrefix + hashToken(token)
created, err := s.cache.SetNX(ctx, key, usedTokenMarker, ttl)
if err != nil {
return fmt.Errorf("store used token entry: %w", err)
}
if !created {
return ErrTokenAlreadyUsed
}
return nil
}
func hashToken(token string) string {
sum := sha256.Sum256([]byte(canonicalizeToken(token)))
return hex.EncodeToString(sum[:])
}
// canonicalizeToken re-encodes the JWT signature segment so noncanonical
// spellings of the same signature map to one stable cache key.
func canonicalizeToken(token string) string {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return token
}
sig, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return token
}
return parts[0] + "." + parts[1] + "." + base64.RawURLEncoding.EncodeToString(sig)
}