diff options
Diffstat (limited to 'lib/api/api_auth.go')
-rw-r--r-- | lib/api/api_auth.go | 200 |
1 files changed, 107 insertions, 93 deletions
diff --git a/lib/api/api_auth.go b/lib/api/api_auth.go index 7af4faacb..cee0397ad 100644 --- a/lib/api/api_auth.go +++ b/lib/api/api_auth.go @@ -16,15 +16,16 @@ import ( ldap "github.com/go-ldap/ldap/v3" "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/rand" - "github.com/syncthing/syncthing/lib/sync" "golang.org/x/exp/slices" ) -var ( - sessions = make(map[string]bool) - sessionsMut = sync.NewMutex() +const ( + maxSessionLifetime = 7 * 24 * time.Hour + maxActiveSessions = 25 + randomTokenLength = 64 ) func emitLoginAttempt(success bool, username, address string, evLogger events.Logger) { @@ -78,75 +79,91 @@ func isNoAuthPath(path string) bool { }) } -func basicAuthAndSessionMiddleware(cookieName, shortID string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if hasValidAPIKeyHeader(r, guiCfg) { - next.ServeHTTP(w, r) - return - } +type basicAuthAndSessionMiddleware struct { + cookieName string + shortID string + guiCfg config.GUIConfiguration + ldapCfg config.LDAPConfiguration + next http.Handler + evLogger events.Logger + tokens *tokenManager +} + +func newBasicAuthAndSessionMiddleware(cookieName, shortID string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger, miscDB *db.NamespacedKV) *basicAuthAndSessionMiddleware { + return &basicAuthAndSessionMiddleware{ + cookieName: cookieName, + shortID: shortID, + guiCfg: guiCfg, + ldapCfg: ldapCfg, + next: next, + evLogger: evLogger, + tokens: newTokenManager("sessions", miscDB, maxSessionLifetime, maxActiveSessions), + } +} + +func (m *basicAuthAndSessionMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if hasValidAPIKeyHeader(r, m.guiCfg) { + m.next.ServeHTTP(w, r) + return + } - for _, cookie := range r.Cookies() { - // We iterate here since there may, historically, be multiple - // cookies with the same name but different path. Any "old" ones - // won't match an existing session and will be ignored, then - // later removed on logout or when timing out. - if cookie.Name == cookieName { - sessionsMut.Lock() - _, ok := sessions[cookie.Value] - sessionsMut.Unlock() - if ok { - next.ServeHTTP(w, r) - return - } + for _, cookie := range r.Cookies() { + // We iterate here since there may, historically, be multiple + // cookies with the same name but different path. Any "old" ones + // won't match an existing session and will be ignored, then + // later removed on logout or when timing out. + if cookie.Name == m.cookieName { + if m.tokens.Check(cookie.Value) { + m.next.ServeHTTP(w, r) + return } } + } - // Fall back to Basic auth if provided - if username, ok := attemptBasicAuth(r, guiCfg, ldapCfg, evLogger); ok { - createSession(cookieName, username, guiCfg, evLogger, w, r) - next.ServeHTTP(w, r) - return - } + // Fall back to Basic auth if provided + if username, ok := attemptBasicAuth(r, m.guiCfg, m.ldapCfg, m.evLogger); ok { + m.createSession(username, false, w, r) + m.next.ServeHTTP(w, r) + return + } - // Exception for static assets and REST calls that don't require authentication. - if isNoAuthPath(r.URL.Path) { - next.ServeHTTP(w, r) - return - } + // Exception for static assets and REST calls that don't require authentication. + if isNoAuthPath(r.URL.Path) { + m.next.ServeHTTP(w, r) + return + } - // Some browsers don't send the Authorization request header unless prompted by a 401 response. - // This enables https://user:pass@localhost style URLs to keep working. - if guiCfg.SendBasicAuthPrompt { - unauthorized(w, shortID) - return - } + // Some browsers don't send the Authorization request header unless prompted by a 401 response. + // This enables https://user:pass@localhost style URLs to keep working. + if m.guiCfg.SendBasicAuthPrompt { + unauthorized(w, m.shortID) + return + } - forbidden(w) - }) + forbidden(w) } -func passwordAuthHandler(cookieName string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, evLogger events.Logger) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req struct { - Username string - Password string - } - if err := unmarshalTo(r.Body, &req); err != nil { - l.Debugln("Failed to parse username and password:", err) - http.Error(w, "Failed to parse username and password.", http.StatusBadRequest) - return - } +func (m *basicAuthAndSessionMiddleware) passwordAuthHandler(w http.ResponseWriter, r *http.Request) { + var req struct { + Username string + Password string + StayLoggedIn bool + } + if err := unmarshalTo(r.Body, &req); err != nil { + l.Debugln("Failed to parse username and password:", err) + http.Error(w, "Failed to parse username and password.", http.StatusBadRequest) + return + } - if auth(req.Username, req.Password, guiCfg, ldapCfg) { - createSession(cookieName, req.Username, guiCfg, evLogger, w, r) - w.WriteHeader(http.StatusNoContent) - return - } + if auth(req.Username, req.Password, m.guiCfg, m.ldapCfg) { + m.createSession(req.Username, req.StayLoggedIn, w, r) + w.WriteHeader(http.StatusNoContent) + return + } - emitLoginAttempt(false, req.Username, r.RemoteAddr, evLogger) - antiBruteForceSleep() - forbidden(w) - }) + emitLoginAttempt(false, req.Username, r.RemoteAddr, m.evLogger) + antiBruteForceSleep() + forbidden(w) } func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, evLogger events.Logger) (string, bool) { @@ -172,11 +189,8 @@ func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg c return "", false } -func createSession(cookieName string, username string, guiCfg config.GUIConfiguration, evLogger events.Logger, w http.ResponseWriter, r *http.Request) { - sessionid := rand.String(32) - sessionsMut.Lock() - sessions[sessionid] = true - sessionsMut.Unlock() +func (m *basicAuthAndSessionMiddleware) createSession(username string, persistent bool, w http.ResponseWriter, r *http.Request) { + sessionid := m.tokens.New() // Best effort detection of whether the connection is HTTPS -- // either directly to us, or as used by the client towards a reverse @@ -186,45 +200,45 @@ func createSession(cookieName string, username string, guiCfg config.GUIConfigur strings.Contains(strings.ToLower(r.Header.Get("forwarded")), "proto=https") // If the connection is HTTPS, or *should* be HTTPS, set the Secure // bit in cookies. - useSecureCookie := connectionIsHTTPS || guiCfg.UseTLS() + useSecureCookie := connectionIsHTTPS || m.guiCfg.UseTLS() + maxAge := 0 + if persistent { + maxAge = int(maxSessionLifetime.Seconds()) + } http.SetCookie(w, &http.Cookie{ - Name: cookieName, + Name: m.cookieName, Value: sessionid, // In HTTP spec Max-Age <= 0 means delete immediately, // but in http.Cookie MaxAge = 0 means unspecified (session) and MaxAge < 0 means delete immediately - MaxAge: 0, + MaxAge: maxAge, Secure: useSecureCookie, Path: "/", }) - emitLoginAttempt(true, username, r.RemoteAddr, evLogger) + emitLoginAttempt(true, username, r.RemoteAddr, m.evLogger) } -func handleLogout(cookieName string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - for _, cookie := range r.Cookies() { - // We iterate here since there may, historically, be multiple - // cookies with the same name but different path. We drop them - // all. - if cookie.Name == cookieName { - sessionsMut.Lock() - delete(sessions, cookie.Value) - sessionsMut.Unlock() - - // Delete the cookie - http.SetCookie(w, &http.Cookie{ - Name: cookieName, - Value: "", - MaxAge: -1, - Secure: cookie.Secure, - Path: cookie.Path, - }) - } +func (m *basicAuthAndSessionMiddleware) handleLogout(w http.ResponseWriter, r *http.Request) { + for _, cookie := range r.Cookies() { + // We iterate here since there may, historically, be multiple + // cookies with the same name but different path. We drop them + // all. + if cookie.Name == m.cookieName { + m.tokens.Delete(cookie.Value) + + // Delete the cookie + http.SetCookie(w, &http.Cookie{ + Name: m.cookieName, + Value: "", + MaxAge: -1, + Secure: cookie.Secure, + Path: cookie.Path, + }) } + } - w.WriteHeader(http.StatusNoContent) - }) + w.WriteHeader(http.StatusNoContent) } func auth(username string, password string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration) bool { |