This adds a "token manager" which handles storing and checking expired tokens, used for both sessions and CSRF tokens. It removes the old, corresponding functionality for CSRFs which saved things in a file. The result is less crap in the state directory, and active login sessions now survive a Syncthing restart (this really annoyed me). It also adds a boolean on login to create a longer-lived session cookie, which is now possible and useful. Thus we can remain logged in over browser restarts, which was also annoying... :) <img width="1001" alt="Screenshot 2023-12-12 at 09 56 34" src="https://github.com/syncthing/syncthing/assets/125426/55cb20c8-78fc-453e-825d-655b94c8623b"> Best viewed with whitespace-insensitive diff, as a bunch of the auth functions became methods instead of closures which changed indentation.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// Copyright (C) 2024 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type tokenManager struct {
|
||||
key string
|
||||
miscDB *db.NamespacedKV
|
||||
lifetime time.Duration
|
||||
maxItems int
|
||||
|
||||
timeNow func() time.Time // can be overridden for testing
|
||||
|
||||
mut sync.Mutex
|
||||
tokens *TokenSet
|
||||
saveTimer *time.Timer
|
||||
}
|
||||
|
||||
func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager {
|
||||
tokens := &TokenSet{
|
||||
Tokens: make(map[string]int64),
|
||||
}
|
||||
if bs, ok, _ := miscDB.Bytes(key); ok {
|
||||
_ = tokens.Unmarshal(bs) // best effort
|
||||
}
|
||||
return &tokenManager{
|
||||
key: key,
|
||||
miscDB: miscDB,
|
||||
lifetime: lifetime,
|
||||
maxItems: maxItems,
|
||||
timeNow: time.Now,
|
||||
mut: sync.NewMutex(),
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
// Check returns true if the token is valid, and updates the token's expiry
|
||||
// time. The token is removed if it is expired.
|
||||
func (m *tokenManager) Check(token string) bool {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
expires, ok := m.tokens.Tokens[token]
|
||||
if ok {
|
||||
if expires < m.timeNow().UnixNano() {
|
||||
// The token is expired.
|
||||
m.saveLocked() // removes expired tokens
|
||||
return false
|
||||
}
|
||||
|
||||
// Give the token further life.
|
||||
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
|
||||
m.saveLocked()
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// New creates a new token and returns it.
|
||||
func (m *tokenManager) New() string {
|
||||
token := rand.String(randomTokenLength)
|
||||
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
|
||||
m.saveLocked()
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// Delete removes a token.
|
||||
func (m *tokenManager) Delete(token string) {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
delete(m.tokens.Tokens, token)
|
||||
m.saveLocked()
|
||||
}
|
||||
|
||||
func (m *tokenManager) saveLocked() {
|
||||
// Remove expired tokens.
|
||||
now := m.timeNow().UnixNano()
|
||||
for token, expiry := range m.tokens.Tokens {
|
||||
if expiry < now {
|
||||
delete(m.tokens.Tokens, token)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a limit on the number of tokens, remove the oldest ones.
|
||||
if m.maxItems > 0 && len(m.tokens.Tokens) > m.maxItems {
|
||||
// Sort the tokens by expiry time, oldest first.
|
||||
type tokenExpiry struct {
|
||||
token string
|
||||
expiry int64
|
||||
}
|
||||
var tokens []tokenExpiry
|
||||
for token, expiry := range m.tokens.Tokens {
|
||||
tokens = append(tokens, tokenExpiry{token, expiry})
|
||||
}
|
||||
slices.SortFunc(tokens, func(i, j tokenExpiry) int {
|
||||
return int(i.expiry - j.expiry)
|
||||
})
|
||||
// Remove the oldest tokens.
|
||||
for _, token := range tokens[:len(tokens)-m.maxItems] {
|
||||
delete(m.tokens.Tokens, token.token)
|
||||
}
|
||||
}
|
||||
|
||||
// Postpone saving until one second of inactivity.
|
||||
if m.saveTimer == nil {
|
||||
m.saveTimer = time.AfterFunc(time.Second, m.scheduledSave)
|
||||
} else {
|
||||
m.saveTimer.Reset(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tokenManager) scheduledSave() {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
m.saveTimer = nil
|
||||
|
||||
bs, _ := m.tokens.Marshal() // can't fail
|
||||
_ = m.miscDB.PutBytes(m.key, bs) // can fail, but what are we going to do?
|
||||
}
|
||||
Reference in New Issue
Block a user