jellyfin / jellyfin-android

Android Client for Jellyfin
https://jellyfin.org
GNU General Public License v2.0
1.57k stars 251 forks source link

Support HTTP basic auth for reverse proxies #123

Closed obbardc closed 1 month ago

obbardc commented 4 years ago

Describe the bug Using Traefik 2 setup as a HTTPS proxy with a http basic auth for all connections, Jellyfin for Android can't connect. This is the same with the Play store version and the latest RC released in this repository. The web interface works fine when using a browser.

To Reproduce

  1. Setup Traefik proxy with http basic auth
  2. Set hostname in Jellyfin app to the address
  3. Press connect
  4. See error "connection couldn't be established"

Expected behavior

The app to connect.

Logs

Screenshots

System (please complete the following information):

Additional context

nielsvanvelzen commented 4 years ago

We can't use the Authorization header for basic authentication because it's used for Jellyfin credentials. A solution I can think of is to use the Proxy-Authenticate header for this but I don't know if Traefik supports it and it will probably not work from within the webui.

obbardc commented 4 years ago

Right - the Traefik http basic auth is just another layer of added security. I expect to have to login twice. The Jellyfin webui works perfectly with separate logins, so in the first instance I'd like to replicate that on mobile app.

I'm currently on vacation, but Ill do some further digging once I am back at the desk ;-).

obbardc commented 4 years ago

For my reference later, here's some info about Android webview handling http basic auth: https://stackoverflow.com/questions/4220832/how-to-handle-basic-authentication-in-webview

obbardc commented 4 years ago

Here's the backtrace, it looks like the issue is with the Jellyfin java API rather than this app.

D/TimberLogger: Adding request to queue: https://jellyfin.<my_host>//System/Info/Public?format=json
E/Volley: [45297] BasicNetwork.performRequest: Unexpected response code 401 for https://jellyfin.<my_host>//System/Info/Public?format=json
E/TimberLogger: VolleyError com.android.volley.AuthFailureError: null
    com.android.volley.AuthFailureError
        at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:195)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
        at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
E/ContinuationResponse: org.jellyfin.apiclient.model.net.HttpException: VolleyError com.android.volley.AuthFailureError: 
        at org.jellyfin.apiclient.interaction.VolleyErrorListener.onErrorResponse(VolleyErrorListener.java:25)
        at com.android.volley.Request.deliverError(Request.java:617)
        at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:104)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7682)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
     Caused by: com.android.volley.AuthFailureError
        at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:195)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
        at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
mxvin commented 4 years ago

I experience similar issue on version 2.0.0-rc.5 - latest beta playstore, but I'm not using any HTTP Basics. Just Jelly behind nginx. Cannot connect to HTTPS (nginx) with app saying 'Connection cannot be established.....'. Turning off h2 have no effect, no log from Jelly and nginx so I suspect this may a problem on HTTPS implementation in the app. No problem if directly connect to Jellyfin via HTTP

nielsvanvelzen commented 4 years ago

@mxvin Please provide app logs

@obbardc the apiclient is currently ongoing a big refactor and it's behavior might change. The error from your logs basically says that no credentials were given when retrieving server info (which is caused by your proxy since the public endpoint doesn't require authentication at all and thus the apiclient won't provide them).

alexandre-abrioux commented 3 years ago

It would be great to be able to input the basic auth directly in the URL, like this: https://user:password@www.example.com/. I don't have any knowledge with Kotlin but looking at the code we would probably need to parse the URL before loading the webview: https://github.com/jellyfin/jellyfin-android/blob/2e6983c731139aad7458581fdb69e888e3ef6998/app/src/main/java/org/jellyfin/mobile/fragment/WebViewFragment.kt#L202 ; and handle auth with the onReceivedHttpAuthRequest listener.

nielsvanvelzen commented 3 years ago

Like I said before, The HTTP Basic Authentication Scheme uses the Authorization header in HTTP requests which is already used for Jellyfin account authorization. We can't add basic auth on top of that.

alexandre-abrioux commented 3 years ago

Thanks. I tried to get more info from my setup where it's actually working behind basic auth.

Jellyfin apparently allows the use of the X-Emby-Authorization header to pass the authentication token, see: https://github.com/jellyfin/jellyfin/blob/4a3411cad17c95f3afe0870a1ff3a9afc10286fa/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs#L211

The web-ui uses this header instead of the Authorization, see the Javascript client code: https://github.com/jellyfin/jellyfin-apiclient-javascript/blob/e2c51b2c67e73dfb1e736cbbfc84d73165f5968a/src/apiClient.js#L177

The Kodi plugin also started to use this header since this PR: https://github.com/jellyfin/jellyfin-kodi/pull/56/files

Is it something we could replicate in the Java client?

nielsvanvelzen commented 3 years ago

We have decided to use the Authorization header because it is a common standard. We will not change it to use the X-Emby-Authorization header.

alexandre-abrioux commented 3 years ago

I agree with that, but we could switch to it if we detect basic auth in the input URL, that could be a great compromise, what do you think?

kelvtech-co-uk commented 3 years ago

Sorry for the noob question but is this issue where the android app fails to connect because of traefik http basic auth being used or is there a general compatibility issue with traefik and the mobile app?

I ask as I'm just dipping my toe into traefik with jelly and am successfully reverse proxying to a public domain but the app on my OnePlus6 just won't connect. I'm not using http basic auth but I confuss I'm not very experienced so some of the details in this thread are a little beyond me at the moment.

obbardc commented 3 years ago

Sorry for the noob question but is this issue where the android app fails to connect because of traefik http basic auth being used or is there a general compatibility issue with traefik and the mobile app?

I ask as I'm just dipping my toe into traefik with jelly and am successfully reverse proxying to a public domain but the app on my OnePlus6 just won't connect. I'm not using http basic auth but I confuss I'm not very experienced so some of the details in this thread are a little beyond me at the moment.

the jellyfin app works fine with traefik. Suggest you should post your configuration files in full detail in their forum for more help - here isn't really the place to discuss that :-).

caillou commented 3 years ago

Would it be conceivable to use the same username and password for both, the reverse proxy and Jellyfin, and tell the reverse proxy to forward the Authorization header?

nielsvanvelzen commented 3 years ago

The Authorization header uses a custom scheme that is specific to Jellyfin so I don't think it's going to work in a proxy.

Maxr1998 commented 3 years ago

Specifically, Jellyfin uses Authorization: MediaBrowser <fields> (he fields contain info like the app id), whereas the proxy uses basic auth with Authorization: Basic <username:password encoded as base64>.

caillou commented 3 years ago

After some more digging on this, I have the impression the Proxy-Authorization header could be used to authenticate on the reverse-proxy, be it Nginx or Traefik. Would this be a possible route?

nielsvanvelzen commented 3 years ago

I think that might be an option. I proposed it earlier (https://github.com/jellyfin/jellyfin-android/issues/123#issuecomment-691633388). We need to add support for it in the SDK first and then expose the option in the UI.

caillou commented 3 years ago

One more question: would it be feasible to use Jellyfin as an authentication backend for Nginx or Traefik?

Could the reverse-proxy use Jellyfin's Authorization header scheme, and validate it against the Jellyfin server? Ideally, this would be transparent to the client.

nielsvanvelzen commented 3 years ago

It might be possible to do with a plugin for nginx/traefik by calling the authenticateByName endpoint with a username+password and if a token is returned the authentication was successful. A bit out of the scope for this issue though. Feel free to join our Matrix channels if you need help with the api.

3c7 commented 3 years ago

Would love to see Basic Auth compatibility with the Android app. I'm using an Nginx reverse proxy with Basic Auth in order to prevent direct exposition of JellyFin to the internet.

Is the use of Authorization header instead of X-Emby-Authorization a general concept which will also be implemented in the WebUI?

jellyfin-bot commented 2 years ago

This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.

If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.

This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on Matrix or Social Media.

anderspitman commented 1 year ago

FWIW, I believe the best solution for this would be for Jellyfin to implement header authentication, and for the clients to implement standard auth flows like OIDC. Most reverse proxies support this (search for "forward auth"). This gives the most flexibility in allowing users to choose an auth system that works for them, and frees Jellyfin from a lot of auth concerns.

That said, I think implementing X-Emby-Authorization is the most reasonable short-term solution. I totally agree with @nielsvanvelzen that standard headers should be preferred and Proxy-Authorization would be the correct choice, but it doesn't seem to be widely supported or at least documented. Temporarily supporting a custom header to enable users to improve their security seems like a worthwhile tradeoff to me.

Janhouse commented 10 months ago

I did some quick tests with forward auth and it seems like Jellyfin already randomly passes different headers for different requests. There is some info here

In order to make forward-auth work with Jellyfin in a good way and without making too many modifications on different clients, Jellyfin client should pass some type of identifier at all times. Or ideally - use the cookie store.

Here is why:

If Jellyfin clients enabled cookie store (not used right now), and allowed to follow redirects (also seems like media player doesn't follow redirects), forwardauth could be used to manage access. This is not basic auth, but it is much better than basic auth, since this would enable use of SSO for all the clients.

image

Simply enabling cookies and redirects don't do much without a custom forwardauth service, but that can be built (either as a 3rd party tool, or as part of Jellyfin server).

In order to tie clients to specific user, a timed pairing mechanism could be used. Similarly to Bluetooth or Zigbee pairing, where any session is allowed to pair when pairing mode is enabled. IP address/network match between client and the session enabling pairing would be required.

High level mock:

image

This can already be somewhat implemented without cookies by using existing headers (Authorization, X-Emby-Token, etc.) but the problem is that clients don't send those headers for all requests and unless user already has a session (from logging in before forwardauth was enabled), it won't be sent at all. IP address/network level checks could be used to allow connecting during initial pairing but it does not resolve the issue with public endpoints not sending the authorization headers afterwards (and we do not want to keep any resources exposed without forward-auth)

So the best way to enable this behavior would be usage of cookies and redirects (which are not enabled on clients yet).

Where does the SSO come into play? SSO can be used to grant access to pairing site.

Urogna commented 7 months ago

@Janhouse suggestion should get more attention

Janhouse commented 7 months ago

@Janhouse suggestion should get more attention

I actually implemented the custom forward-auth service for Jellyfin and the approach works fine for all the clients I tested with (android, android tv, ios, desktop).

hhftechnology commented 4 months ago

@Janhouse can you explain it in detail please how did you do it.

erpalma commented 3 months ago

Any update on this issue?

kjvalencik commented 2 months ago

Great idea with forward_auth. It can be used to easily integrate additional auth mechanisms. For example, I'm using it in Caddy along with oauth2-proxy to add Google auth.

example.com {
    handle /oauth2/* {
        reverse_proxy oauth2-proxy:4180
    }

    handle {
        forward_auth oauth2-proxy:4180 {
            uri /oauth2/auth?allowed_emails=example.user1@gmail.com,example.user2@gmail.com
        }

        reverse_proxy jellyfin:8096
    }
}

This works great for browsers, unfortunately, most other Jellyfin apps can't do OAuth. We can add some basic auth to support those, detecting the header and swapping the auth method:

example.com {
    # Determine if we have basic auth
    @basic_auth header Authorization "Basic *"

    handle /oauth2/* {
        reverse_proxy oauth2-proxy:4180
    }

    handle {
        # Use basic auth if we have a basic auth header
        handle @basic_auth {
            basic_auth {
                 media $2a$...
            }
        }

        # Use OAuth
        handle {
            forward_auth oauth2-proxy:4180 {
                uri /oauth2/auth?allowed_emails=example.user1@gmail.com,example.user2@gmail.com
            }
        }

        reverse_proxy jellyfin:8096
    }
}

Now if basic auth is provided, it will use it. This gets Kodi working and any other app that uses the X-Emby-Authorization header. Unfortunately, it won't work for apps that use the Authorization header, like Android.

We can solve this by getting creative and putting the basic auth header on the URL and mapping it back in the reverse proxy.

  1. Copy any existing Authorization header to the X-Emby-Authorization header
  2. Pass auth in the URL like /basic/<base64(username:password)/...rest of the URL...
  3. Extract auth from the URL and write it to the Authorization header

Currently, Jellyfin checks X-Emby-Authorization before Authorization, but if that gets swapped at some point it will be necessary to either delete the Authorization header or map the original to a temp variable.

I tested with Kodi, Web and Android. In Android, I pass https://example.com/basic/<base64(username:password)> as the server. Unfortunately, since Caddy doesn't have a way to base64 encode, it requires doing that with the username/password manually.

I found that Jellyfin will return incorrect redirects. This is usually solved by setting "Base URL", but we can't do that since the URL is dynamic. Instead, we can rewrite it.

reverse_proxy jellyfin:8096 {
    header_down Location /?(.*) "/basic/{re.path_auth.1}/$1"
}

Here is the full version that allows OAuth2 and basic auth while supporting most clients.

example.com {
    log {
        format filter {
            # Remove the password from the logs
            request>uri regexp ^/basic/[^/]* ""
        }
    }

    handle /oauth2/* {
        reverse_proxy oauth2-proxy:4180
    }

    handle {
        # Save a copy of the original authorization header
        map "{header.X-Emby-Authorization}" "{emby_auth}" {
           ~(.+) "$1"
           default "{header.Authorization}"
        }

        # Check everywhere we could have auth
        @basic_auth header Authorization "Basic *"
        @path_auth path_regexp ^/basic/([^/]*)
        @oauth_cookie header Cookie _oauth2_proxy=*

        # Make path based auth look like basic auth
        uri @path_auth strip_prefix "/basic/{re.path_auth.1}"
        request_header @path_auth "X-Emby-Authorization" "{emby_auth}"
        request_header @path_auth Authorization "Basic {re.path_auth.1}"

        handle @basic_auth {
            basic_auth {
                 media $2a$...
            }

            reverse_proxy jellyfin:8096 {
                header_down Location /?(.*) "/basic/{re.path_auth.1}/$1"
            }
        }

        handle @oauth_cookie {
            forward_auth oauth2-proxy:4180 {
                uri /oauth2/auth?allowed_emails=example.user1@gmail.com,example.user2@gmail.com
            }

            reverse_proxy jellyfin:8096
        }

        handle {
            respond <<EOF
                Not Found

                EOF 404
        }

A couple of notes:

I would recommend only using basic auth in clients that absolutely need it. Path based basic auth should be nearly secure as typical basic auth because the path is encrypted by TLS, although it will likely show in server logs.

Hopefully this helps others!

sj14 commented 1 month ago

I did some ugly hacking but it seems to work: https://github.com/sj14/ip-auth Basically a proxy which only accepts specific IPs (can be preconfigured or a be a host/DDNS address), or added dynamically by passing basic auth once (from any device on the same IP). I'm currently testing it as a sidecar in my Jellyfin pod on Kubernetes.

Janhouse commented 1 month ago

Jellyfin has a somewhat terrible code base that is split in multiple parts and everyone is doing whatever they want, without anyone overseeing the development efforts.

Because of the fragmentation of the code base and lack of general architecture principles, my custom forward-auth for Jellyfin is forced to use multiple methods of identifying sessions:

1) Paths like /, /web/, /System/Info, /Branding/Css, /system/info/public/, /web/*.json don't provide DeviceId or Token, so they have to be let through at almost all times, which sucks and makes it simple to identify that Jellyfin is running there. So they have to be blocked behind timed gate.

2) Some requests have unique session/key identifiers '(?<=api_key=)(.*?)(?=&|$), (?<=[Dd]eviceId=)(.*?)(?=&|$), /\/web\/.+\.(js|css)\?([a-z0-9]{20})$/, (?<=[Tt]oken=")(.*?)(?="), those are not great but better than the 1st option.

3) And some requests provide headers x-mediabrowser-token, x-emby-authorization and authorization, and those are the best, but almost none of the code-base uses those.

4) And some clients fully support cookies, for which you can set your own cookie and ignore the rest of the issues.

None of this is set in stone, between versions, those identifiers tend to shift away, more requests are available publicly so you have to resolve to using IP whitelisting, at least partially.

In my case I use timed pairing session which opens the gate for all requests until session is identified and stored in the database.

image

Afterwards it uses all of the mentioned methods to keep the session open. It works the best with Jellyfin for Android TV since it always allows to identify the device id early on. One of the desktop client also supports cookies, but not on all operating systems. For other apps you sometimes have to start the pairing process again to get access.

Jellyfin developers should really step up and solve that mess by introducing some general auth mechanism on the server side that covers all of the scenarios. Either in the form of some proxy or just rewriting all of the affected services. :disappointed:

nielsvanvelzen commented 1 month ago

I'm going to close this issue as it has derailed quite a bit and the discussion is no longer about HTTP basic auth support in the mobile Android app. It has been mentioned before that we use the authorization header for Jellyfin itself already and thus we won't be adding any support for basic auth. We're open to alternative options like proxy auth but that wouldn't be specific to the Android client and needs to be discussed in our meta discussion repository first.

Hacking around with a reverse proxy is strongly discouraged and we won't provide any support for it.

thomask77 commented 1 month ago

This is unfortunate, because Jellyfin 10.9.11 also broke basic-auth for desktop clients.