Subject: [PATCH] Use host as part of the key in memoryCredentialsCache
---
Index: src/org/openstreetmap/josm/io/auth/CredentialsManager.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/auth/CredentialsManager.java b/src/org/openstreetmap/josm/io/auth/CredentialsManager.java
--- a/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(date 1741440837937)
@@ -133,7 +133,7 @@
             }
         }
         // see #11914: clear cache before we store new value
-        purgeCredentialsCache(requestorType);
+        purgeCredentialsCache(requestorType, host);
         delegate.store(requestorType, host, credentials);
     }
 
@@ -141,7 +141,7 @@
     public CredentialsAgentResponse getCredentials(RequestorType requestorType, String host, boolean noSuccessWithLastResponse)
             throws CredentialsAgentException {
         CredentialsAgentResponse credentials = delegate.getCredentials(requestorType, host, noSuccessWithLastResponse);
-        if (requestorType == RequestorType.SERVER) {
+        if (requestorType == RequestorType.SERVER && Objects.equals(OsmApi.getOsmApi().getHost(), host)) {
             // see #11914 : Keep UserIdentityManager up to date
             String userName = credentials.getUsername();
             userName = userName == null ? "" : userName.trim();
@@ -174,4 +174,9 @@
     public void purgeCredentialsCache(RequestorType requestorType) {
         delegate.purgeCredentialsCache(requestorType);
     }
+
+    @Override
+    public void purgeCredentialsCache(RequestorType requestorType, String host) {
+        delegate.purgeCredentialsCache(requestorType, host);
+    }
 }
Index: test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java b/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java
--- a/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java	(date 1741443273485)
@@ -1,8 +1,13 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.io.auth;
 
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 import org.openstreetmap.josm.testutils.annotations.HTTP;
 
+import java.net.Authenticator;
+import java.util.List;
+
 /**
  * Test class for {@link CredentialsManager}
  */
@@ -12,4 +17,29 @@
     public CredentialsManager createAgent() {
         return new CredentialsManager(new JosmPreferencesCredentialAgent());
     }
+
+    @Test
+    public void testMultipleUnsavedHostsLookup() throws CredentialsAgentException {
+        final AbstractCredentialsAgent aca = new JosmPreferencesCredentialAgent();
+        // A provider that mimics user giving the credentials and choosing not to store them in preferences.
+        AbstractCredentialsAgent.setCredentialsProvider((requestorType, agent, response, username, password, host) -> {
+            response.setUsername("user" + host);
+            response.setPassword("password".toCharArray());
+            response.setSaveCredentials(false);
+            response.setCanceled(false);
+        });
+        final CredentialsManager agent = new CredentialsManager(aca);
+
+        String host1 = "example.com";
+        String host2 = "example.org";
+        for (String host : List.of(host1, host2)) {
+            // Try to get credentials after "failure" => provider gives the credentials.
+            agent.getCredentials(Authenticator.RequestorType.SERVER, host, true);
+        }
+        // Both hosts should receive their respective credentials.
+        CredentialsAgentResponse response = agent.getCredentials(Authenticator.RequestorType.SERVER, host1, false);
+        Assertions.assertEquals("user" + host1, response.getUsername());
+        response = agent.getCredentials(Authenticator.RequestorType.SERVER, host2, false);
+        Assertions.assertEquals("user" + host2, response.getUsername());
+    }
 }
Index: src/org/openstreetmap/josm/io/OsmServerReader.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/OsmServerReader.java b/src/org/openstreetmap/josm/io/OsmServerReader.java
--- a/src/org/openstreetmap/josm/io/OsmServerReader.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/src/org/openstreetmap/josm/io/OsmServerReader.java	(date 1741440837953)
@@ -207,7 +207,7 @@
             }
             try {
                 if (response.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
-                    CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER);
+                    CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER, OsmApi.getOsmApi().getHost());
                     throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED, null, null);
                 }
 
Index: src/org/openstreetmap/josm/io/auth/AbstractCredentialsAgent.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/auth/AbstractCredentialsAgent.java b/src/org/openstreetmap/josm/io/auth/AbstractCredentialsAgent.java
--- a/src/org/openstreetmap/josm/io/auth/AbstractCredentialsAgent.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/src/org/openstreetmap/josm/io/auth/AbstractCredentialsAgent.java	(date 1741442758870)
@@ -3,11 +3,12 @@
 
 import java.net.Authenticator.RequestorType;
 import java.net.PasswordAuthentication;
-import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 
 import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Pair;
 
 /**
  * Partial implementation of the {@link CredentialsAgent} interface.
@@ -47,7 +48,7 @@
         credentialsProvider = Objects.requireNonNull(provider, "provider");
     }
 
-    protected Map<RequestorType, PasswordAuthentication> memoryCredentialsCache = new EnumMap<>(RequestorType.class);
+    protected Map<Pair<RequestorType, String>, PasswordAuthentication> memoryCredentialsCache = new HashMap<>();
 
     @Override
     public CredentialsAgentResponse getCredentials(final RequestorType requestorType, final String host, boolean noSuccessWithLastResponse)
@@ -64,9 +65,10 @@
          * Last request was successful and there was no credentials stored in file (or only the username is stored).
          * -> Try to recall credentials that have been entered manually in this session.
          */
-        if (!noSuccessWithLastResponse && memoryCredentialsCache.containsKey(requestorType) &&
+        Pair<RequestorType, String> mccKey = Pair.create(requestorType, host);
+        if (!noSuccessWithLastResponse && memoryCredentialsCache.containsKey(mccKey) &&
                 (credentials == null || credentials.getPassword() == null || credentials.getPassword().length == 0)) {
-            PasswordAuthentication pa = memoryCredentialsCache.get(requestorType);
+            PasswordAuthentication pa = memoryCredentialsCache.get(mccKey);
             response.setUsername(pa.getUserName());
             response.setPassword(pa.getPassword());
             response.setCanceled(false);
@@ -88,7 +90,7 @@
                 ));
             } else {
                 // User decides not to save credentials to file. Keep it in memory so we don't have to ask over and over again.
-                memoryCredentialsCache.put(requestorType, new PasswordAuthentication(response.getUsername(), response.getPassword()));
+                memoryCredentialsCache.put(mccKey, new PasswordAuthentication(response.getUsername(), response.getPassword()));
             }
         } else {
             // We got it from file.
@@ -101,7 +103,12 @@
 
     @Override
     public final void purgeCredentialsCache(RequestorType requestorType) {
-        memoryCredentialsCache.remove(requestorType);
+        memoryCredentialsCache.keySet().removeIf(pair -> pair.a == requestorType);
+    }
+
+    @Override
+    public void purgeCredentialsCache(RequestorType requestorType, String host) {
+        memoryCredentialsCache.remove(Pair.create(requestorType, host));
     }
 
     /**
Index: src/org/openstreetmap/josm/io/OsmApi.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/OsmApi.java b/src/org/openstreetmap/josm/io/OsmApi.java
--- a/src/org/openstreetmap/josm/io/OsmApi.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/src/org/openstreetmap/josm/io/OsmApi.java	(date 1741440837945)
@@ -824,7 +824,7 @@
                         throw new OsmApiException(retCode, errorHeader, errorBody);
                 case HttpURLConnection.HTTP_UNAUTHORIZED:
                 case HttpURLConnection.HTTP_FORBIDDEN:
-                    CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER);
+                    CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER, getHost());
                     throw new OsmApiException(retCode, errorHeader, errorBody, activeConnection.getURL().toString(),
                             doAuthenticate ? retrieveBasicAuthorizationLogin(client) : null, response.getContentType());
                 default:
Index: src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java b/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
--- a/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(revision d0251052ab11561561ee0109a4d9c9f2f5f318ea)
+++ b/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(date 1741443259273)
@@ -83,11 +83,21 @@
     /**
      * Purges the internal credentials cache for the given requestor type.
      * @param requestorType the type of service.
-     * {@link RequestorType#SERVER} for the OSM API server, {@link RequestorType#PROXY} for a proxy server
+     * {@link RequestorType#PROXY} for a proxy server, {@link RequestorType#SERVER} for other servers.
      * @since 12992
      */
     void purgeCredentialsCache(RequestorType requestorType);
 
+    /**
+     * Purges the internal credentials cache for the given requestor type and host.
+     * @param requestorType the type of service.
+     * @param host the host.
+     * {@link RequestorType#PROXY} for a proxy server, {@link RequestorType#SERVER} for other servers.
+     */
+    default void purgeCredentialsCache(RequestorType requestorType, String host) {
+        purgeCredentialsCache(requestorType);
+    }
+
     /**
      * Provide a Panel that is shown below the API password / username fields
      * in the JOSM Preferences. (E.g. a warning that password is saved unencrypted.)
