Repository: conan-io/conan

Issue:
[bug] Cached login expires
We use baselined VMs to do our build/test. During the vm configuration, we set the user/pass via `conan user` using a artifactory token which we set not expire (although this still shows 365 days expiration...but I digress). However, we are finding that the login still seems to expire (maybe after 30 days?). We can launch the baseline, login again using the same token, and then it continues to work...so it doesn't appear to be a problem with the token. Is there a way to prevent this?

### Environment Details (include every applicable attribute)
  * Operating System+version: Both Windows 10 and RHEL 7
  * Compiler+version: n/a
  * Conan version: 1.48
  * Python version: 3.10.4

### Steps to reproduce (Include if Applicable)
1. login to remote using non-expiring token
2. wait 30 days
3. login stops working

[feature] Improve error message when not authenticated with remote
### What is your suggestion?

Currently when the user is not authenticated with a JFrog Conan repository, the Conan client won't say so. Neither during operations like conan create / install, nor while explicitly searching for a specific package.

Instead the Conan client reports something like this:

    $ conan search -r conanserver mypkg
    There are no packages matching the 'mypkg' pattern

This is technically correct in a way. But it also annoyingly insufficient, especially to new users. They expect to be notified when they need to log in, especially since the JFrog Artifactory Conan server seems to reset the authentication token quite often.

A simple warning would already be sufficient, something like:

    $ conan search -r conanserver mypkg
    Warning: skipping remote "conanserver" due to authentication failure
    There are no packages matching the 'mypkg' pattern

This would really improve usability and would prevent users from wondering what's going on. Especially since the output of commands like conan create can be very verbose and confusing, it is often not clear what's actually happening.

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide


Discussion:
Hi @rconde01 

There are 2 tokens involved here:
- The server token you are using as "password" to auth
- A "jwt" token that is stored and cached to avoid using the main password or token over and over at every API request, which would be super annoying

The jwt had an issue in Artifactory and it never expired IIRC, so it was a security issue. Now it is defaulted to 30 days. I think this can also be changed in some Artifactory configuration, but I am not sure it is great to make it non-expiring, as that  wouldn't be a security best practice.

That means there is nothing in the client related to expiring tokens, everything is server side. What you could do is to define the conan CONAN_PASSWORD env-var with the token value, and it will re-auth from the client when necessary.
@memsharded is there some way to give user a better error in case of token expiration? Currently our Artifactory instance doesn't ask explicitly for credentials so user sees "ERROR: Unable to find 'library/version@user/channel#recipehash' in remotes ?".

maybe conan can store expiration date for "jwt" token?
> is there some way to give user a better error in case of token expiration? Currently our Artifactory instance doesn't ask explicitly for credentials so user sees "ERROR: Unable to find 'library/version@user/channel#recipehash' in remotes

@memsharded this is the same problem we face on developer machines. and it always takes some minutes to recognize that you didn't use a wrong package reference but the login expired. Is there a way to signal this to the user?

Also when using `conan user` this will show something like this:
```
Current user of remote '...' set to: '...' [Authenticated]
```
which states that you are authenticated although the token expired.
The ``conan remote auth`` command was introduced in Conan 2.0 to force a real authentication in the servers, forcing users to re-enter credentials if necessary or erroring. That would be useful for that.

Otherwise, if it is important for developers to be authenticated in the servers, it would be recommended to disable anonymous read access from those servers.

Hi @stevn 

I think this issue has been reported before, and it seems something that cannot be solved, because it is a matter of server configuration. The server is not returning any authentication failure, it is returning a 404-NotFound. This is a typical server security strategy, not returning any information about things for non authenticated users. It is possible to configure the server so it actually throws an auth challenge and request the user to enter credentials, instead of returning 404:
```
conan search * -r=conan-local
Please log in to "conan-local" to perform this action. Execute "conan remote login" command.
Remote 'conan-local' username:
```
@memsharded 

I think the client should distinguish between an empty results list and a http 404 error? Seems weird to me to treat both as the same thing.

I must admit I haven't looked at the server communication code yet though.
This topic seems to be discussed here for example:

https://stackoverflow.com/questions/13366730/proper-rest-response-for-empty-table
As @memsharded said, this approach is quite normal, where an unauthenticated user can not know whether a resource is missing or if they don't have enough permissions to see it. https://jfrog.com/help/r/jfrog-platform-administration-documentation/hide-existence-of-unauthorized-resources explains how to toggle this feature in Artifactory
Hey @memsharded and @RubenRBS 

Would it be possible to at least print a hint that the user is not authenticated? It could be printed when artifactory is returning 404 and the user status for this remote is unauthenticated. In our company we can't easily toggle the hiding feature in Artifactory and it happens basically every week that a developer run in this issue. A hint would increase the self-solving rate dramatically, I guess.
Hi @nicosmd, maybe that can work, thanks for the suggestion! Let me take a look into it and see how it goes :)



Patch:
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index e67745ba8e4..ca2dc361727 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -60,18 +60,10 @@ def call_rest_api_method(self, remote, method_name, *args, **kwargs):
             raise ForbiddenException(f"Permission denied for user: '{user}': {e}")
         except AuthenticationException:
             # User valid but not enough permissions
-            if user is None or token is None:
-                # token is None when you change user with user command
-                # Anonymous is not enough, ask for a user
-                ConanOutput().info('Please log in to "%s" to perform this action. '
-                                   'Execute "conan remote login" command.' % remote.name)
-                if self._get_credentials_and_authenticate(rest_client, user, remote):
-                    return self.call_rest_api_method(remote, method_name, *args, **kwargs)
-            else:
-                # Token expired or not valid, so clean the token and repeat the call
-                # (will be anonymous call but exporting who is calling)
-                # logger.info("Token expired or not valid, cleaning the saved token and retrying")
-                self._clear_user_tokens_in_db(user, remote)
+            # token is None when you change user with user command
+            # Anonymous is not enough, ask for a user
+            ConanOutput().info(f"Remote '{remote.name}' needs authentication, obtaining credentials")
+            if self._get_credentials_and_authenticate(rest_client, user, remote):
                 return self.call_rest_api_method(remote, method_name, *args, **kwargs)
 
     def _get_credentials_and_authenticate(self, rest_client, user, remote):
@@ -95,14 +87,6 @@ def _get_credentials_and_authenticate(self, rest_client, user, remote):
                 return True
         raise AuthenticationException("Too many failed login attempts, bye!")
 
-    def _clear_user_tokens_in_db(self, user, remote):
-        try:
-            self._creds.set(remote, user, token=None)
-        except Exception as e:
-            out = ConanOutput()
-            out.error('Your credentials could not be stored in local cache', error_type="exception")
-            out.debug(str(e) + '\n')
-
     def _authenticate(self, rest_client, remote, user, password):
         try:
             token = rest_client.authenticate(user, password)


Test patch:
diff --git a/test/integration/remote/auth_test.py b/test/integration/remote/auth_test.py
index 80aacdcb8ac..da585c94e1f 100644
--- a/test/integration/remote/auth_test.py
+++ b/test/integration/remote/auth_test.py
@@ -202,9 +202,9 @@ def test_token_expired():
        */*@*/*: admin
        """)
     save(os.path.join(server_folder, ".conan_server", "server.conf"), server_conf)
-    server = TestServer(base_path=server_folder, users={"admin": "password"})
+    server = TestServer(base_path=server_folder, users={"admin": "password", "other": "pass"})
 
-    c = TestClient(servers={"default": server}, inputs=["admin", "password"])
+    c = TestClient(servers={"default": server}, inputs=["admin", "password", "other", "pass"])
     c.save({"conanfile.py": GenConanfile()})
     c.run("create . --name=pkg --version=0.1 --user=user --channel=stable")
     c.run("upload * -r=default -c")
@@ -216,13 +216,12 @@ def test_token_expired():
     import time
     time.sleep(3)
     c.users = {}
-    conan_conf = "core:non_interactive=True"
-    c.save_home({"global.conf": conan_conf})
     c.run("remove * -c")
     c.run("install --requires=pkg/0.1@user/stable")
+    assert "Remote 'default' needs authentication, obtaining credentials" in c.out
     user, token, _ = localdb.get_login(server.fake_url)
-    assert user == "admin"
-    assert token is None
+    assert user == "other"
+    assert token is not None
 
 
 def test_auth_username_space():