diff --git a/config-sample.yaml b/config-sample.yaml index 07574c922..870a4fa24 100644 --- a/config-sample.yaml +++ b/config-sample.yaml @@ -100,7 +100,11 @@ rtc: # secret: "" # # Path for file containing shared secret for TURN server authentication # # When both secret and secret_file are set, secret takes precedence + # # An empty/whitespace-only secret file fails startup rather than silently + # # falling back to static credentials # secret_file: "/var/lib/coturn/secret" + # # TTL of generated credentials in seconds. Defaults to 14400 (4h) when 0. + # # Negative values fall back to the 300s (5m) default; values are capped at 86400 (24h). # ttl: 14400 # seconds # # Insecure username/password authentication # username: "" @@ -304,7 +308,8 @@ keys: # # optional (set only if not using external TLS termination) # # cert_file: /path/to/cert.pem # # key_file: /path/to/key.pem -# # TTL of the TURN credentials in seconds - defaults to 300 +# # TTL of the TURN credentials in seconds - defaults to 300. +# # Values <= 0 fall back to the 300s (5m) default; values are capped at 86400 (24h). # ttl_seconds: 300 # # list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to. # # By default (i. e. empty list), all restricted peer CIDRs are denied access. @@ -316,6 +321,7 @@ keys: # # list of peer CIDRs to deny access to. # # This applies to all peer CIDRs, including restricted ones. # # Deny list takes precedence over allow list. +# # A malformed CIDR in either list fails startup rather than being silently ignored. # deny_peer_cidrs: # - 10.0.0.0/8 # - 192.168.0.0/16 diff --git a/pkg/config/config.go b/pkg/config/config.go index d79c5b7ca..0eb3ec747 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -46,12 +46,27 @@ import ( const ( generatedCLIFlagUsage = "generated" + + // TURNMaxTTLSeconds is the operational maximum for TURN credential TTLs (24 hours). + // It bounds credential lifetime and prevents time.Duration overflow when multiplying + // by time.Second (values above ~9.2e9 seconds wrap on 64-bit builds). + TURNMaxTTLSeconds = 24 * 60 * 60 + + // DefaultTURNTTLSeconds is the default TTL for embedded TURN credentials, and the + // fallback used when a configured TTL is <= 0. + DefaultTURNTTLSeconds = 300 + + // DefaultExternalTURNTTLSeconds is the default TTL applied to external TURN + // (static-auth-secret) credentials when the configured TTL is left at 0. + DefaultExternalTURNTTLSeconds = 14400 ) var ( ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0") ErrTURNSecretFileIncorrectPermission = errors.New("turn secret file others permissions must be set to 0") ErrKeysNotSet = errors.New("one of key-file or keys must be provided") + ErrTURNSecretEmpty = errors.New("turn secret is empty") + ErrTURNServerNoCredentials = errors.New("turn server has no usable credentials: set a non-empty secret/secret_file for dynamic auth, or username and credential for static auth") ) type Config struct { @@ -174,7 +189,8 @@ type TURNServer struct { // File containing the secret SecretFile string `yaml:"secret_file,omitempty"` // TTL is the time-to-live in seconds for generated credentials when using Secret. - // Defaults to 14400 seconds (4 hours) if not specified + // Defaults to 14400 seconds (4 hours) when 0. Negative values fall back to the 5m + // default and large values are capped at TURNMaxTTLSeconds. TTL int `yaml:"ttl,omitempty"` } @@ -252,7 +268,8 @@ type TURNConfig struct { RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"` ExternalTLS bool `yaml:"external_tls,omitempty"` BindAddresses []string `yaml:"bind_addresses,omitempty"` - // TTL of the TURN credentials in seconds - defaults to 300 + // TTL of the TURN credentials in seconds - defaults to 300. Values <= 0 fall back to the + // 300s (5m) default and large values are capped at TURNMaxTTLSeconds. TTLSeconds int `yaml:"ttl_seconds,omitempty"` // list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to. // By default (i. e. empty list), all restricted peer CIDRs are denied access. @@ -521,7 +538,7 @@ var DefaultConfig = Config{ TURN: TURNConfig{ Enabled: false, BindAddresses: []string{"0.0.0.0"}, - TTLSeconds: 300, + TTLSeconds: DefaultTURNTTLSeconds, }, NodeSelector: NodeSelectorConfig{ Kind: "any", @@ -580,6 +597,8 @@ func NewConfig(confString string, strictMode bool, c *cli.Command, baseFlags []c return nil, fmt.Errorf("could not validate RTC config: %v", err) } + conf.NormalizeTURNTTLs() + // expand env vars in filenames file, err := homedir.Expand(os.ExpandEnv(conf.KeyFile)) if err != nil { @@ -734,30 +753,89 @@ func (conf *Config) ValidateKeys() error { func (conf *Config) LoadTURNSecrets() error { var otherFilter os.FileMode = 0o007 for i, s := range conf.RTC.TURNServers { - if s.SecretFile == "" { - continue - } - if s.Secret != "" { + // trim first so a blank inline secret falls back to secret_file rather than being treated as set + inlineSecret := strings.TrimSpace(s.Secret) + switch { + case s.SecretFile != "" && inlineSecret != "": logger.Warnw("both secret and secret_file are set for TURN server, the hardcoded secret will be used", nil, "host", s.Host, "port", s.Port) - continue + conf.RTC.TURNServers[i].Secret = inlineSecret + case s.SecretFile != "": + st, err := os.Stat(s.SecretFile) + if err != nil { + return err + } + if st.Mode().Perm()&otherFilter != 0o000 { + return ErrTURNSecretFileIncorrectPermission + } + data, err := os.ReadFile(s.SecretFile) + if err != nil { + return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err) + } + secret := strings.TrimSpace(string(data)) + if secret == "" { + return fmt.Errorf("turn server %q secret file %q: %w", s.Host, s.SecretFile, ErrTURNSecretEmpty) + } + conf.RTC.TURNServers[i].Secret = secret + default: + conf.RTC.TURNServers[i].Secret = inlineSecret } - st, err := os.Stat(s.SecretFile) - if err != nil { - return err + + // validate credential mode as an explicit union rather than inferring it from a + // post-load empty secret (which would silently fall back to static credentials) + s := conf.RTC.TURNServers[i] + hasDynamic := s.Secret != "" + hasStatic := s.Username != "" && s.Credential != "" + if !hasDynamic && !hasStatic { + return fmt.Errorf("turn server %q: %w", s.Host, ErrTURNServerNoCredentials) } - if st.Mode().Perm()&otherFilter != 0o000 { - return ErrTURNSecretFileIncorrectPermission - } - data, err := os.ReadFile(s.SecretFile) - if err != nil { - return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err) - } - conf.RTC.TURNServers[i].Secret = strings.TrimSpace(string(data)) } return nil } +// ClampTURNTTLSeconds bounds a TURN credential TTL to [DefaultTURNTTLSeconds, TURNMaxTTLSeconds]: +// non-positive values (which would otherwise produce already-expired credentials) fall back to +// the 5m default and overflowing values clamp to the max. The second return value reports whether +// the input was out of range. +func ClampTURNTTLSeconds(ttlSeconds int) (int, bool) { + switch { + case ttlSeconds <= 0: + return DefaultTURNTTLSeconds, true + case ttlSeconds > TURNMaxTTLSeconds: + return TURNMaxTTLSeconds, true + default: + return ttlSeconds, false + } +} + +// NormalizeTURNTTLs clamps configured TURN TTLs to the safe range, warning on any +// adjustment. Safe to call more than once. +func (conf *Config) NormalizeTURNTTLs() { + if clamped, changed := ClampTURNTTLSeconds(conf.TURN.TTLSeconds); changed { + logger.Warnw( + "turn.ttl_seconds out of range, using safe value", nil, + "configured", conf.TURN.TTLSeconds, + "clamped", clamped, + "max", TURNMaxTTLSeconds, + ) + conf.TURN.TTLSeconds = clamped + } + for i := range conf.RTC.TURNServers { + // external TTL of 0 means "use the default", so only cap the upper bound here; + // non-positive values fall back to the default when credentials are generated + if ttl := conf.RTC.TURNServers[i].TTL; ttl > TURNMaxTTLSeconds { + logger.Warnw( + "turn_servers ttl out of range, using safe value", nil, + "host", conf.RTC.TURNServers[i].Host, + "configured", ttl, + "clamped", TURNMaxTTLSeconds, + "max", TURNMaxTTLSeconds, + ) + conf.RTC.TURNServers[i].TTL = TURNMaxTTLSeconds + } + } +} + func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) { defaultConfig := &DefaultConfig flags := make([]cli.Flag, 0) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index da2b2e692..f5381bbc6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -15,6 +15,8 @@ package config import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -83,3 +85,125 @@ func TestGeneratedFlags(t *testing.T) { func TestYAMLTag(t *testing.T) { require.NoError(t, configtest.CheckYAMLTags(Config{})) } + +func TestClampTURNTTLSeconds(t *testing.T) { + cases := []struct { + name string + in int + want int + changed bool + }{ + {"negative falls back to default", -1, DefaultTURNTTLSeconds, true}, + {"large negative falls back to default", -1 << 40, DefaultTURNTTLSeconds, true}, + {"zero falls back to default", 0, DefaultTURNTTLSeconds, true}, + {"in range preserved", 600, 600, false}, + {"max preserved", TURNMaxTTLSeconds, TURNMaxTTLSeconds, false}, + {"over max clamps to max", TURNMaxTTLSeconds + 1, TURNMaxTTLSeconds, true}, + {"overflowing value clamps to max", 1<<62 + 1, TURNMaxTTLSeconds, true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, changed := ClampTURNTTLSeconds(c.in) + require.Equal(t, c.want, got) + require.Equal(t, c.changed, changed) + }) + } +} + +func TestNormalizeTURNTTLs(t *testing.T) { + conf, err := NewConfig("", true, nil, nil) + require.NoError(t, err) + + conf.TURN.TTLSeconds = -5 + conf.RTC.TURNServers = []TURNServer{ + {Host: "over", TTL: TURNMaxTTLSeconds + 100}, + {Host: "negative", TTL: -1}, + {Host: "default", TTL: 0}, + {Host: "ok", TTL: 600}, + } + + conf.NormalizeTURNTTLs() + + // embedded TTL <= 0 falls back to the 5m default + require.Equal(t, DefaultTURNTTLSeconds, conf.TURN.TTLSeconds) + // external TTLs: only the upper bound is capped at load; 0/negative keep their + // "use the default" meaning and are resolved when credentials are generated + require.Equal(t, TURNMaxTTLSeconds, conf.RTC.TURNServers[0].TTL) + require.Equal(t, -1, conf.RTC.TURNServers[1].TTL) + require.Equal(t, 0, conf.RTC.TURNServers[2].TTL) + require.Equal(t, 600, conf.RTC.TURNServers[3].TTL) +} + +func TestNewConfigNormalizesTURNTTL(t *testing.T) { + const content = `turn: + ttl_seconds: -10` + conf, err := NewConfig(content, true, nil, nil) + require.NoError(t, err) + require.Equal(t, DefaultTURNTTLSeconds, conf.TURN.TTLSeconds) +} + +func writeSecretFile(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "secret") + require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) + return path +} + +func TestLoadTURNSecrets(t *testing.T) { + t.Run("empty secret file is rejected", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, "")}} + require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNSecretEmpty) + }) + + t.Run("whitespace-only secret file is rejected", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, " \n\t ")}} + require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNSecretEmpty) + }) + + t.Run("valid secret file is loaded and trimmed", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", SecretFile: writeSecretFile(t, " topsecret\n")}} + require.NoError(t, conf.LoadTURNSecrets()) + require.Equal(t, "topsecret", conf.RTC.TURNServers[0].Secret) + }) + + t.Run("inline whitespace secret without static creds is rejected", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " "}} + require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials) + }) + + t.Run("no credentials at all is rejected", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h"}} + require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials) + }) + + t.Run("static credentials are accepted", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", Username: "u", Credential: "c"}} + require.NoError(t, conf.LoadTURNSecrets()) + }) + + t.Run("partial static credentials are rejected", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", Username: "u"}} + require.ErrorIs(t, conf.LoadTURNSecrets(), ErrTURNServerNoCredentials) + }) + + t.Run("inline secret takes precedence and is trimmed", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " inline ", SecretFile: writeSecretFile(t, "fromfile")}} + require.NoError(t, conf.LoadTURNSecrets()) + require.Equal(t, "inline", conf.RTC.TURNServers[0].Secret) + }) + + t.Run("blank inline secret falls back to secret file", func(t *testing.T) { + conf := &Config{} + conf.RTC.TURNServers = []TURNServer{{Host: "h", Secret: " ", SecretFile: writeSecretFile(t, "fromfile")}} + require.NoError(t, conf.LoadTURNSecrets()) + require.Equal(t, "fromfile", conf.RTC.TURNServers[0].Secret) + }) +} diff --git a/pkg/service/roommanager.go b/pkg/service/roommanager.go index 36656e126..0447f14e9 100644 --- a/pkg/service/roommanager.go +++ b/pkg/service/roommanager.go @@ -1096,11 +1096,14 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types. var username, credential string if s.Secret != "" { - // Generate dynamic credentials using TURN static auth secrets + // Generate dynamic credentials using TURN static auth secrets. + // 0 means use the default; clamp the rest so a negative or overflowing TTL + // cannot produce a wrong-lifetime credential. ttl := s.TTL if ttl == 0 { - ttl = 14400 // Default 4 hours + ttl = config.DefaultExternalTURNTTLSeconds } + ttl, _ = config.ClampTURNTTLSeconds(ttl) expiry := time.Now().Add(time.Duration(ttl) * time.Second).Unix() participantID := string(participant.ID()) diff --git a/pkg/service/turn.go b/pkg/service/turn.go index 47b82d03e..c476aeac1 100644 --- a/pkg/service/turn.go +++ b/pkg/service/turn.go @@ -46,6 +46,20 @@ const ( var ErrExpired = errors.New("expired") +// parsePeerCIDRs compiles a list of CIDR strings, failing with a field-specific +// error on any invalid entry so a malformed peer policy is never silently ignored. +func parsePeerCIDRs(field string, cidrs []string) ([]*net.IPNet, error) { + parsed := make([]*net.IPNet, 0, len(cidrs)) + for _, cidr := range cidrs { + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("invalid CIDR %q in %s: %w", cidr, field, err) + } + parsed = append(parsed, ipnet) + } + return parsed, nil +} + func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) { turnConf := conf.TURN if !turnConf.Enabled { @@ -64,6 +78,17 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone } } + // parse peer CIDR policies once at startup so a malformed entry fails loudly + // instead of being silently skipped on every permission decision (fail-open) + allowRestrictedPeerCIDRs, err := parsePeerCIDRs("turn.allow_restricted_peer_cidrs", turnConf.AllowRestrictedPeerCIDRs) + if err != nil { + return nil, err + } + denyPeerCIDRs, err := parsePeerCIDRs("turn.deny_peer_cidrs", turnConf.DenyPeerCIDRs) + if err != nil { + return nil, err + } + serverConfig := turn.ServerConfig{ Realm: LivekitRealm, AuthHandler: authHandler, @@ -105,12 +130,10 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone peerIP.IsPrivate() || peerIP.IsUnspecified() { allowed := false - for _, cidr := range turnConf.AllowRestrictedPeerCIDRs { - if _, ipnet, err := net.ParseCIDR(cidr); err == nil { - if ipnet.Contains(peerIP) { - allowed = true - break - } + for _, ipnet := range allowRestrictedPeerCIDRs { + if ipnet.Contains(peerIP) { + allowed = true + break } } if !allowed { @@ -120,11 +143,9 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone // if allowed, check deny list for overrides } - for _, cidr := range turnConf.DenyPeerCIDRs { - if _, ipnet, err := net.ParseCIDR(cidr); err == nil { - if ipnet.Contains(peerIP) { - return false - } + for _, ipnet := range denyPeerCIDRs { + if ipnet.Contains(peerIP) { + return false } } @@ -206,6 +227,8 @@ func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler { } func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) { + // clamp defensively: non-positive TTLs fall back to the default and overflowing ones are capped + ttlSeconds, _ = config.ClampTURNTTLSeconds(ttlSeconds) expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix() return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)), expiry } diff --git a/pkg/service/turn_test.go b/pkg/service/turn_test.go index 32abd28d8..ab6b2d9ad 100644 --- a/pkg/service/turn_test.go +++ b/pkg/service/turn_test.go @@ -18,6 +18,7 @@ import ( "fmt" "net" "testing" + "time" "github.com/jxskiss/base62" "github.com/pion/stun/v3" @@ -26,6 +27,8 @@ import ( "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" + + "github.com/livekit/livekit-server/pkg/config" ) const ( @@ -75,7 +78,8 @@ func TestTURNAuthHandler_HandleAuth_ExpiredAllocateRejected(t *testing.T) { h := newTestTurnAuthHandler() pID := livekit.ParticipantID("PA_expired_alloc") - username, _ := h.CreateUsername(turnTestAPIKey, pID, -60) + expiry := time.Now().Add(-time.Minute).Unix() + username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry)) _, _, ok := h.HandleAuth(&turn.RequestAttributes{ Username: username, Realm: LivekitRealm, @@ -89,7 +93,8 @@ func TestTURNAuthHandler_HandleAuth_ExpiredNonAllocateAllowed(t *testing.T) { h := newTestTurnAuthHandler() pID := livekit.ParticipantID("PA_expired_refresh") - username, expiry := h.CreateUsername(turnTestAPIKey, pID, -60) + expiry := time.Now().Add(-time.Minute).Unix() + username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry)) // CreatePassword still enforces ErrExpired on its own, but the server hands // the same key it generated at allocation time — reproduce that by directly @@ -206,3 +211,70 @@ func TestTURNAuthHandler_CreatePassword_ZeroExpiryRejected(t *testing.T) { _, err := h.CreatePassword(turnTestAPIKey, pID, 0) require.ErrorIs(t, err, ErrExpired) } + +func TestParsePeerCIDRs(t *testing.T) { + t.Run("valid entries are compiled", func(t *testing.T) { + nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/24", "10.0.0.0/8"}) + require.NoError(t, err) + require.Len(t, nets, 2) + require.True(t, nets[0].Contains(net.ParseIP("203.0.113.5"))) + require.False(t, nets[0].Contains(net.ParseIP("203.0.114.5"))) + }) + + t.Run("empty list is fine", func(t *testing.T) { + nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", nil) + require.NoError(t, err) + require.Empty(t, nets) + }) + + t.Run("invalid entry is rejected with field context", func(t *testing.T) { + _, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/33"}) + require.Error(t, err) + require.Contains(t, err.Error(), "turn.deny_peer_cidrs") + require.Contains(t, err.Error(), "203.0.113.0/33") + }) +} + +func TestNewTurnServer_InvalidPeerCIDRFailsStartup(t *testing.T) { + for _, tc := range []struct { + name string + mutID func(c *config.Config) + field string + }{ + { + name: "invalid deny cidr", + mutID: func(c *config.Config) { c.TURN.DenyPeerCIDRs = []string{"203.0.113.0/33"} }, + field: "turn.deny_peer_cidrs", + }, + { + name: "invalid allow cidr", + mutID: func(c *config.Config) { c.TURN.AllowRestrictedPeerCIDRs = []string{"not-a-cidr"} }, + field: "turn.allow_restricted_peer_cidrs", + }, + } { + t.Run(tc.name, func(t *testing.T) { + conf := &config.Config{} + conf.TURN.Enabled = true + conf.TURN.UDPPort = 3478 + tc.mutID(conf) + + _, err := NewTurnServer(conf, nil, false) + require.Error(t, err) + require.Contains(t, err.Error(), tc.field) + }) + } +} + +func TestTURNAuthHandler_CreateUsername_TTLClamped(t *testing.T) { + h := newTestTurnAuthHandler() + pID := livekit.ParticipantID("PA_ttl_clamp") + + // An overflowing TTL must not wrap into a past expiry; it clamps to the max. + _, overflowExpiry := h.CreateUsername(turnTestAPIKey, pID, 1<<62+1) + require.Greater(t, overflowExpiry, time.Now().Unix()) + require.LessOrEqual(t, overflowExpiry, time.Now().Unix()+int64(config.TURNMaxTTLSeconds)+1) + + // A non-positive TTL falls back to the default rather than producing a past/wrapped expiry. + _, negativeExpiry := h.CreateUsername(turnTestAPIKey, pID, -1<<40) + require.InDelta(t, time.Now().Unix()+int64(config.DefaultTURNTTLSeconds), negativeExpiry, 2) +} diff --git a/test/singlenode_test.go b/test/singlenode_test.go index 6d88f4d8a..08f67fd00 100644 --- a/test/singlenode_test.go +++ b/test/singlenode_test.go @@ -1434,8 +1434,9 @@ func TestTurnAuthFailure(t *testing.T) { validPassword, err := authHandler.CreatePassword(testApiKey, pID, validExpiry) require.NoError(t, err) - // username encoded with an already-expired timestamp. - expiredUsername, _ := authHandler.CreateUsername(testApiKey, pID, -10) + // username encoded with an already-expired timestamp; constructed directly + // because CreateUsername floors non-positive TTLs to the default. + expiredUsername := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", testApiKey, pID, time.Now().Add(-time.Minute).Unix())) // username encoded with an api key the server does not know about. unknownAPIKeyUsername, _ := authHandler.CreateUsername("unknown-api-key", pID, 300)