Repository: inventree/InvenTree

Issue:
Feature request: reverse proxy login support
I have traefik running as a reverse proxy in front of inventree.   The reverse proxy handles the AUTHENTICATION (and maybe AUTHORIZATION) before handing it off to any server running behind it.

In this case I have `https://inventree.{mydomain}.com`  pointing to the reverse proxy.  The reverse proxy authenticates with google, and then sets a cookie for auth.   The proxy then checks the presence of a valid auth cookie before handing the traffic off to inventree running in a docker container.  (This is all done with traefik, btw) 

In this scenario I have two login pages.  First for the google login page, and then the second for the inventree login page.  

So the reverse proxy will add a header to the request, in my case "X-Forwarded-User: me@gmail.com".  

It would be nice to have something along the lines in the config:

INVENTREE_REVERSE_PROXY_LOGIN=True
INVENTREE_REVERSE_PROXY_HEADER="X-Forwarded-User"

This would allow easier login for people/corporations that already have a reverse proxy system setup using LDAP/SSO/etc.  And Inventree wouldn't have to deal with complex auth mechanisms directly at all.  

Lastly Inventree would need something like this: 

INVENTREE_REVERSE_PROXY_AUTO_CREATE_USER=True/False

If True, an unknown user would be granted access (because both AUTHENTICATION of the user and AUTHORIZATION of the user accessing Inventree have been handled elsewhere) and Inventree should just add the new user into the User's table.

If False, AUTHENTICATION is handled by the reverse proxy, but AUTHORIZATION of the user accessing Inventree is controlled by the Django User table.  If a user exists in table and is_active=True then the user is granted access.  Otherwise the user gets a 403 Forbidden Page.

This way you can do fine grained control of user AUTHORIZATION in Inventree, yet still rely upon external AUTHENTICATION to validate who the person is.




Discussion:
sure, this seems reasonable and has been requested before:

- https://github.com/inventree/InvenTree/issues/625
- https://github.com/inventree/InvenTree/issues/634

I'm not sure how to implement this - have you any experience on configuring django in such a fashion?

We'd also need to clearly document this :)
Yes.  (Sadly)

So yeah... in 2021 it's fugly. It should be simple, but it's not.  There are companies making billions of dollars on this.
 
So what I'm suggesting is probably the simplest case possible for inventree.

Authentication is either handled by django's login page, or via the X-Forwarded-User header.   Then if you want something more complex than the simple login page, it's up to user to implement it and make sure the logged in user is put into the X-Forwarded-User header (or whatever header is configured above). 

Fortunately Django has support for this built in though the RemoteUserMiddleware and RemoteUserBackend classes.  YAY!

This is worth a careful read through. 

https://docs.djangoproject.com/en/3.2/howto/auth-remote-user/
@rcludwick is this feature something you'd be willing to spend some time on? I'm happy to support it but this is really not my area of expertise!
I'll look at it in my spare time.  It's mostly "configuration" thankfully.
@rcludwick did you find time to do this and would you be willing to share your findings for the docs?
No, I didn't get time to do this.

Having dealt with auth before, it comes down to two different ways to do
authentication in Django.

First is the middleware.  This looks at the request coming in, figures out
who the user is and then attaches the user to request.user and passes the
request on.  This is the way Django web login works.

The second is through authentication and authorization classes on DRF for
the apis.  This is usually through an http header.

In my case I have inventree sitting behind traefik-forward-auth which
handles the auth for me, and can add a header "proxy-forwarded-for" which
holds the name/email of the authenticated user.

So all requests from a browser have a cookie from traefik-forward-auth
which it intercepts and validates.  So any requests that make it through to
inventree should be considered authenticated.

One reason to do this is that I don't have to expose the inventree login
pages to the internet directly, but instead are redirected to google's
oauth2 service.

So in my configuration I need a middleware in Django that handles the
"proxy-forwarded-for" header properly.  This is the part I was going to
look at.

Many companies will have some kind of custom auth proxy in this
configuration that spits out a cookie or a header that Django can look at.
That should probably suffice for a good chunk of people.

So all requests that come from a browser will have a cookie attached to it
that provides auth.  You can either use the django login system or roll
your own that provides a header for inventree.

Great!  Now what about the API?

So this gets more complex, because much of this doesn't necessarily involve
a human in the loop, nor a web browser capable of storing and forwarding
cookies.

But on the other hand, DRF can use the default Django session cookies to
auth web browser api calls.

Yet you can also specify custom authentication and authorization classes in
a DRF view that are different than middleware auth.

Some people can put their API behind a proxy that will handle auth and
provide throttling and metrics.  Others may just not care and expect
everyone to use a web browser when using the API.

In my case traefik-forward-auth won't authenticate machine traffic, because
I have to login through Google via a web page.  Any apis I want to use with
a machine client will need to be whitelisted with traefik-forward-auth, and
then some kind of authorization header needs to be used and handled by
inventree directly.

Often people use JWTs (python-jose e.g.) that are generated by another
server. but this also could be done with a random string generated on
inventree passed to the machine client by the user.

So the key takeaway is that auth needs to be configurable.  And API auth
could be different than the browser.

You don't have to go crazy, but just provide some simple schemes for the
majority of use cases, and then if some company wants you to integrate with
their homegrown auth server, then charge them $200/hr.






On Thu, Aug 19, 2021, 3:56 PM Matthias Mair ***@***.***>
wrote:

> @rcludwick <https://github.com/rcludwick> did you find time to do this
> and would you be willing to share your findings for the docs?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/inventree/InvenTree/issues/1693#issuecomment-902276071>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAL67I6ULUY3KWY3ZN3QC33T5V42BANCNFSM47APK6ZA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email>
> .
>

@rcludwick thanks for the input, I will try to implement something if I find time. Your input would be appreciated if you find time 👼.
In summary, you just need two things.

A way to authenticate browser requests in middleware using a http header
(proxy_forwarded_for=user_or_email).

And... Some kind of token auth for API machine access, this could be a
token in the db with a foreign key back to the user.  Also default
permissions for unauthed API users should be configurable.  Many people
might want read only, others might want a 403.

Authlib is a good library to look at, particularly their Django
implementations for tokens .

This stuff is a 🐇🕳️.   I would avoid anything more complex than the
simplest API auth mechanism for now which is really just a shared secret.
Do jwt later until after you get a pluggable auth layer built.




On Fri, Aug 20, 2021, 2:22 PM Matthias Mair ***@***.***>
wrote:

> @rcludwick <https://github.com/rcludwick> thanks for the input, I will
> try to implement something if I find time. Your input would be appreciated
> if you find time 👼.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/inventree/InvenTree/issues/1693#issuecomment-902936473>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAL67I5DPDEYLGP7BUQOYCTT522OVANCNFSM47APK6ZA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email>
> .
>

This is similar to #625 but more specific about how it could be implemented.
I played around with this because a client needed it. It is very easy to do with the authentication backend and middleware settings in the config.yaml. It was ~40 LoC but really depenendet on how the infra is built out. Could be very dangerous if the admin setting it up does not know their stuff.
@SchrodingersGat should we still implement a solution for this? Maybe a configurable header that is used as a user reference?
I'm happy to provide a reference example but not implement it by default. A clear warning should be added to the documentation that "all care should be taken" but the implementing admin.
So what should I sketch out? A simple proxy set auth-header, LDAP or something else? Google etc. are already implemented in the SSO package.
@matmair I'm happy for you to make a suggestion based on what you think is sensible. This is straying very far from my area of knowledge.

My key requirements would be:

- It is all disabled by default
- It is reasonably difficult to 'accidentally' turn on
- It is documented with a very clear caveat that the admin must know exactly what they are doing.
@matmair happy to bump this one to 0.7.0?
@rcludwick are you still interested in this?

@matmair are you still keen to implement?
@SchrodingersGat yes. I planned with a longer release window and wanted to do it in the Easter break

Resolving patch:
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -2,6 +2,9 @@
 from django.urls import reverse_lazy, Resolver404
 from django.shortcuts import redirect
 from django.conf.urls import include, url
+from django.conf import settings
+from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
+
 import logging
 
 from rest_framework.authtoken.models import Token
@@ -112,3 +115,16 @@ def process_request(self, request):
                 super().process_request(request)
         except Resolver404:
             pass
+
+
+class InvenTreeRemoteUserMiddleware(PersistentRemoteUserMiddleware):
+    """
+    Middleware to check if HTTP-header based auth is enabled and to set it up
+    """
+    header = settings.REMOTE_LOGIN_HEADER
+
+    def process_request(self, request):
+        if not settings.REMOTE_LOGIN:
+            return
+
+        return super().process_request(request)
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -289,6 +289,7 @@ def _is_true(x):
     'django.middleware.csrf.CsrfViewMiddleware',
     'corsheaders.middleware.CorsMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'InvenTree.middleware.InvenTreeRemoteUserMiddleware',       # Remote / proxy auth
     'django_otp.middleware.OTPMiddleware',                      # MFA support
     'InvenTree.middleware.CustomAllauthTwoFactorMiddleware',    # Flow control for allauth
     'django.contrib.messages.middleware.MessageMiddleware',
@@ -302,6 +303,7 @@ def _is_true(x):
 MIDDLEWARE.append('error_report.middleware.ExceptionProcessor')
 
 AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
+    'django.contrib.auth.backends.RemoteUserBackend',           # proxy login
     'django.contrib.auth.backends.ModelBackend',
     'allauth.account.auth_backends.AuthenticationBackend',      # SSO login via external providers
 ])
@@ -853,6 +855,10 @@ def _is_true(x):
 SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
 ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
 
+# login settings
+REMOTE_LOGIN = get_setting('INVENTREE_REMOTE_LOGIN', CONFIG.get('remote_login', False))
+REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', CONFIG.get('remote_login_header', 'REMOTE_USER'))
+
 # Markdownx configuration
 # Ref: https://neutronx.github.io/django-markdownx/customization/
 MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')


Test patch: