Repository: syncthing/syncthing (language: go)

Issue:
Display error in 1.26 with login screen
I reverted back to http-basic-auth in 1.26. Essential information about the authentication with the new login screen is not displayed to me. There is full access after authorization because I can, for example, reactivate Basic Auth via the GUI. Browser: Chrome 118. In Firefox the GUI doesn't even build completely, some JS restarts the build every few seconds. After activating http-basic-auth, the GUI works again as usual in Firefox.
![2023-11-09_09-17](https://github.com/syncthing/syncthing/assets/33317545/eef74df0-6edc-4c59-b4ee-0f8b631142c7)
![2023-11-09_09-18](https://github.com/syncthing/syncthing/assets/33317545/43729a65-9367-4b0b-82b1-14dd7a4a7037)



Discussion / hints:
We're aware of the issue but weren't able reproduce it yet. Your screenshot is pretty interesting though, as half of the UI appears to work. This almost looks like some kind of race condition.
I can now add the following observation. After I was able to observe the error pattern on two hosts with syncthing increased from 1.25 to 1.26, I switched back to basic-auth as written and the error disappeared. Now I have switched two more hosts and the error didn't even occur on them in the same browser session with the new login mask, which surprised me. Then I switched the two first migrated hosts back to the new login mask as a cross-check, and lo and behold, the error disappeared, as if Chrome had to get used to it... If I can do anything to help find the cause, let me do so let me know.
Doesn't work for me in Chrome Version 119.0.6045.124 (Official Build) (64-bit).
But it works in Firefox and Edge.
![Screenshot 2023-11-10 003926](https://github.com/syncthing/syncthing/assets/150396699/5f7087fa-b086-45be-bc5a-966342c7f6ef)


![image](https://github.com/syncthing/syncthing/assets/150396699/e6254f24-a62f-4d1e-ae4a-7040365edf14)
Firefox 119.0.1 (64-bit)

@yuri-273 can you open the developer console in Chrome by pressing the F12 key? I'd be interested to see which cookies are present. Redacting the value column is enough.

https://developer.chrome.com/docs/devtools/application/cookies/


I just experienced the same issue, and I fixed it by clearing cookies. It seems there were 2 cookies named `sessionid-xxxx`






Deleted cookies. The problem is not solved.
![image](https://github.com/syncthing/syncthing/assets/150396699/715aa9e2-ec72-41e9-913d-5360705b8582)

I see this too (Chrome 119.0.6045.105; syncthing 1.26 built from source using Go 1.20.5). Not tried clearing cookies yet, will do so. Backed down to 1.24, which fortunately worked fine. The only extension I'm using is uBlock Origin, which is not blocking anything on this page.
I use MS Edge, met this issue also.
And I found why: I use Nginx proxy manager, and enabled `cache asset`. After disable it, syncthing works well.
I had the "endless reload loop" here (Firefox 118.0.2) on multiple installations of 1.26. Clearing cookies has solved the problem.

These were the cookies before I did that:

| Name             | Path         | Expires | HttpOnly | Secure |
| ---------------- | ------------ | ------- | -------- | ------ |
| CSRF-Token-XXXXX | /            | Session | false    | false  |
| sessionid-XXXXX  | /            | Session | false    | true   |
| sessionid-XXXXX  | /rest        | Session | false    | true   |
| sessionid-XXXXX  | /rest/svc    | Session | false    | true   |
| sessionid-XXXXX  | /rest/system | Session | false    | true   |

After deleting the cookies and logging in again, only CSRF-Token-XXXXX and the sessionid-XXXXX for path "/" were recreated.
Had to revert to Basic Auth as well (Chrome here). Clearing cache and cookies did not help. It logged but the interface was broken. Could only login through an anonymous tab.
So, my interpretation of all this:

- Previously, we did not set `Path: /` on the cookie. Sometimes it ended up on that path, but we also sometimes polluted the cookie space with `/rest` etc cookies as seen above.
- Currently, we set the cookie on `/` but when *getting* the cookie we sometimes get a random cookie for another path that we didn't set. This is an auth failure so we loop.

We can solve this now by either looking at *all* cookies and verifying that we check the one for `/`, or we can use a new cookie name and the old ones should be irrelevant.
There's a separate issue that logging *out* will not work as expected if the browser has also cached basic-auth credentials, but that's less critical imho.

Fix patch:
diff --git a/gui/default/syncthing/app.js b/gui/default/syncthing/app.js
index 80e9a20d7b4..0a961f50869 100644
--- a/gui/default/syncthing/app.js
+++ b/gui/default/syncthing/app.js
@@ -39,9 +39,8 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
         return;
     }
 
-    var deviceIDShort = metadata.deviceID.substr(0, 5);
-    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
-    $httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
+    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + metadata.deviceIDShort;
+    $httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + metadata.deviceIDShort;
 });
 
 // @TODO: extract global level functions into separate service(s)
diff --git a/lib/api/api.go b/lib/api/api.go
index 1f2fd3a5546..065b634d4be 100644
--- a/lib/api/api.go
+++ b/lib/api/api.go
@@ -365,15 +365,15 @@ func (s *service) Serve(ctx context.Context) error {
 
 	// Wrap everything in CSRF protection. The /rest prefix should be
 	// protected, other requests will grant cookies.
-	var handler http.Handler = newCsrfManager(s.id.String()[:5], "/rest", guiCfg, mux, locations.Get(locations.CsrfTokens))
+	var handler http.Handler = newCsrfManager(s.id.Short().String(), "/rest", guiCfg, mux, locations.Get(locations.CsrfTokens))
 
 	// Add our version and ID as a header to responses
 	handler = withDetailsMiddleware(s.id, handler)
 
 	// Wrap everything in basic auth, if user/password is set.
 	if guiCfg.IsAuthEnabled() {
-		sessionCookieName := "sessionid-" + s.id.String()[:5]
-		handler = basicAuthAndSessionMiddleware(sessionCookieName, guiCfg, s.cfg.LDAP(), handler, s.evLogger)
+		sessionCookieName := "sessionid-" + s.id.Short().String()
+		handler = basicAuthAndSessionMiddleware(sessionCookieName, s.id.Short().String(), guiCfg, s.cfg.LDAP(), handler, s.evLogger)
 		handlePasswordAuth := passwordAuthHandler(sessionCookieName, guiCfg, s.cfg.LDAP(), s.evLogger)
 		restMux.Handler(http.MethodPost, "/rest/noauth/auth/password", handlePasswordAuth)
 
@@ -719,6 +719,7 @@ func (*service) getSystemPaths(w http.ResponseWriter, _ *http.Request) {
 func (s *service) getJSMetadata(w http.ResponseWriter, _ *http.Request) {
 	meta, _ := json.Marshal(map[string]interface{}{
 		"deviceID":      s.id.String(),
+		"deviceIDShort": s.id.Short().String(),
 		"authenticated": true,
 	})
 	w.Header().Set("Content-Type", "application/javascript")
diff --git a/lib/api/api_auth.go b/lib/api/api_auth.go
index 7f96100683c..7af4faacb5e 100644
--- a/lib/api/api_auth.go
+++ b/lib/api/api_auth.go
@@ -42,8 +42,8 @@ func antiBruteForceSleep() {
 	time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
 }
 
-func unauthorized(w http.ResponseWriter) {
-	w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
+func unauthorized(w http.ResponseWriter, shortID string) {
+	w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="Authorization Required (%s)"`, shortID))
 	http.Error(w, "Not Authorized", http.StatusUnauthorized)
 }
 
@@ -78,7 +78,7 @@ func isNoAuthPath(path string) bool {
 		})
 }
 
-func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler {
+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)
@@ -117,7 +117,7 @@ func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfigura
 		// 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)
+			unauthorized(w, shortID)
 			return
 		}
 
diff --git a/lib/protocol/deviceid.go b/lib/protocol/deviceid.go
index 5effb8e3897..fee2f6d65e1 100644
--- a/lib/protocol/deviceid.go
+++ b/lib/protocol/deviceid.go
@@ -13,10 +13,15 @@ import (
 	"github.com/syncthing/syncthing/lib/sha256"
 )
 
-const DeviceIDLength = 32
+const (
+	DeviceIDLength      = 32
+	ShortIDStringLength = 7
+)
 
-type DeviceID [DeviceIDLength]byte
-type ShortID uint64
+type (
+	DeviceID [DeviceIDLength]byte
+	ShortID  uint64
+)
 
 var (
 	LocalDeviceID  = repeatedDeviceID(0xff)
@@ -94,7 +99,7 @@ func (s ShortID) String() string {
 	}
 	var bs [8]byte
 	binary.BigEndian.PutUint64(bs[:], uint64(s))
-	return base32.StdEncoding.EncodeToString(bs[:])[:7]
+	return base32.StdEncoding.EncodeToString(bs[:])[:ShortIDStringLength]
 }
 
 func (n *DeviceID) UnmarshalText(bs []byte) error {
diff --git a/next-gen-gui/src/app/api-utils.ts b/next-gen-gui/src/app/api-utils.ts
index 50d0a874d69..13d4137c725 100644
--- a/next-gen-gui/src/app/api-utils.ts
+++ b/next-gen-gui/src/app/api-utils.ts
@@ -1,8 +1,7 @@
 import { environment } from '../environments/environment'
 
 export const deviceID = (): String => {
-    const dID: String = environment.production ? globalThis.metadata['deviceID'] : '12345';
-    return dID.substring(0, 5)
+    return environment.production ? globalThis.metadata['deviceIDShort'] : '1234567';
 }
 
 export const apiURL: String = '/'


Test patch:
diff --git a/test/http_test.go b/test/http_test.go
index 4541c75b472..3dd9cf75196 100644
--- a/test/http_test.go
+++ b/test/http_test.go
@@ -173,7 +173,7 @@ func TestHTTPPOSTWithoutCSRF(t *testing.T) {
 	}
 	res.Body.Close()
 	hdr := res.Header.Get("Set-Cookie")
-	id := res.Header.Get("X-Syncthing-ID")[:5]
+	id := res.Header.Get("X-Syncthing-ID")[:protocol.ShortIDStringLength]
 	if !strings.Contains(hdr, "CSRF-Token") {
 		t.Error("Missing CSRF-Token in", hdr)
 	}