mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:45:14 -04:00
Move the daemon's memguard-backed JWT token cache from client/server into client/ssh/jwtcache so it can be reused outside the daemon process. The TTL derivation from Config.SSHJWTCacheTTL (nil=default, 0=disabled) moves into jwtcache.ResolveTTL. Wire the cache into the iOS SDK's SSH client: iOS has no daemon to delegate caching to, so without it every reconnect forced the user through the browser OAuth device-code flow. The cache lives on the long-lived Client (the app creates a new SSHClient per session) and uses the same config-driven TTL semantics as the daemon.
110 lines
2.5 KiB
Go
110 lines
2.5 KiB
Go
// Package jwtcache provides an in-memory, TTL-bound cache for SSH JWT tokens.
|
|
// The token is kept in a secure memguard enclave and wiped from memory when it
|
|
// expires. It is shared by the daemon gRPC server and the mobile SDKs, which
|
|
// have no daemon process to delegate caching to.
|
|
package jwtcache
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/awnumar/memguard"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// DefaultTTL is used when no TTL is configured: caching disabled.
|
|
const DefaultTTL = 0
|
|
|
|
// Cache stores a single JWT token in a secure enclave until it expires.
|
|
type Cache struct {
|
|
mu sync.RWMutex
|
|
enclave *memguard.Enclave
|
|
expiresAt time.Time
|
|
timer *time.Timer
|
|
maxTokenSize int
|
|
}
|
|
|
|
// New creates an empty Cache.
|
|
func New() *Cache {
|
|
return &Cache{
|
|
maxTokenSize: 8192,
|
|
}
|
|
}
|
|
|
|
// Store caches the token for maxAge. A previously stored token is wiped.
|
|
func (c *Cache) Store(token string, maxAge time.Duration) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.cleanup()
|
|
|
|
if c.timer != nil {
|
|
c.timer.Stop()
|
|
}
|
|
|
|
tokenBytes := []byte(token)
|
|
c.enclave = memguard.NewEnclave(tokenBytes)
|
|
|
|
c.expiresAt = time.Now().Add(maxAge)
|
|
|
|
var timer *time.Timer
|
|
timer = time.AfterFunc(maxAge, func() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.timer != timer {
|
|
return
|
|
}
|
|
c.cleanup()
|
|
c.timer = nil
|
|
log.Debugf("JWT token cache expired after %v, securely wiped from memory", maxAge)
|
|
})
|
|
c.timer = timer
|
|
}
|
|
|
|
// Get returns the cached token, or false if none is stored or it has expired.
|
|
func (c *Cache) Get() (string, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if c.enclave == nil || time.Now().After(c.expiresAt) {
|
|
return "", false
|
|
}
|
|
|
|
buffer, err := c.enclave.Open()
|
|
if err != nil {
|
|
log.Debugf("Failed to open JWT token enclave: %v", err)
|
|
return "", false
|
|
}
|
|
defer buffer.Destroy()
|
|
|
|
token := string(buffer.Bytes())
|
|
return token, true
|
|
}
|
|
|
|
// cleanup destroys the secure enclave, must be called with lock held
|
|
func (c *Cache) cleanup() {
|
|
if c.enclave != nil {
|
|
c.enclave = nil
|
|
}
|
|
c.expiresAt = time.Time{}
|
|
}
|
|
|
|
// ResolveTTL converts the configured TTL (seconds, from
|
|
// profilemanager.Config.SSHJWTCacheTTL) into a duration. Returns DefaultTTL
|
|
// when unset; 0 means caching is disabled.
|
|
func ResolveTTL(configuredSeconds *int) time.Duration {
|
|
if configuredSeconds == nil {
|
|
return DefaultTTL
|
|
}
|
|
|
|
seconds := *configuredSeconds
|
|
if seconds == 0 {
|
|
log.Debug("SSH JWT cache disabled (configured to 0)")
|
|
return 0
|
|
}
|
|
|
|
ttl := time.Duration(seconds) * time.Second
|
|
log.Debugf("SSH JWT cache TTL set to %v from config", ttl)
|
|
return ttl
|
|
}
|