Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] No possibility of token authentication against API when forward auth is enabled
### Description

Hello Paperless-Team,

the paperless version 2.4.0 broke my integration. I´m using paperless' forward auth feature together with a reverse proxy and authelia for authentication. The API endpoints are bypassed in the authelia config because they were secured with a dedicated user token.

The API is used for two things on my setup:

1) Homepage widget.
2) The paperless mobile app. 

Both are using tokens for authentication and do not require forward auth. It worked flawlessly over monthes. I think my problem has something to do with the following change: #5386. This does not take into account, that a mixed setup might also be useful.

This might also be a security flaw, when you update to 2.4.0 and the API endpoint was bypassed (becasue it was secured in the past with a token), it is easy to get access to another users account.

Thanks in advance for taking your time.

### Steps to reproduce

1. Enable forward auth
2. Bypass the API endpoint on your authentication provider (e.g. authelia)
3. Try to authenticate with a token

### Webserver logs

```bash
not applicable
```


### Browser logs

_No response_

### Paperless-ngx version

2.4.0

### Host OS

Ubuntu 22.04

### Installation method

Docker - official image

### Browser

Firefox

### Configuration changes

_No response_

### Other

_No response_

### Please confirm the following

- [X] I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation.
- [X] I have already searched for relevant existing issues and discussions before opening this report.
- [X] I have updated the title field above with a concise description.


Discussion:
Sorry can you explain this a bit more. You're saying what what exactly now happens bypassing the RP to API endpoints?

I believe you should still be able to use token auth with the API endpoints

Ps. Its this one if anything: https://github.com/paperless-ngx/paperless-ngx/pull/5386
The change is now that you can use Remote User headers to authenticate against the API, previously you couldn't, token auth has a lower priority, meaning that if Remote-User is present thats what will be used. So basically your reverse proxy should remove that header if you dont want it to be used
Yea, I have a similar setup and just checked to verify here.

I bypass authelia using traefik labels like this:

```
      - traefik.http.routers.paperless-api-rtr.rule=Host(`paperless.mydomain.com`) && PathPrefix(`/api/`) && HeadersRegexp(`Authorization`, `Token`)
      ...
      - traefik.http.routers.paperless-api-rtr.middlewares=chain-no-auth
```

Then

```
curl -H "Authorization: Token abc123" https://paperless.domain.com/api/
{"detail":"Invalid token."}%
```

So ultimately I think this is a setup issue, though I agree annoying it was breaking change for you. Please let us know if you have more clarification, etc. we can certainly re-address if needed
thanks for sharing your configuration. So the idea is to bypass it on the reverse proxy level - instead of a bypass rule in the authelia configuration.

I tested it in a similar way then you did. I do not use traeffic instead I'm using caddy. For others reading this thread and having trouble - here is my configuration:

```
@except expression !((path_regexp('^/api/.*')) && (header_regexp('Authorization', 'Basic.*|Token.*')) && !(header_regexp('Remote-User', '.*')))
import auth @except
```

Where auth is substituted as

```
(auth) {
    forward_auth {args.0} https://auth.example.com {
        uri /api/veify?rd=https://auth.example.com
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
        header_up Host {upstream_hostport}
    }
}
```

Testing it:

`curl -H "Remote-User: xyz" https://paperless.example.com/api/statistics/?format=json` leads to:
`<a href="https://auth.example.com/?rd=https://paperless.example.com%2Fapi%2Fstatistics%2F%3Fformat%3Djson&amp;rm=GET">302 Found</a>` ✅ 

`curl -H "Authorization: Token blabla" https://paperless.example.com/api/statistics/?format=json` leads to:
`{"detail":"Invalid token."}` ✅ 

`curl -H "Authorization: Basic dGVzdDpibGE=" https://paperless.example.comapi/statistics/?format=json` leads to
`{"detail":"Invalid username/password."}` ✅ 

`curl -H @header.txt https://paperless.example.com/api/statistics/?format=json` leads to
`<a href="https://auth.example.com/?rd=https://paperless.example.com%2Fapi%2Fstatistics%2F%3Fformat%3Djson&amp;rm=GET">302 Found</a>` ✅ 

Content of header.txt
```
Remote-User: abc
Authorization: Basic dGVzdDpibGE=
```



But my concern about the security topic still holds. If people did it like i did, they may have a security topic now. They have to adapt their bypass rules accordingly. Otherwise you only have to add the Remote-User header and you are in.

I cant say Im 100% clear on what you had before, but I'll note this from our docs:

```
Warning
This will allow authentication by simply adding a Remote-User: <username> header to a request. Use with care! You especially *must: ensure that any such header is not passed from your proxy server to paperless.

If you're exposing paperless to the internet directly, do not use this.

Also see the warning [in the official documentation](https://docs.djangoproject.com/en/4.1/howto/auth-remote-user/#configuration).
```

The RP should never pass that header to the app as that makes faking authentication very simple.

Yes I will be on the look out for other reports from other users, thanks
correct. I read an understood this warning at the moment i integrated it. I did it that way, because i knew you cannot interact with the API without a secret. But you changed this behavior silently and my previous assumption doesn't hold anymore.
What I mean is I think thats what you were doing previously which is why you weren't able to use the token because you were unintentionally passing the header. The warning isnt that the API doesnt work without a secret, the warning is dont pass the header to paperless.

Again, all thats changed is now you can use the header and if you do it is of a higher 'priority'. But as long as the header isnt there a token is still required.

At least this is my understanding of it all, I dont pretend to be an expert.

Resolving patch:
diff --git a/src/paperless/auth.py b/src/paperless/auth.py
--- a/src/paperless/auth.py
+++ b/src/paperless/auth.py
@@ -47,11 +47,3 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
     """
 
     header = settings.HTTP_REMOTE_USER_HEADER_NAME
-
-
-class PaperlessRemoteUserAuthentication(authentication.RemoteUserAuthentication):
-    """
-    REMOTE_USER authentication for DRF which overrides the default header.
-    """
-
-    header = settings.HTTP_REMOTE_USER_HEADER_NAME
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -420,31 +420,19 @@ def _parse_beat_schedule() -> dict:
     # regular login in case the provided user does not exist.
     MIDDLEWARE.insert(_index + 1, "paperless.auth.AutoLoginMiddleware")
 
+ENABLE_HTTP_REMOTE_USER = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
+HTTP_REMOTE_USER_HEADER_NAME = os.getenv(
+    "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME",
+    "HTTP_REMOTE_USER",
+)
 
-def _parse_remote_user_settings() -> str:
-    global MIDDLEWARE, AUTHENTICATION_BACKENDS, REST_FRAMEWORK
-    enable = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
-    if enable:
-        MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
-        AUTHENTICATION_BACKENDS.insert(
-            0,
-            "django.contrib.auth.backends.RemoteUserBackend",
-        )
-        REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].insert(
-            0,
-            "paperless.auth.PaperlessRemoteUserAuthentication",
-        )
-
-    header_name = os.getenv(
-        "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME",
-        "HTTP_REMOTE_USER",
+if ENABLE_HTTP_REMOTE_USER:
+    MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
+    AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend")
+    REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
+        "rest_framework.authentication.RemoteUserAuthentication",
     )
 
-    return header_name
-
-
-HTTP_REMOTE_USER_HEADER_NAME = _parse_remote_user_settings()
-
 # X-Frame options for embedded PDF display:
 X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN"
 


Test patch:
diff --git a/src/paperless/tests/test_remote_user.py b/src/paperless/tests/test_remote_user.py
deleted file mode 100644
--- a/src/paperless/tests/test_remote_user.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import os
-from unittest import mock
-
-from django.contrib.auth.models import User
-from rest_framework import status
-from rest_framework.test import APITestCase
-
-from documents.tests.utils import DirectoriesMixin
-from paperless.settings import _parse_remote_user_settings
-
-
-class TestRemoteUser(DirectoriesMixin, APITestCase):
-    def setUp(self):
-        super().setUp()
-
-        self.user = User.objects.create_superuser(
-            username="temp_admin",
-        )
-
-    def test_remote_user(self):
-        """
-        GIVEN:
-            - Configured user
-            - Remote user auth is enabled
-        WHEN:
-            - API call is made to get documents
-        THEN:
-            - Call succeeds
-        """
-
-        with mock.patch.dict(
-            os.environ,
-            {
-                "PAPERLESS_ENABLE_HTTP_REMOTE_USER": "True",
-            },
-        ):
-            _parse_remote_user_settings()
-
-            response = self.client.get("/api/documents/")
-
-            # 403 testing locally, 401 on ci...
-            self.assertIn(
-                response.status_code,
-                [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN],
-            )
-
-            response = self.client.get(
-                "/api/documents/",
-                headers={
-                    "Remote-User": self.user.username,
-                },
-            )
-
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-    def test_remote_user_header_setting(self):
-        """
-        GIVEN:
-            - Remote user header name is set
-        WHEN:
-            - Settings are parsed
-        THEN:
-            - Correct header name is returned
-        """
-
-        with mock.patch.dict(
-            os.environ,
-            {
-                "PAPERLESS_ENABLE_HTTP_REMOTE_USER": "True",
-                "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME": "HTTP_FOO",
-            },
-        ):
-            header_name = _parse_remote_user_settings()
-
-            self.assertEqual(header_name, "HTTP_FOO")