Originally created by @rmkjr on GitHub (Jun 24, 2022).
Is your feature request related to a problem? Please describe.
Zero Trust like app access, in particular Cloudflare Tunnels, are becoming quite popular in the self-hosted world. They allow you to make your self-hosted applications available externally without opening ports. However, they typically use a OpenID Connect or SAML style SSO approach to authenticate access which means an intermediate login page appears before connectivity to the application endpoint is then allowed. As a workaround to client apps that cannot support this additional authentication step, Cloudflare supports the use of service tokens passed with requests as additional headers. https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
The only alternative would be to forward the ports directly from the internet, or to always have a VPN or similar running when remote. Allowing custom headers to be set for the server connection would allow for very secure remote access.
Additional context
N/A
Originally created by @rmkjr on GitHub (Jun 24, 2022).
### Is your feature request related to a problem? Please describe.
Zero Trust like app access, in particular Cloudflare Tunnels, are becoming quite popular in the self-hosted world. They allow you to make your self-hosted applications available externally without opening ports. However, they typically use a OpenID Connect or SAML style SSO approach to authenticate access which means an intermediate login page appears before connectivity to the application endpoint is then allowed. As a workaround to client apps that cannot support this additional authentication step, Cloudflare supports the use of service tokens passed with requests as additional headers.
https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
### Describe the solution you'd like
Support adding additional custom headers, custom keys and values, when setting up the server connection to allow us to set additional headers for requests made to the server. A great app citizen that has done this quite well is LunaSea.
https://docs.lunasea.app/modules/sonarr#custom-headers
https://imgur.com/a/JSC1bYn
### Describe alternatives you've considered
The only alternative would be to forward the ports directly from the internet, or to always have a VPN or similar running when remote. Allowing custom headers to be set for the server connection would allow for very secure remote access.
### Additional context
N/A
Thanks for the detailed request with an example implementation. Do you think this is something that would be implemented in the server settings and used for all clients? Or is this something on the connect screen of the mobile app where you would specify custom headers?
@advplyr commented on GitHub (Jun 24, 2022):
Thanks for the detailed request with an example implementation. Do you think this is something that would be implemented in the server settings and used for all clients? Or is this something on the connect screen of the mobile app where you would specify custom headers?
Typically I would see this implemented on the connect screen of the mobile app. If you were to say have multiple unique users, you may want to give them unique headers to authenticate to the tunnel broker in addition to their normal Audiobookshelf username/password.
That should also be a bit simpler. As the mobile app, if a user provides some number of extra headers via optional fields, would just need to append them to the http requests. The server and its authentication mechanisms could just ignore them as it's only the tunnel broker that is using them.
@rmkjr commented on GitHub (Jun 24, 2022):
Typically I would see this implemented on the connect screen of the mobile app. If you were to say have multiple unique users, you may want to give them unique headers to authenticate to the tunnel broker in addition to their normal Audiobookshelf username/password.
That should also be a bit simpler. As the mobile app, if a user provides some number of extra headers via optional fields, would just need to append them to the http requests. The server and its authentication mechanisms could just ignore them as it's only the tunnel broker that is using them.
The way adding a new server connection config works now is you first enter your server address
On submit the mobile app will ping the address using https://abs.example.com/ping
Would this /ping request require custom headers already added?
@advplyr commented on GitHub (Jun 25, 2022):
The way adding a new server connection config works now is you first enter your server address

On submit the mobile app will ping the address using `https://abs.example.com/ping`
Would this /ping request require custom headers already added?
That is my understanding yes. Every http request made to the server endpoint should append on any added custom headers.
In the case of a Cloudflare tunnel, the url entered into the app is a Cloudflare endpoint. When a request is made to the endpoint the first thing it does is check the request headers. If the service auth token headers exist, or if a valid cookie header from a past interactive authentication session exist, then Cloudflare will pass that traffic to the Audiobookshelf server uninterrupted. If no such header(s) exist, then Cloudflare will attempt to redirect that request to the configured authentication IdP for authentication. By creating a service auth token and appending it to all request, Cloudflare will see that request as already authenticated, and pass it to the underlying endpoint, in this case the Audiobookshelf server, without needing the app to handle any extra authentication pages/steps.
So to your point, you would have to give users the opportunity to add/set their custom headers at that onboarding screen so they exist and can be added to the first request made to the server, as well as every following request.
LunaSea does it as a secondary screen. Basically a button on that submit button screen to add custom headers. That button then opens an additional screen where a user can add an arbitrary number of custom key/value pairs. Then you go back to the screen with the server url, and when a user clicks submit the added keys and values will be sent as appended request headers for all future requests including the first one to /ping.
@rmkjr commented on GitHub (Jun 25, 2022):
That is my understanding yes. Every http request made to the server endpoint should append on any added custom headers.
In the case of a Cloudflare tunnel, the url entered into the app is a Cloudflare endpoint. When a request is made to the endpoint the first thing it does is check the request headers. If the service auth token headers exist, or if a valid cookie header from a past interactive authentication session exist, then Cloudflare will pass that traffic to the Audiobookshelf server uninterrupted. If no such header(s) exist, then Cloudflare will attempt to redirect that request to the configured authentication IdP for authentication. By creating a service auth token and appending it to all request, Cloudflare will see that request as already authenticated, and pass it to the underlying endpoint, in this case the Audiobookshelf server, without needing the app to handle any extra authentication pages/steps.
So to your point, you would have to give users the opportunity to add/set their custom headers at that onboarding screen so they exist and can be added to the first request made to the server, as well as every following request.
LunaSea does it as a secondary screen. Basically a button on that submit button screen to add custom headers. That button then opens an additional screen where a user can add an arbitrary number of custom key/value pairs. Then you go back to the screen with the server url, and when a user clicks submit the added keys and values will be sent as appended request headers for all future requests including the first one to /ping.
Got it. I'm not sure what the best UI/UX is for this given I did this a bit differently then most servers with the /ping step.
We could re-design this or add a "Add Custom Headers" button to the left of the submit button which would open a modal to put in custom headers.
@advplyr commented on GitHub (Jun 25, 2022):
Got it. I'm not sure what the best UI/UX is for this given I did this a bit differently then most servers with the `/ping` step.
We could re-design this or add a "Add Custom Headers" button to the left of the submit button which would open a modal to put in custom headers.
The idea of an extra button to open a modal like you've described I think makes total sense.
I'm on both your TestFlight and Google Play beta, and also have Cloudflare tunnels setup for other apps, so I'll definitely be able to help you test it out.
Have to say btw, this whole thing is super cool. Have been playing around with ABS after getting my instance stood up this week and it's quite amazing! Thanks for thinking up and creating such an awesome project!
@rmkjr commented on GitHub (Jun 25, 2022):
The idea of an extra button to open a modal like you've described I think makes total sense.
I'm on both your TestFlight and Google Play beta, and also have Cloudflare tunnels setup for other apps, so I'll definitely be able to help you test it out.
Have to say btw, this whole thing is super cool. Have been playing around with ABS after getting my instance stood up this week and it's quite amazing! Thanks for thinking up and creating such an awesome project!
I'm about half-way into implementing this and realized that we won't be able to use headers for static content on the server like images. For apps do you typically allow bypassing for static content? Like specifying a path to allow through.
@advplyr commented on GitHub (Jun 25, 2022):
I'm about half-way into implementing this and realized that we won't be able to use headers for static content on the server like images. For apps do you typically allow bypassing for static content? Like specifying a path to allow through.
Another issue is the images for book covers and authors are going through the api routes.
Here is an example get request for a book cover image /api/items/li_rghidt3v6x9ef35sq3/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwiaWF0IjoxNjUyNTYyNDM5fQ
This is requested directly from an HTML image element and will cache and return the optimized image.
@advplyr commented on GitHub (Jun 25, 2022):
Another issue is the images for book covers and authors are going through the api routes.
Here is an example get request for a book cover image `/api/items/li_rghidt3v6x9ef35sq3/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwiaWF0IjoxNjUyNTYyNDM5fQ`
This is requested directly from an HTML image element and will cache and return the optimized image.
I think one could technically bypass tunnel authentication for particular URL locations, but that's not one that I've run into so far with the several apps I have routed that way.
I think ideally all requests to the server endpoint, including those for well-known static content, would add the additional request headers.
I've been out of the dev game for about a decade, so going to brush up against some knowledge edges here. It looks like you have the user token passed as an "Authorization" header in most cases. I think these extra headers would be appended basically every place that ABS user token is added to requests.
I also see the http img element for covers for example where it is setting the src to the known cover url. In those cases, does this url endpoint also bypass checking the ABS's own Authorization header? That is to say the /api endpoint (or maybe just /api/items) is open and unauthenticed? If that is true, then I think you'd be correct. Either the particular location (like /api/items) would have to bypass authentication tunnel side so it can remain unauthenticed, or those request would have to be refactored and be handled by a request method that can handle additional request headers rather than the inherent thing that is used to load an img tag's src.
@rmkjr commented on GitHub (Jun 25, 2022):
I think one could technically bypass tunnel authentication for particular URL locations, but that's not one that I've run into so far with the several apps I have routed that way.
I think ideally all requests to the server endpoint, including those for well-known static content, would add the additional request headers.
I've been out of the dev game for about a decade, so going to brush up against some knowledge edges here. It looks like you have the user token passed as an "Authorization" header in most cases. I think these extra headers would be appended basically every place that ABS user token is added to requests.
I also see the http img element for covers for example where it is setting the src to the known cover url. In those cases, does this url endpoint also bypass checking the ABS's own Authorization header? That is to say the /api endpoint (or maybe just /api/items) is open and unauthenticed? If that is true, then I think you'd be correct. Either the particular location (like /api/items) would have to bypass authentication tunnel side so it can remain unauthenticed, or those request would have to be refactored and be handled by a request method that can handle additional request headers rather than the inherent thing that is used to load an img tag's src.
When authenticating for images ABS will check the query string token passed in the url.
That is why in the example request above for the image you see the token passed in the URL. This is also used when streaming, your token gets passed in as a query string when direct playing an mp3 file for example.
I'm not sure how to set custom headers for requests made through HTML elements, it may not be possible. Do these other services you use that offer this support serve images from your server? I would be curious if you could check the HTTP request or if they have source code to look at.
@advplyr commented on GitHub (Jun 25, 2022):
When authenticating for images ABS will check the query string `token` passed in the url.
That is why in the example request above for the image you see the token passed in the URL. This is also used when streaming, your token gets passed in as a query string when direct playing an mp3 file for example.
I'm not sure how to set custom headers for requests made through HTML elements, it may not be possible. Do these other services you use that offer this support serve images from your server? I would be curious if you could check the HTTP request or if they have source code to look at.
LunaSea, which aggregates as an app frontend for quite a few of the services I host out through these tunnels, does have similar static content loaded. For example, both Radarr and Sonarr have image covers for Movies and TV shows that the LunaSea app will pull from the respective sever and display.
I poked around its source a bit, but from the little I could interpret, it looks to be squarely in some overarching framework that may happen to have a request method that is capable of adding request headers. Guessing that means they're requesting the image content via those methods and then building their views rather than an img tag in a view loading it directly.
@rmkjr commented on GitHub (Jun 25, 2022):
LunaSea, which aggregates as an app frontend for quite a few of the services I host out through these tunnels, does have similar static content loaded. For example, both Radarr and Sonarr have image covers for Movies and TV shows that the LunaSea app will pull from the respective sever and display.
I poked around its source a bit, but from the little I could interpret, it looks to be squarely in some overarching framework that may happen to have a request method that is capable of adding request headers. Guessing that means they're requesting the image content via those methods and then building their views rather than an img tag in a view loading it directly.
https://github.com/JagandeepBrar/LunaSea
Yeah that is the only way to do it. We would need to write a custom image component as a wrapper around the img tag that would fetch the images first with the proper headers then use file blob in img src. I think it is common to do this so it shouldn't impact performance.
There are also images used in the player notification and in android auto that would need to be downloaded first.
@advplyr commented on GitHub (Jun 26, 2022):
Yeah that is the only way to do it. We would need to write a custom image component as a wrapper around the img tag that would fetch the images first with the proper headers then use file blob in img src. I think it is common to do this so it shouldn't impact performance.
There are also images used in the player notification and in android auto that would need to be downloaded first.
I took a look at lunasea and how it integrate into other apps and I have a couple of thoughts.
It looks like loading static assets are not a problem since the the Cloudflare Tunnel will generate and set a JWT (cookie) on the first auth to the tunnel. So any other requests that come from the client should send the cookie along with the http request, which happens normally from most apps.
As for putting this in front of ABS. Since this system is much more designed for apps that dont have there own user logins this isnt as useful as it could be since this basically puts an additional auth/login (the tunnel) in front of the app. Which works great for things like Radarr and such since they dont have any kind of user/login system. Its also a weird workflow when it comes to the clients trying to connect into the server since it would require another set of creds to be passed into the app.
@hskrtich commented on GitHub (Jun 26, 2022):
I took a look at lunasea and how it integrate into other apps and I have a couple of thoughts.
It looks like loading static assets are not a problem since the the Cloudflare Tunnel will generate and set a JWT (cookie) on the first auth to the tunnel. So any other requests that come from the client should send the cookie along with the http request, which happens normally from most apps.
According to https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
`When a request is made to an application behind our network, the request will submit them both to Access. If the service token is valid, Access generates a JWT scoped to the application. All subsequent requests with that JWT will succeed until the expiration of that JWT.`
As for putting this in front of ABS. Since this system is much more designed for apps that dont have there own user logins this isnt as useful as it could be since this basically puts an additional auth/login (the tunnel) in front of the app. Which works great for things like Radarr and such since they dont have any kind of user/login system. Its also a weird workflow when it comes to the clients trying to connect into the server since it would require another set of creds to be passed into the app.
If that is the case then that could really simplify things with the images and stream requests. I think in order to test that I would need to set this up.
@advplyr commented on GitHub (Jun 26, 2022):
If that is the case then that could really simplify things with the images and stream requests. I think in order to test that I would need to set this up.
An example what does this very well are all the 'arr' apps. Like Sonarr, Radarr, Prowlarr. and also SABnzbd. Each app has an API key (in the case of audiobookshelf, there would be an API key per user account).
I am then able to use NZB360 which passes the API key in the https headers. I use Traefik to catch this API key to allow access through the proxy and then login to the endpoint application.
If the API key is missing like when accessing not via the NZB360 app, It will use google oauth to authenticate the proxy.
@Presjar commented on GitHub (Jul 14, 2022):
An example what does this very well are all the 'arr' apps. Like Sonarr, Radarr, Prowlarr. and also SABnzbd. Each app has an API key (in the case of audiobookshelf, there would be an API key per user account).
I am then able to use NZB360 which passes the API key in the https headers. I use Traefik to catch this API key to allow access through the proxy and then login to the endpoint application.
If the API key is missing like when accessing not via the NZB360 app, It will use google oauth to authenticate the proxy.
`Snip from my dockerconfig
## HTTP Routers Auth Bypass
- "traefik.http.routers.prowlarr-rtr-bypass.entrypoints=https"
- "traefik.http.routers.prowlarr-rtr-bypass.rule=Host(`prowlarr.$DOMAINNAME`) && (Headers(`X-Api-Key`, `$PROWLARR_API_KEY`) || Query(`apikey`, `$PROWLARR_API_KEY`))"
- "traefik.http.routers.prowlarr-rtr-bypass.priority=100"
## HTTP Routers Auth
- "traefik.http.routers.prowlarr-rtr.entrypoints=https"
- "traefik.http.routers.prowlarr-rtr.rule=Host(`prowlarr.$DOMAINNAME`)"
- "traefik.http.routers.prowlarr-rtr.priority=99"
`
The big question still is the static assets. If anyone is able to test what @bskrtich mentioned about the cookie then the header stuff is almost implemented for all the API requests, just not the static file requests like images and audio files
@advplyr commented on GitHub (Jul 22, 2022):
The big question still is the static assets. If anyone is able to test what @bskrtich mentioned about the cookie then the header stuff is almost implemented for all the API requests, just not the static file requests like images and audio files
I've got my audiobookshelf instance behind a reverse proxy with some auth middleware, how would I go about testing the app?
I'm using the android 9.59-beta version, but I don't see a button for custom headers on the add server screen. Do I need a different build with the feature enabled?
@zackyancey commented on GitHub (Nov 12, 2022):
I've got my audiobookshelf instance behind a reverse proxy with some auth middleware, how would I go about testing the app?
I'm using the android 9.59-beta version, but I don't see a button for custom headers on the add server screen. Do I need a different build with the feature enabled?
@advplyr commented on GitHub (Nov 12, 2022):
To test this you would need to uncomment out this code block https://github.com/advplyr/audiobookshelf-app/blob/master/components/connection/ServerConnectForm.vue#L25
and build the app from source. That is just to show the button that allows you to add custom headers.
Id like to try to take a stab at auto showing a web login page if the server is behind a zero trust config. This would allow users to login how ever they need to via the zero trust and see the correct session headers with out the header http header field.
@hskrtich commented on GitHub (Nov 12, 2022):
Id like to try to take a stab at auto showing a web login page if the server is behind a zero trust config. This would allow users to login how ever they need to via the zero trust and see the correct session headers with out the header http header field.
I haven't gone deeply into the SSO & auth middleware world for self-hosting but it seems like the solutions being implemented are all over the place.
@advplyr commented on GitHub (Nov 12, 2022):
I haven't gone deeply into the SSO & auth middleware world for self-hosting but it seems like the solutions being implemented are all over the place.
For the most part, they're implementing main open standards utilised even in enterprise settings. Most commonly nowadays they implement using SAML or OIDC but similar apps like Calibre-Web, they simply read a header and combine it with LDAP for a more crude solution
Solutions like Authentik are available now where all of that is possible as it's pretty much All-in-one so we can pair up pretty much SSO choice made in any other app
@Avsynthe commented on GitHub (Nov 13, 2022):
For the most part, they're implementing main open standards utilised even in enterprise settings. Most commonly nowadays they implement using SAML or OIDC but similar apps like Calibre-Web, they simply read a header and combine it with LDAP for a more crude solution
Solutions like Authentik are available now where all of that is possible as it's pretty much All-in-one so we can pair up pretty much SSO choice made in any other app
I gave it a shot, but it wasn't able to connect to the server:
2022-11-13 09:56:25.057 9871-9871/com.audiobookshelf.app.debug I/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: [Axios] Making request to https://.../ping
2022-11-13 09:56:25.152 9871-9905/com.audiobookshelf.app.debug D/EGL_emulation: app_time_stats: avg=101.26ms min=6.50ms max=480.49ms count=10
2022-11-13 09:56:25.660 9871-9949/com.audiobookshelf.app.debug I/cr_X509Util: Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
2022-11-13 09:56:25.752 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/connect - Line 0 - Msg: Access to XMLHttpRequest at 'https://.../ping' from origin 'http://localhost' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: Axios error code NaN
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/a8e4746.js - Line 1 - Msg: Server check failed Error: Network Error
I can get to the site in a web browser, so I don't think there's an HTTPS problem. Could the pre-flight request be being sent without the right headers? The auth provider will try to redirect to the sign in page if it doesn't see the right headers, so that might be the redirect in the log. I can curl /ping with the right headers manually and that part of it seems to work.
@zackyancey commented on GitHub (Nov 13, 2022):
I gave it a shot, but it wasn't able to connect to the server:
```
2022-11-13 09:56:25.057 9871-9871/com.audiobookshelf.app.debug I/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: [Axios] Making request to https://.../ping
2022-11-13 09:56:25.152 9871-9905/com.audiobookshelf.app.debug D/EGL_emulation: app_time_stats: avg=101.26ms min=6.50ms max=480.49ms count=10
2022-11-13 09:56:25.660 9871-9949/com.audiobookshelf.app.debug I/cr_X509Util: Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
2022-11-13 09:56:25.752 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/connect - Line 0 - Msg: Access to XMLHttpRequest at 'https://.../ping' from origin 'http://localhost' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/57a3121.js - Line 1 - Msg: Axios error code NaN
2022-11-13 09:56:25.754 9871-9871/com.audiobookshelf.app.debug E/Capacitor/Console: File: http://localhost/_nuxt/a8e4746.js - Line 1 - Msg: Server check failed Error: Network Error
```
I can get to the site in a web browser, so I don't think there's an HTTPS problem. Could the pre-flight request be being sent without the right headers? The auth provider will try to redirect to the sign in page if it doesn't see the right headers, so that might be the redirect in the log. I can curl `/ping` with the right headers manually and that part of it seems to work.
I don't know what's considered standard, but the one I'm using looks like it will redirect to the sign in page if you try to access any protected URL without being signed in.
@zackyancey commented on GitHub (Nov 13, 2022):
I don't know what's considered standard, but the one I'm using looks like it will redirect to the sign in page if you try to access any protected URL without being signed in.
Then disabling your auth middleware to see how the request come in. Unless you can add logs to your auth middleware to see what the request looks like.
@advplyr commented on GitHub (Nov 13, 2022):
I guess one way to test would be to run the server locally and add a console.log for the req here https://github.com/advplyr/audiobookshelf/blob/master/server/Auth.js#L20
Then disabling your auth middleware to see how the request come in. Unless you can add logs to your auth middleware to see what the request looks like.
Yes, its normal to redirect any request including a preflight/OPTIONS request since at that point any request isnt authenticated. The header will need to get set for any request sent to the server.
@hskrtich commented on GitHub (Nov 13, 2022):
Yes, its normal to redirect any request including a preflight/OPTIONS request since at that point any request isnt authenticated. The header will need to get set for *any* request sent to the server.
I'm not sure why the headers wouldn't be included in the preflight request. I would assume they are but only way to find out for sure is to log the request.
@advplyr commented on GitHub (Nov 13, 2022):
I'm not sure why the headers wouldn't be included in the preflight request. I would assume they are but only way to find out for sure is to log the request.
X-API-Key is the header that's needed to get past the authentication. I see it mentioned in the Access-Control-Request-Headers header, but the key header itself is missing.
@zackyancey commented on GitHub (Nov 13, 2022):
Here's a log for the request:
```
{
"level": "info",
"ts": 1668373973.5617716,
"logger": "http.log.access.log1",
"msg": "handled request",
"request": {
"remote_ip": "...",
"remote_port": "...",
"proto": "HTTP/2.0",
"method": "OPTIONS",
"host": "audiobookshelf.example.com",
"uri": "/ping",
"headers": {
"Sec-Fetch-Mode": [
"cors"
],
"X-Requested-With": [
"com.audiobookshelf.app.debug"
],
"Sec-Fetch-Site": [
"cross-site"
],
"Access-Control-Request-Headers": [
"x-api-key"
],
"User-Agent": [
"Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"
],
"Origin": [
"http://localhost"
],
"Sec-Fetch-Dest": [
"empty"
],
"Referer": [
"http://localhost/"
],
"Accept-Encoding": [
"gzip, deflate"
],
"Accept-Language": [
"en-US,en;q=0.9"
],
"Accept": [
"*/*"
],
"Access-Control-Request-Method": [
"GET"
]
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "audiobookshelf.example.com"
}
},
"user_id": "",
"duration": 0.00022408,
"size": 5,
"status": 302,
"resp_headers": {
"Server": [
"Caddy"
],
"Alt-Svc": [
"h3=\":443\"; ma=2592000"
],
"Location": [
"(redirects to the auth portal)"
]
}
}
```
`X-API-Key` is the header that's needed to get past the authentication. I see it mentioned in the `Access-Control-Request-Headers` header, but the key header itself is missing.
Yeah, if I turn off the middleware but leave the custom header, it is present in the request itself but still not in the preflight.
{"level":"info","ts":1668375891.9092393,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/ping","headers":{"Accept":["*/*"],"Access-Control-Request-Headers":["x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002833663,"size":2,"status":200,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Server":["Caddy"]}}
{"level":"info","ts":1668375891.9207191,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/ping","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"If-None-Match":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Accept":["application/json, text/plain, */*"],"X-Api-Key":["API_KEY"]}},"user_id":"","duration":0.002336694,"size":0,"status":304,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Server":["Caddy"],"Access-Control-Allow-Credentials":["true"],"Etag":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"]}}
@zackyancey commented on GitHub (Nov 13, 2022):
Yeah, if I turn off the middleware but leave the custom header, it is present in the request itself but still not in the preflight.
```
{"level":"info","ts":1668375891.9092393,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/ping","headers":{"Accept":["*/*"],"Access-Control-Request-Headers":["x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002833663,"size":2,"status":200,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Server":["Caddy"]}}
{"level":"info","ts":1668375891.9207191,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"...","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/ping","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"If-None-Match":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Accept":["application/json, text/plain, */*"],"X-Api-Key":["API_KEY"]}},"user_id":"","duration":0.002336694,"size":0,"status":304,"resp_headers":{"Access-Control-Allow-Origin":["*"],"Server":["Caddy"],"Access-Control-Allow-Credentials":["true"],"Etag":["W/\"10-oV4hJxRVSENxc/wX8+mA4/Pe4tA\""],"Date":["Sun, 13 Nov 2022 21:44:51 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"]}}
```
I did some reading up and it seems like the auth middleware should not be requiring those headers in preflight requests.
The preflight request gets sent when there is a header in the request that isn't CORS safelisted. In your case that header is not safelisted so the preflight request gets sent out first to see if the request should be sent.
In that case it makes sense that the preflight request wouldn't include those headers since the preflight request is being sent to check if they are accepted.
The solutions to this preflight header issue I'm seeing are to fix the server to properly handle preflight requests.
@advplyr commented on GitHub (Nov 13, 2022):
I did some reading up and it seems like the auth middleware should not be requiring those headers in preflight requests.
The preflight request gets sent when there is a header in the request that isn't [CORS safelisted](https://fetch.spec.whatwg.org/#cors-safelisted-request-header). In your case that header is not safelisted so the preflight request gets sent out first to see if the request should be sent.
In that case it makes sense that the preflight request wouldn't include those headers since the preflight request is being sent to check if they are accepted.
The solutions to this preflight header issue I'm seeing are to fix the server to properly handle preflight requests.
Reading through this the server should probably not be redirecting a preflight request at all. Apparently it is now supported but not all browsers have made that change (not sure when that was written).
They do provide a possible solution which is to send a simple request first (request not requiring preflight) then get the redirect URL.
Make another request (the real request) using the URL you obtained from Response.url or XMLHttpRequest.responseURL in the first step.
Update: actually I don't think that would be a solution for us since we need to continue sending those headers with all requests which would then get rejected by the server preflight everytime.
@advplyr commented on GitHub (Nov 13, 2022):
Reading through [this](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests) the server should probably not be redirecting a preflight request at all. Apparently it is now supported but not all browsers have made that change (not sure when that was written).
They do provide a possible solution which is to send a simple request first (request not requiring preflight) then get the redirect URL.
> If that's not possible, then another way is to:
>
> 1. Make a [simple request](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) (using [Response.url](https://developer.mozilla.org/en-US/docs/Web/API/Response/url) for the Fetch API, or [XMLHttpRequest.responseURL](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseURL)) to determine what URL the real preflighted request would end up at.
> 3. Make another request (the real request) using the URL you obtained from Response.url or XMLHttpRequest.responseURL in the first step.
Update: actually I don't think that would be a solution for us since we need to continue sending those headers with all requests which would then get rejected by the server preflight everytime.
Hm, I wonder if that's something I can fix in my server configuration.
On the app side, I did some more testing with the auth disabled and I don't think that it's sending the token for image requests:
{"level":"info","ts":1668380662.7924142,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_yu52740ihsp4drvw61/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Authorization":[],"X-Api-Key":["..."],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Language":["en-US,en;q=0.9"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Origin":["http://localhost"]}},"user_id":"","duration":0.022020836,"size":29757,"status":200,"resp_headers":{"X-Powered-By":["Express"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["29757"],"Date":["Sun, 13 Nov 2022 23:04:22 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Etag":["W/\"743d-PyVlnv+/nY4QEAXE7pwQmFD6V/E\""]}}
{"level":"info","ts":1668380738.3990152,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"Access-Control-Request-Headers":["authorization,x-api-key"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept":["*/*"],"Access-Control-Request-Method":["GET"]}},"user_id":"","duration":0.000878268,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4180558,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Api-Key":["..."],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"Origin":["http://localhost"],"Referer":["http://localhost/"]}},"user_id":"","duration":0.001421874,"size":576,"status":200,"resp_headers":{"Etag":["W/\"240-KzyrH3qKuqhGQKwddXPtonvTIC0\""],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["576"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4455457,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Accept":["*/*"],"Access-Control-Request-Headers":["authorization,x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"]}},"user_id":"","duration":0.00140514,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Headers":["*"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Content-Length":["2"],"X-Powered-By":["Express"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Origin":["*"]}}
{"level":"info","ts":1668380738.453754,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Connection":["keep-alive"],"X-Api-Key":["..."],"Origin":["http://localhost"],"Referer":["http://localhost/"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002885038,"size":1086,"status":200,"resp_headers":{"Access-Control-Allow-Headers":["*"],"Content-Type":["application/json; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Length":["1086"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"],"Etag":["W/\"43e-U/Os9cVnBKrVPxqyec91eukucCE\""]}}
{"level":"info","ts":1668380738.4691706,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40886","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwidXNlcm5hbWUiOiJyb290IiwiaWF0IjoxNjYwNDExOTYwfQ.-MgjHunuacvRMgukmb3Kc5s7ZVI0enTybpsIodCsG1w&ts=1661008790276","headers":{"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Referer":["http://localhost/"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"]}},"user_id":"","duration":0.003245206,"size":16742,"status":200,"resp_headers":{"Access-Control-Allow-Credentials":["true"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Server":["Caddy"],"Content-Type":["image/webp"],"Access-Control-Allow-Headers":["*"]}}
The first few lines have the token for the GETs and not on the OPTIONSs as we'd expect, but the last line is getting a cover and doesn't have the token.
@zackyancey commented on GitHub (Nov 14, 2022):
Hm, I wonder if that's something I can fix in my server configuration.
On the app side, I did some more testing with the auth disabled and I don't think that it's sending the token for image requests:
```
{"level":"info","ts":1668380662.7924142,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_yu52740ihsp4drvw61/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Authorization":[],"X-Api-Key":["..."],"X-Requested-With":["com.audiobookshelf.app.debug"],"Referer":["http://localhost/"],"Accept-Language":["en-US,en;q=0.9"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Origin":["http://localhost"]}},"user_id":"","duration":0.022020836,"size":29757,"status":200,"resp_headers":{"X-Powered-By":["Express"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["29757"],"Date":["Sun, 13 Nov 2022 23:04:22 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Etag":["W/\"743d-PyVlnv+/nY4QEAXE7pwQmFD6V/E\""]}}
{"level":"info","ts":1668380738.3990152,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"Access-Control-Request-Headers":["authorization,x-api-key"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept":["*/*"],"Access-Control-Request-Method":["GET"]}},"user_id":"","duration":0.000878268,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Headers":["*"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"Content-Length":["2"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4180558,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx?include=filterdata","headers":{"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Api-Key":["..."],"Connection":["keep-alive"],"Accept":["application/json, text/plain, */*"],"Origin":["http://localhost"],"Referer":["http://localhost/"]}},"user_id":"","duration":0.001421874,"size":576,"status":200,"resp_headers":{"Etag":["W/\"240-KzyrH3qKuqhGQKwddXPtonvTIC0\""],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Type":["application/json; charset=utf-8"],"Content-Length":["576"],"Access-Control-Allow-Headers":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"]}}
{"level":"info","ts":1668380738.4455457,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"OPTIONS","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Referer":["http://localhost/"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Accept":["*/*"],"Access-Control-Request-Headers":["authorization,x-api-key"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Sec-Fetch-Mode":["cors"],"Access-Control-Request-Method":["GET"],"Origin":["http://localhost"],"X-Requested-With":["com.audiobookshelf.app.debug"]}},"user_id":"","duration":0.00140514,"size":2,"status":200,"resp_headers":{"Server":["Caddy"],"Access-Control-Allow-Headers":["*"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Access-Control-Allow-Credentials":["true"],"Content-Type":["text/plain; charset=utf-8"],"Content-Length":["2"],"X-Powered-By":["Express"],"Etag":["W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""],"Access-Control-Allow-Origin":["*"]}}
{"level":"info","ts":1668380738.453754,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40820","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/libraries/lib_t7e5b16g1unxr7xwmx/items?sort=addedAt&desc=1&limit=20&page=0&minified=1","headers":{"Connection":["keep-alive"],"X-Api-Key":["..."],"Origin":["http://localhost"],"Referer":["http://localhost/"],"Accept":["application/json, text/plain, */*"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"],"Authorization":[],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"]}},"user_id":"","duration":0.002885038,"size":1086,"status":200,"resp_headers":{"Access-Control-Allow-Headers":["*"],"Content-Type":["application/json; charset=utf-8"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Content-Length":["1086"],"Server":["Caddy"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Credentials":["true"],"X-Powered-By":["Express"],"Etag":["W/\"43e-U/Os9cVnBKrVPxqyec91eukucCE\""]}}
{"level":"info","ts":1668380738.4691706,"logger":"http.log.access.log2","msg":"handled request","request":{"remote_ip":"...","remote_port":"40886","proto":"HTTP/1.1","method":"GET","host":"audiobookshelf.example.com","uri":"/api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwidXNlcm5hbWUiOiJyb290IiwiaWF0IjoxNjYwNDExOTYwfQ.-MgjHunuacvRMgukmb3Kc5s7ZVI0enTybpsIodCsG1w&ts=1661008790276","headers":{"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Referer":["http://localhost/"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"X-Requested-With":["com.audiobookshelf.app.debug"],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Linux; Android 13; sdk_gphone64_x86_64 Build/TPB4.220624.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.74 Mobile Safari/537.36"]}},"user_id":"","duration":0.003245206,"size":16742,"status":200,"resp_headers":{"Access-Control-Allow-Credentials":["true"],"Date":["Sun, 13 Nov 2022 23:05:38 GMT"],"X-Powered-By":["Express"],"Access-Control-Allow-Origin":["*"],"Access-Control-Allow-Methods":["GET, POST, PATCH, PUT, DELETE, OPTIONS"],"Server":["Caddy"],"Content-Type":["image/webp"],"Access-Control-Allow-Headers":["*"]}}
```
The first few lines have the token for the GETs and not on the OPTIONSs as we'd expect, but the last line is getting a cover and doesn't have the token.
Yeah for images and other requests that are happening in HTML tags the token is put in as a query string /api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
@advplyr commented on GitHub (Nov 14, 2022):
Yeah for images and other requests that are happening in HTML tags the token is put in as a query string `/api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`
That is actually what my original question about cookies was about. There are some requests we want to make from an HTML img or audio tag that we won't be able to easily attach headers to. We discussed this above https://github.com/advplyr/audiobookshelf-app/issues/254#issuecomment-1166317071
Ah, I see, so I'll need to get the preflight redirect issues worked out on my middleware before I can test that then.
@zackyancey commented on GitHub (Nov 14, 2022):
Ah, I see, so I'll need to get the preflight redirect issues worked out on my middleware before I can test that then.
Ok, so I can turn off the authentication for OPTIONS requests, and now I can log in. Covers and streams don't work though. I would guess that the middleware might not be providing a cookie when authenticating with an API key—it's meant for API access after all, not browsing. You probably only get the cookie if you go through the login page.
In my case this could probably be solved with the method @hskrtich mentioned of bringing up a sign in page to authenticate and get the cookie (don't JWTs usually come with an expiration date though? Would you have to sign in periodically in that case?). I don't think it would work in all cases—another simple way you can expose a self-hosted service is with HTTPS+basic auth. With that setup I don't think there'd be any way other than getting the headers into the image/stream requests.
@zackyancey commented on GitHub (Nov 14, 2022):
Ok, so I can turn off the authentication for OPTIONS requests, and now I can log in. Covers and streams don't work though. I would guess that the middleware might not be providing a cookie when authenticating with an API key—it's meant for API access after all, not browsing. You probably only get the cookie if you go through the login page.
In my case this could probably be solved with the method @hskrtich mentioned of bringing up a sign in page to authenticate and get the cookie (don't JWTs usually come with an expiration date though? Would you have to sign in periodically in that case?). I don't think it would work in all cases—another simple way you can expose a self-hosted service is with HTTPS+basic auth. With that setup I don't think there'd be any way other than getting the headers into the image/stream requests.
I'm not sure that cookies are going to work. Maybe it will work with the requests coming out of webview but requests are also sent natively in the Kotlin/Swift code.
Images and streams are all going through the same endpoints so a temporary solution could be to whitelist those endpoints.
@advplyr commented on GitHub (Nov 14, 2022):
I'm not sure that cookies are going to work. Maybe it will work with the requests coming out of webview but requests are also sent natively in the Kotlin/Swift code.
Images and streams are all going through the same endpoints so a temporary solution could be to whitelist those endpoints.
@hskrtich commented on GitHub (Nov 14, 2022):
Web session will need to be saved (which include cookies) and used for both the native requests and webviews. Something like this
https://stackoverflow.com/questions/11224454/android-share-session-between-webview-and-httpclient
Is this issue still being worked on? Safe remote access would be great to have if you want to access your library on the go.
@Gathaeryx commented on GitHub (Jan 29, 2024):
Is this issue still being worked on? Safe remote access would be great to have if you want to access your library on the go.
i wanted to check in on this issue, as I am attempting to move my ABS server from its current setup behind HA Proxy to being setup with access through Cloudflare tunnel. The Website/wepapp to my endpoint works and I can login, but the mobile app fails because of a redirect.
If there is any info i can add to help with this issue please let me know
@rweatherly commented on GitHub (Mar 17, 2024):
i wanted to check in on this issue, as I am attempting to move my ABS server from its current setup behind HA Proxy to being setup with access through Cloudflare tunnel. The Website/wepapp to my endpoint works and I can login, but the mobile app fails because of a redirect.
If there is any info i can add to help with this issue please let me know
This issue affects my ABS deployment as well; it can only be accessed through Cloudflare tunnel. I cannot connect through the app.
@edutchie commented on GitHub (Jun 6, 2024):
This issue affects my ABS deployment as well; it can only be accessed through Cloudflare tunnel. I cannot connect through the app.
@thiemo commented on GitHub (Jun 10, 2024):
A possible workaround is to use the [WARP client to bypass authentication](https://community.cloudflare.com/t/warp-as-an-access-authentication-method/600912). Works for me on iOS.
@sevenlayercookie commented on GitHub (Jul 14, 2024):
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. Here is the commit adding custom headers:
@sevenlayercookie commented on GitHub (Jul 14, 2024):
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. [Here is the commit ](https://github.com/rasmuslos/ShelfPlayer/commit/9dbefecd6c6794a1df25f68db18d549d609e0282)adding custom headers:
@mattheys commented on GitHub (Oct 15, 2024):
> To test this you would need to uncomment out this code block https://github.com/advplyr/audiobookshelf-app/blob/master/components/connection/ServerConnectForm.vue#L25 and build the app from source. That is just to show the button that allows you to add custom headers.
Is this still the case? That link seems to go to something that isn't a comment anymore. Also I tried to set this to true but I can't see the options to add custom headers. https://github.com/advplyr/audiobookshelf-app/blob/256be3521d563e50bb05038da42619b9e613215c/components/connection/ServerConnectForm.vue#L105
Is this still the case? That link seems to go to something that isn't a comment anymore. Also I tried to set this to true but I can't see the options to add custom headers.
It appears that comment and button are no longer in the current version of the file, so I'm guessing it is no longer a fix.
@BobSleeuw commented on GitHub (Nov 4, 2024):
> > To test this you would need to uncomment out this code block https://github.com/advplyr/audiobookshelf-app/blob/master/components/connection/ServerConnectForm.vue#L25 and build the app from source. That is just to show the button that allows you to add custom headers.
>
> Is this still the case? That link seems to go to something that isn't a comment anymore. Also I tried to set this to true but I can't see the options to add custom headers.
>
> https://github.com/advplyr/audiobookshelf-app/blob/256be3521d563e50bb05038da42619b9e613215c/components/connection/ServerConnectForm.vue#L105
I don't know what to do with the information myself, but you can view the version of the file at that point in time here: https://github.com/advplyr/audiobookshelf-app/blob/a852114e9c7eccb8fb640be8164e8034c8ed88a2/components/connection/ServerConnectForm.vue#L26
It appears that comment and button are no longer in the current version of the file, so I'm guessing it is no longer a fix.
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. Here is the commit adding custom headers:
Are there any instructions anywhere for configuring this? I'm in a similar situation
@Gibby503 commented on GitHub (Apr 24, 2025):
> Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
>
>
>
> Plappa source code isn't published currently, but ShelfPlayer is. [Here is the commit ](https://github.com/rasmuslos/ShelfPlayer/commit/9dbefecd6c6794a1df25f68db18d549d609e0282)adding custom headers:
Are there any instructions anywhere for configuring this? I'm in a similar situation
@sevenlayercookie commented on GitHub (Apr 24, 2025):
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. Here is the commit adding custom headers:
Are there any instructions anywhere for configuring this? I'm in a similar situation
For now, on iOS, you'll have to use Plappa or Shelfplayer. Not sure what the Android options are.
Custom Headers Instructions (you can likely skip the preflight stuff and step 3, depending on how your Cloudflare is set up)
@sevenlayercookie commented on GitHub (Apr 24, 2025):
> > Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
> >
> >
> >
> > Plappa source code isn't published currently, but ShelfPlayer is. [Here is the commit ](https://github.com/rasmuslos/ShelfPlayer/commit/9dbefecd6c6794a1df25f68db18d549d609e0282)adding custom headers:
>
> Are there any instructions anywhere for configuring this? I'm in a similar situation
For now, on iOS, you'll have to use [Plappa](https://apps.apple.com/us/app/plappa/id6475201956) or [Shelfplayer](https://apps.apple.com/us/app/shelfplayer/id6475221163). Not sure what the Android options are.
[Custom Headers Instructions](https://chatgpt.com/share/680aa497-ea00-800d-af81-4201560bc826) (you can likely skip the preflight stuff and step 3, depending on how your Cloudflare is set up)
There's also Lissen which supports both Android and iOS ABS connections with custom headers. The android source code is available here: https://github.com/GrakovNe/lissen-android
I have a Caddy proxy set up and it works perfectly with Lissen and ABS.
@ppodgorski1 commented on GitHub (May 2, 2025):
There's also Lissen which supports both Android and iOS ABS connections with custom headers. The android source code is available here: https://github.com/GrakovNe/lissen-android
I have a Caddy proxy set up and it works perfectly with Lissen and ABS.
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. Here is the commit adding custom headers:
Are there any instructions anywhere for configuring this? I'm in a similar situation
For now, on iOS, you'll have to use Plappa or Shelfplayer. Not sure what the Android options are.
Custom Headers Instructions (you can likely skip the preflight stuff and step 3, depending on how your Cloudflare is set up)
I have everything pretty much configured per these instructions but still having issues getting it to work. Customer headers entered in plappa and it still fails
@Gibby503 commented on GitHub (May 14, 2025):
> > > Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
> > > Plappa source code isn't published currently, but ShelfPlayer is. [Here is the commit ](https://github.com/rasmuslos/ShelfPlayer/commit/9dbefecd6c6794a1df25f68db18d549d609e0282)adding custom headers:
> >
> >
> > Are there any instructions anywhere for configuring this? I'm in a similar situation
>
> For now, on iOS, you'll have to use [Plappa](https://apps.apple.com/us/app/plappa/id6475201956) or [Shelfplayer](https://apps.apple.com/us/app/shelfplayer/id6475221163). Not sure what the Android options are.
>
> [Custom Headers Instructions](https://chatgpt.com/share/680aa497-ea00-800d-af81-4201560bc826) (you can likely skip the preflight stuff and step 3, depending on how your Cloudflare is set up)
I have everything pretty much configured per these instructions but still having issues getting it to work. Customer headers entered in plappa and it still fails
Just adding on that I'm another person that would really appreciate this feature, I can't really use the apps until it's supported.
@Rathlord commented on GitHub (Jul 30, 2025):
Just adding on that I'm another person that would really appreciate this feature, I can't really use the apps until it's supported.
Is there any way to know if this is on the roadmap for development? It seems this issue is stagnant
Code contributions are welcome. Be aware it can take a while for things to be reviewed or merged due to limited development time when spread across the server, web client, and two apps. The focus has been on the server side updates in recent months (notably the data model, API, and front end).
@nichwall commented on GitHub (Aug 4, 2025):
> Is there any way to know if this is on the roadmap for development? It seems this issue is stagnant
Code contributions are welcome. Be aware it can take a while for things to be reviewed or merged due to limited development time when spread across the server, web client, and two apps. The focus has been on the server side updates in recent months (notably the data model, API, and front end).
Another +1 for this feature 🙏 @nichwall I understand resources are limited and that there's a lot to cover. Any way I can sponsor this project and hopefully help (patreon, ko-fi or similar)? 👀 Thanks for such an amazing OSS project ❤️
@dcaixinha commented on GitHub (Oct 1, 2025):
Another +1 for this feature 🙏 @nichwall I understand resources are limited and that there's a lot to cover. Any way I can sponsor this project and hopefully help (patreon, ko-fi or similar)? 👀 Thanks for such an amazing OSS project ❤️
If anyone who contributes to the project reads this, could you reach out with a quick summary of the current state of any such work done + what's blocking it? I will literally implement the feature, just need a super basic/quick overview of how you currently connect to the ABS server is pretty much all I need.
@tspader commented on GitHub (Oct 4, 2025):
If anyone who contributes to the project reads this, could you reach out with a quick summary of the current state of any such work done + what's blocking it? I will literally implement the feature, just need a super basic/quick overview of how you currently connect to the ABS server is pretty much all I need.
In case it helps @tspader or anyone else picking this up, the open-source Immich app is another great example that implements this feature well (for scenarios like accessing the server via a Cloudflare Tunnel).
Header Injection Logic: This function reads the saved headers and merges them with the access token before every request. mobile/lib/services/api.service.dart
Sharing for inspiration while showing respect to Immich.
@elp3dr0 commented on GitHub (Oct 8, 2025):
In case it helps @tspader or anyone else picking this up, the open-source Immich app is another great example that implements this feature well (for scenarios like accessing the server via a Cloudflare Tunnel).
**Header Configuration/Storage (UI):** This is where users set and save the headers.
[mobile/lib/widgets/settings/custom_proxy_headers_settings](https://github.com/immich-app/immich/blob/53680d9643f7de5c18e2fd5195b4b6706cf6066f/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart#L4).
**Header Injection Logic:** This function reads the saved headers and merges them with the access token before every request.
[mobile/lib/services/api.service.dart](https://github.com/immich-app/immich/blob/53680d9643f7de5c18e2fd5195b4b6706cf6066f/mobile/lib/services/api.service.dart#L188)
For a practical example of this feature in action, here is a video showing how a user configures the Immich app with headers to bypass Cloudflare Zero Trust: [Can't get past ClouldFlare Zero Trust on Mobile? Do THIS!](http://www.youtube.com/watch?v=J4vVYFVWu5Q).
Sharing for inspiration while showing respect to Immich.
I agree we would greatly benefit from the support of custom headers for Cloudflare tunnel
i am using NZB360 and it does support custom headers too
@adelatour11 commented on GitHub (Dec 25, 2025):
I agree we would greatly benefit from the support of custom headers for Cloudflare tunnel
i am using NZB360 and it does support custom headers too
I had an alternative question. Does the app only use specific endpoints like /api?
In that case a solution (for some setups) would be to allow access to /api without auth (since the requests are authenticated with an api key anyways).
@xoxfaby commented on GitHub (Jan 8, 2026):
I had an alternative question. Does the app only use specific endpoints like /api?
In that case a solution (for some setups) would be to allow access to /api without auth (since the requests are authenticated with an api key anyways).
@renegadepixels commented on GitHub (Jan 13, 2026):
Just want to add another +1 for this feature, I do not want to switch to third party apps just to get a secure connection to my server externally, I would be super appreciative if this was implemented. Thanks for all the hard work!
@renegadepixels commented on GitHub (Jan 13, 2026):
Just want to add another +1 for this feature, I do not want to switch to third party apps just to get a secure connection to my server externally, I would be super appreciative if this was implemented. Thanks for all the hard work!
@MichaelKopt commented on GitHub (Feb 22, 2026):
The merge request with the implementation has been there for almost two months. - https://github.com/advplyr/audiobookshelf-app/pull/1768. Could someone please proceed with this matter?
For anyone using the iOS app and wanting a workaround while still hiding the app behind Cloudflare zero trust, here's a good tutorial from the HomeAssistant community. It basically runs an automation to add your new IP to a trusted IP list every time you disconnect from wifi (or manually whenever you want access). It's a bit hacky, but it works. It also includes a cleanup script so that you don't have a bunch of old IPs lingering on the list. 10/10 would recommend until this feature gets added into the iOS app.
@nate-griff commented on GitHub (Feb 26, 2026):
Huge +1 for this feature.
For anyone using the iOS app and wanting a workaround while still hiding the app behind Cloudflare zero trust, [here's a good tutorial from the HomeAssistant community](https://community.home-assistant.io/t/automated-security-ip-based-bypass-for-cloudflare-zero-trust-access-to-self-hosted-applications/981152). It basically runs an automation to add your new IP to a trusted IP list every time you disconnect from wifi (or manually whenever you want access). It's a bit hacky, but it works. It also includes a cleanup script so that you don't have a bunch of old IPs lingering on the list. 10/10 would recommend until this feature gets added into the iOS app.
@sevenlayercookie commented on GitHub (Feb 26, 2026):
Huge +1 for this feature.
For anyone using the iOS app and wanting a workaround while still hiding the app behind Cloudflare zero trust, here's a good tutorial from the HomeAssistant community. It basically runs an automation to add your new IP to a trusted IP list every time you disconnect from wifi (or manually whenever you want access). It's a bit hacky, but it works. It also includes a cleanup script so that you don't have a bunch of old IPs lingering on the list. 10/10 would recommend until this feature gets added into the iOS app.
That's a good idea. Makes me think that could be a great use for a Cloudflare worker. I might try to set that up.
@sevenlayercookie commented on GitHub (Feb 26, 2026):
> Huge +1 for this feature.
>
> For anyone using the iOS app and wanting a workaround while still hiding the app behind Cloudflare zero trust, [here's a good tutorial from the HomeAssistant community](https://community.home-assistant.io/t/automated-security-ip-based-bypass-for-cloudflare-zero-trust-access-to-self-hosted-applications/981152). It basically runs an automation to add your new IP to a trusted IP list every time you disconnect from wifi (or manually whenever you want access). It's a bit hacky, but it works. It also includes a cleanup script so that you don't have a bunch of old IPs lingering on the list. 10/10 would recommend until this feature gets added into the iOS app.
That's a good idea. Makes me think that could be a great use for a Cloudflare worker. I might try to set that up.
That's a good idea. Makes me think that could be a great use for a Cloudflare worker. I might try to set that up.
Ooh good idea! Please do share if you get it working. I've yet to dabble with workers but this seems like a good excuse to do so
@nate-griff commented on GitHub (Feb 26, 2026):
> That's a good idea. Makes me think that could be a great use for a Cloudflare worker. I might try to set that up.
Ooh good idea! Please do share if you get it working. I've yet to dabble with workers but this seems like a good excuse to do so
I ended up renting a $3 per month tiny VPS and setting it up as a proxy to my home ABS using Caddy. Connected it to my home via Tailscale (free) and limited it to the ABS docker port using tailscale IP rules. I put the proxy behind cloudflare (free) and severely restricted IP traffic to my region. Also expose other docker containers through the same proxy like Immich.
@ppodgorski1 commented on GitHub (Feb 27, 2026):
I ended up renting a $3 per month tiny VPS and setting it up as a proxy to my home ABS using Caddy. Connected it to my home via Tailscale (free) and limited it to the ABS docker port using tailscale IP rules. I put the proxy behind cloudflare (free) and severely restricted IP traffic to my region. Also expose other docker containers through the same proxy like Immich.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Originally created by @rmkjr on GitHub (Jun 24, 2022).
Is your feature request related to a problem? Please describe.
Zero Trust like app access, in particular Cloudflare Tunnels, are becoming quite popular in the self-hosted world. They allow you to make your self-hosted applications available externally without opening ports. However, they typically use a OpenID Connect or SAML style SSO approach to authenticate access which means an intermediate login page appears before connectivity to the application endpoint is then allowed. As a workaround to client apps that cannot support this additional authentication step, Cloudflare supports the use of service tokens passed with requests as additional headers.
https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
Describe the solution you'd like
Support adding additional custom headers, custom keys and values, when setting up the server connection to allow us to set additional headers for requests made to the server. A great app citizen that has done this quite well is LunaSea.
https://docs.lunasea.app/modules/sonarr#custom-headers
https://imgur.com/a/JSC1bYn
Describe alternatives you've considered
The only alternative would be to forward the ports directly from the internet, or to always have a VPN or similar running when remote. Allowing custom headers to be set for the server connection would allow for very secure remote access.
Additional context
N/A
@advplyr commented on GitHub (Jun 24, 2022):
Thanks for the detailed request with an example implementation. Do you think this is something that would be implemented in the server settings and used for all clients? Or is this something on the connect screen of the mobile app where you would specify custom headers?
@rmkjr commented on GitHub (Jun 24, 2022):
Typically I would see this implemented on the connect screen of the mobile app. If you were to say have multiple unique users, you may want to give them unique headers to authenticate to the tunnel broker in addition to their normal Audiobookshelf username/password.
That should also be a bit simpler. As the mobile app, if a user provides some number of extra headers via optional fields, would just need to append them to the http requests. The server and its authentication mechanisms could just ignore them as it's only the tunnel broker that is using them.
@advplyr commented on GitHub (Jun 25, 2022):
The way adding a new server connection config works now is you first enter your server address
On submit the mobile app will ping the address using
https://abs.example.com/pingWould this /ping request require custom headers already added?
@rmkjr commented on GitHub (Jun 25, 2022):
That is my understanding yes. Every http request made to the server endpoint should append on any added custom headers.
In the case of a Cloudflare tunnel, the url entered into the app is a Cloudflare endpoint. When a request is made to the endpoint the first thing it does is check the request headers. If the service auth token headers exist, or if a valid cookie header from a past interactive authentication session exist, then Cloudflare will pass that traffic to the Audiobookshelf server uninterrupted. If no such header(s) exist, then Cloudflare will attempt to redirect that request to the configured authentication IdP for authentication. By creating a service auth token and appending it to all request, Cloudflare will see that request as already authenticated, and pass it to the underlying endpoint, in this case the Audiobookshelf server, without needing the app to handle any extra authentication pages/steps.
So to your point, you would have to give users the opportunity to add/set their custom headers at that onboarding screen so they exist and can be added to the first request made to the server, as well as every following request.
LunaSea does it as a secondary screen. Basically a button on that submit button screen to add custom headers. That button then opens an additional screen where a user can add an arbitrary number of custom key/value pairs. Then you go back to the screen with the server url, and when a user clicks submit the added keys and values will be sent as appended request headers for all future requests including the first one to /ping.
@advplyr commented on GitHub (Jun 25, 2022):
Got it. I'm not sure what the best UI/UX is for this given I did this a bit differently then most servers with the
/pingstep.We could re-design this or add a "Add Custom Headers" button to the left of the submit button which would open a modal to put in custom headers.
@rmkjr commented on GitHub (Jun 25, 2022):
The idea of an extra button to open a modal like you've described I think makes total sense.
I'm on both your TestFlight and Google Play beta, and also have Cloudflare tunnels setup for other apps, so I'll definitely be able to help you test it out.
Have to say btw, this whole thing is super cool. Have been playing around with ABS after getting my instance stood up this week and it's quite amazing! Thanks for thinking up and creating such an awesome project!
@advplyr commented on GitHub (Jun 25, 2022):
I'm about half-way into implementing this and realized that we won't be able to use headers for static content on the server like images. For apps do you typically allow bypassing for static content? Like specifying a path to allow through.
@advplyr commented on GitHub (Jun 25, 2022):
Another issue is the images for book covers and authors are going through the api routes.
Here is an example get request for a book cover image
/api/items/li_rghidt3v6x9ef35sq3/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJyb290IiwiaWF0IjoxNjUyNTYyNDM5fQThis is requested directly from an HTML image element and will cache and return the optimized image.
@rmkjr commented on GitHub (Jun 25, 2022):
I think one could technically bypass tunnel authentication for particular URL locations, but that's not one that I've run into so far with the several apps I have routed that way.
I think ideally all requests to the server endpoint, including those for well-known static content, would add the additional request headers.
I've been out of the dev game for about a decade, so going to brush up against some knowledge edges here. It looks like you have the user token passed as an "Authorization" header in most cases. I think these extra headers would be appended basically every place that ABS user token is added to requests.
I also see the http img element for covers for example where it is setting the src to the known cover url. In those cases, does this url endpoint also bypass checking the ABS's own Authorization header? That is to say the /api endpoint (or maybe just /api/items) is open and unauthenticed? If that is true, then I think you'd be correct. Either the particular location (like /api/items) would have to bypass authentication tunnel side so it can remain unauthenticed, or those request would have to be refactored and be handled by a request method that can handle additional request headers rather than the inherent thing that is used to load an img tag's src.
@advplyr commented on GitHub (Jun 25, 2022):
When authenticating for images ABS will check the query string
tokenpassed in the url.That is why in the example request above for the image you see the token passed in the URL. This is also used when streaming, your token gets passed in as a query string when direct playing an mp3 file for example.
I'm not sure how to set custom headers for requests made through HTML elements, it may not be possible. Do these other services you use that offer this support serve images from your server? I would be curious if you could check the HTTP request or if they have source code to look at.
@rmkjr commented on GitHub (Jun 25, 2022):
LunaSea, which aggregates as an app frontend for quite a few of the services I host out through these tunnels, does have similar static content loaded. For example, both Radarr and Sonarr have image covers for Movies and TV shows that the LunaSea app will pull from the respective sever and display.
I poked around its source a bit, but from the little I could interpret, it looks to be squarely in some overarching framework that may happen to have a request method that is capable of adding request headers. Guessing that means they're requesting the image content via those methods and then building their views rather than an img tag in a view loading it directly.
https://github.com/JagandeepBrar/LunaSea
@advplyr commented on GitHub (Jun 26, 2022):
Yeah that is the only way to do it. We would need to write a custom image component as a wrapper around the img tag that would fetch the images first with the proper headers then use file blob in img src. I think it is common to do this so it shouldn't impact performance.
There are also images used in the player notification and in android auto that would need to be downloaded first.
@hskrtich commented on GitHub (Jun 26, 2022):
I took a look at lunasea and how it integrate into other apps and I have a couple of thoughts.
It looks like loading static assets are not a problem since the the Cloudflare Tunnel will generate and set a JWT (cookie) on the first auth to the tunnel. So any other requests that come from the client should send the cookie along with the http request, which happens normally from most apps.
According to https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/#connect-your-service-to-access
When a request is made to an application behind our network, the request will submit them both to Access. If the service token is valid, Access generates a JWT scoped to the application. All subsequent requests with that JWT will succeed until the expiration of that JWT.As for putting this in front of ABS. Since this system is much more designed for apps that dont have there own user logins this isnt as useful as it could be since this basically puts an additional auth/login (the tunnel) in front of the app. Which works great for things like Radarr and such since they dont have any kind of user/login system. Its also a weird workflow when it comes to the clients trying to connect into the server since it would require another set of creds to be passed into the app.
@advplyr commented on GitHub (Jun 26, 2022):
If that is the case then that could really simplify things with the images and stream requests. I think in order to test that I would need to set this up.
@Presjar commented on GitHub (Jul 14, 2022):
An example what does this very well are all the 'arr' apps. Like Sonarr, Radarr, Prowlarr. and also SABnzbd. Each app has an API key (in the case of audiobookshelf, there would be an API key per user account).
I am then able to use NZB360 which passes the API key in the https headers. I use Traefik to catch this API key to allow access through the proxy and then login to the endpoint application.
If the API key is missing like when accessing not via the NZB360 app, It will use google oauth to authenticate the proxy.
`Snip from my dockerconfig
HTTP Routers Auth Bypass
HTTP Routers Auth
`
@advplyr commented on GitHub (Jul 22, 2022):
The big question still is the static assets. If anyone is able to test what @bskrtich mentioned about the cookie then the header stuff is almost implemented for all the API requests, just not the static file requests like images and audio files
@zackyancey commented on GitHub (Nov 12, 2022):
I've got my audiobookshelf instance behind a reverse proxy with some auth middleware, how would I go about testing the app?
I'm using the android 9.59-beta version, but I don't see a button for custom headers on the add server screen. Do I need a different build with the feature enabled?
@advplyr commented on GitHub (Nov 12, 2022):
To test this you would need to uncomment out this code block https://github.com/advplyr/audiobookshelf-app/blob/master/components/connection/ServerConnectForm.vue#L25
and build the app from source. That is just to show the button that allows you to add custom headers.
@hskrtich commented on GitHub (Nov 12, 2022):
Id like to try to take a stab at auto showing a web login page if the server is behind a zero trust config. This would allow users to login how ever they need to via the zero trust and see the correct session headers with out the header http header field.
@advplyr commented on GitHub (Nov 12, 2022):
I haven't gone deeply into the SSO & auth middleware world for self-hosting but it seems like the solutions being implemented are all over the place.
@Avsynthe commented on GitHub (Nov 13, 2022):
For the most part, they're implementing main open standards utilised even in enterprise settings. Most commonly nowadays they implement using SAML or OIDC but similar apps like Calibre-Web, they simply read a header and combine it with LDAP for a more crude solution
Solutions like Authentik are available now where all of that is possible as it's pretty much All-in-one so we can pair up pretty much SSO choice made in any other app
@zackyancey commented on GitHub (Nov 13, 2022):
I gave it a shot, but it wasn't able to connect to the server:
I can get to the site in a web browser, so I don't think there's an HTTPS problem. Could the pre-flight request be being sent without the right headers? The auth provider will try to redirect to the sign in page if it doesn't see the right headers, so that might be the redirect in the log. I can curl
/pingwith the right headers manually and that part of it seems to work.@advplyr commented on GitHub (Nov 13, 2022):
Would it be normal for the auth middleware to perform a redirect from a preflight request?
@zackyancey commented on GitHub (Nov 13, 2022):
I don't know what's considered standard, but the one I'm using looks like it will redirect to the sign in page if you try to access any protected URL without being signed in.
@advplyr commented on GitHub (Nov 13, 2022):
I guess one way to test would be to run the server locally and add a console.log for the req here https://github.com/advplyr/audiobookshelf/blob/master/server/Auth.js#L20
Then disabling your auth middleware to see how the request come in. Unless you can add logs to your auth middleware to see what the request looks like.
@hskrtich commented on GitHub (Nov 13, 2022):
Yes, its normal to redirect any request including a preflight/OPTIONS request since at that point any request isnt authenticated. The header will need to get set for any request sent to the server.
@advplyr commented on GitHub (Nov 13, 2022):
I'm not sure why the headers wouldn't be included in the preflight request. I would assume they are but only way to find out for sure is to log the request.
@zackyancey commented on GitHub (Nov 13, 2022):
Here's a log for the request:
X-API-Keyis the header that's needed to get past the authentication. I see it mentioned in theAccess-Control-Request-Headersheader, but the key header itself is missing.@advplyr commented on GitHub (Nov 13, 2022):
Ah I see. Were you able to verify that it is going into the regular request?
@zackyancey commented on GitHub (Nov 13, 2022):
I didn't, but I can give that a shot.
@zackyancey commented on GitHub (Nov 13, 2022):
Yeah, if I turn off the middleware but leave the custom header, it is present in the request itself but still not in the preflight.
@advplyr commented on GitHub (Nov 13, 2022):
I did some reading up and it seems like the auth middleware should not be requiring those headers in preflight requests.
The preflight request gets sent when there is a header in the request that isn't CORS safelisted. In your case that header is not safelisted so the preflight request gets sent out first to see if the request should be sent.
In that case it makes sense that the preflight request wouldn't include those headers since the preflight request is being sent to check if they are accepted.
The solutions to this preflight header issue I'm seeing are to fix the server to properly handle preflight requests.
@advplyr commented on GitHub (Nov 13, 2022):
Reading through this the server should probably not be redirecting a preflight request at all. Apparently it is now supported but not all browsers have made that change (not sure when that was written).
They do provide a possible solution which is to send a simple request first (request not requiring preflight) then get the redirect URL.
Update: actually I don't think that would be a solution for us since we need to continue sending those headers with all requests which would then get rejected by the server preflight everytime.
@zackyancey commented on GitHub (Nov 14, 2022):
Hm, I wonder if that's something I can fix in my server configuration.
On the app side, I did some more testing with the auth disabled and I don't think that it's sending the token for image requests:
The first few lines have the token for the GETs and not on the OPTIONSs as we'd expect, but the last line is getting a cover and doesn't have the token.
@advplyr commented on GitHub (Nov 14, 2022):
Yeah for images and other requests that are happening in HTML tags the token is put in as a query string
/api/items/li_1rhorfrg74e0obskg7/cover?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9That is actually what my original question about cookies was about. There are some requests we want to make from an HTML img or audio tag that we won't be able to easily attach headers to. We discussed this above https://github.com/advplyr/audiobookshelf-app/issues/254#issuecomment-1166317071
@zackyancey commented on GitHub (Nov 14, 2022):
Ah, I see, so I'll need to get the preflight redirect issues worked out on my middleware before I can test that then.
@zackyancey commented on GitHub (Nov 14, 2022):
Ok, so I can turn off the authentication for OPTIONS requests, and now I can log in. Covers and streams don't work though. I would guess that the middleware might not be providing a cookie when authenticating with an API key—it's meant for API access after all, not browsing. You probably only get the cookie if you go through the login page.
In my case this could probably be solved with the method @hskrtich mentioned of bringing up a sign in page to authenticate and get the cookie (don't JWTs usually come with an expiration date though? Would you have to sign in periodically in that case?). I don't think it would work in all cases—another simple way you can expose a self-hosted service is with HTTPS+basic auth. With that setup I don't think there'd be any way other than getting the headers into the image/stream requests.
@hskrtich commented on GitHub (Nov 14, 2022):
Yes the JWT do expire. The pop up log in window would need to be shown anytime it redirects to do auth.
@advplyr commented on GitHub (Nov 14, 2022):
I'm not sure that cookies are going to work. Maybe it will work with the requests coming out of webview but requests are also sent natively in the Kotlin/Swift code.
Images and streams are all going through the same endpoints so a temporary solution could be to whitelist those endpoints.
@hskrtich commented on GitHub (Nov 14, 2022):
Web session will need to be saved (which include cookies) and used for both the native requests and webviews. Something like this
https://stackoverflow.com/questions/11224454/android-share-session-between-webview-and-httpclient
@Keavon commented on GitHub (Apr 22, 2023):
Hello, thanks for working on this! Any updates?
@Gathaeryx commented on GitHub (Jan 29, 2024):
Is this issue still being worked on? Safe remote access would be great to have if you want to access your library on the go.
@rweatherly commented on GitHub (Mar 17, 2024):
i wanted to check in on this issue, as I am attempting to move my ABS server from its current setup behind HA Proxy to being setup with access through Cloudflare tunnel. The Website/wepapp to my endpoint works and I can login, but the mobile app fails because of a redirect.
If there is any info i can add to help with this issue please let me know
@edutchie commented on GitHub (Jun 6, 2024):
This issue affects my ABS deployment as well; it can only be accessed through Cloudflare tunnel. I cannot connect through the app.
@thiemo commented on GitHub (Jun 10, 2024):
A possible workaround is to use the WARP client to bypass authentication. Works for me on iOS.
@sevenlayercookie commented on GitHub (Jul 14, 2024):
Both Plappa and Shelfplayer support custom headers using Audiobookshelf server. I am using this to allow Plappa and ShelfPlayer to authenticate Cloudflare Access using headers. It works flawlessly. Books stream perfectly, cover images, everything. ABS mobile should be able to add this easily.
Plappa source code isn't published currently, but ShelfPlayer is. Here is the commit adding custom headers:
@mattheys commented on GitHub (Oct 15, 2024):
Is this still the case? That link seems to go to something that isn't a comment anymore. Also I tried to set this to true but I can't see the options to add custom headers. https://github.com/advplyr/audiobookshelf-app/blob/256be3521d563e50bb05038da42619b9e613215c/components/connection/ServerConnectForm.vue#L105
@BobSleeuw commented on GitHub (Nov 4, 2024):
I don't know what to do with the information myself, but you can view the version of the file at that point in time here: https://github.com/advplyr/audiobookshelf-app/blob/a852114e9c7eccb8fb640be8164e8034c8ed88a2/components/connection/ServerConnectForm.vue#L26
It appears that comment and button are no longer in the current version of the file, so I'm guessing it is no longer a fix.
@radhoo2k10 commented on GitHub (Apr 6, 2025):
Hi. Any news on this. The app Lissen got custom headers. Is it possible to use that code?
@clb92 commented on GitHub (Apr 8, 2025):
I'm also very interested in this feature. For now, I'll be using Lissen, which has this feature.
@Gibby503 commented on GitHub (Apr 24, 2025):
Are there any instructions anywhere for configuring this? I'm in a similar situation
@sevenlayercookie commented on GitHub (Apr 24, 2025):
For now, on iOS, you'll have to use Plappa or Shelfplayer. Not sure what the Android options are.
Custom Headers Instructions (you can likely skip the preflight stuff and step 3, depending on how your Cloudflare is set up)
@ppodgorski1 commented on GitHub (May 2, 2025):
There's also Lissen which supports both Android and iOS ABS connections with custom headers. The android source code is available here: https://github.com/GrakovNe/lissen-android
I have a Caddy proxy set up and it works perfectly with Lissen and ABS.
@Gibby503 commented on GitHub (May 14, 2025):
I have everything pretty much configured per these instructions but still having issues getting it to work. Customer headers entered in plappa and it still fails
@Rathlord commented on GitHub (Jul 30, 2025):
Just adding on that I'm another person that would really appreciate this feature, I can't really use the apps until it's supported.
@Gathaeryx commented on GitHub (Aug 4, 2025):
Is there any way to know if this is on the roadmap for development? It seems this issue is stagnant
@nichwall commented on GitHub (Aug 4, 2025):
Code contributions are welcome. Be aware it can take a while for things to be reviewed or merged due to limited development time when spread across the server, web client, and two apps. The focus has been on the server side updates in recent months (notably the data model, API, and front end).
@hagaram commented on GitHub (Aug 11, 2025):
I would also appreciate this feature very much
@dcaixinha commented on GitHub (Oct 1, 2025):
Another +1 for this feature 🙏 @nichwall I understand resources are limited and that there's a lot to cover. Any way I can sponsor this project and hopefully help (patreon, ko-fi or similar)? 👀 Thanks for such an amazing OSS project ❤️
@tspader commented on GitHub (Oct 4, 2025):
If anyone who contributes to the project reads this, could you reach out with a quick summary of the current state of any such work done + what's blocking it? I will literally implement the feature, just need a super basic/quick overview of how you currently connect to the ABS server is pretty much all I need.
@elp3dr0 commented on GitHub (Oct 8, 2025):
In case it helps @tspader or anyone else picking this up, the open-source Immich app is another great example that implements this feature well (for scenarios like accessing the server via a Cloudflare Tunnel).
Header Configuration/Storage (UI): This is where users set and save the headers.
mobile/lib/widgets/settings/custom_proxy_headers_settings.
Header Injection Logic: This function reads the saved headers and merges them with the access token before every request.
mobile/lib/services/api.service.dart
For a practical example of this feature in action, here is a video showing how a user configures the Immich app with headers to bypass Cloudflare Zero Trust: Can't get past ClouldFlare Zero Trust on Mobile? Do THIS!.
Sharing for inspiration while showing respect to Immich.
@adelatour11 commented on GitHub (Dec 25, 2025):
I agree we would greatly benefit from the support of custom headers for Cloudflare tunnel
i am using NZB360 and it does support custom headers too
@xoxfaby commented on GitHub (Jan 8, 2026):
I had an alternative question. Does the app only use specific endpoints like /api?
In that case a solution (for some setups) would be to allow access to /api without auth (since the requests are authenticated with an api key anyways).
@renegadepixels commented on GitHub (Jan 13, 2026):
Just want to add another +1 for this feature, I do not want to switch to third party apps just to get a secure connection to my server externally, I would be super appreciative if this was implemented. Thanks for all the hard work!
@MichaelKopt commented on GitHub (Feb 22, 2026):
The merge request with the implementation has been there for almost two months. - https://github.com/advplyr/audiobookshelf-app/pull/1768. Could someone please proceed with this matter?
@nate-griff commented on GitHub (Feb 26, 2026):
Huge +1 for this feature.
For anyone using the iOS app and wanting a workaround while still hiding the app behind Cloudflare zero trust, here's a good tutorial from the HomeAssistant community. It basically runs an automation to add your new IP to a trusted IP list every time you disconnect from wifi (or manually whenever you want access). It's a bit hacky, but it works. It also includes a cleanup script so that you don't have a bunch of old IPs lingering on the list. 10/10 would recommend until this feature gets added into the iOS app.
@MichaelKopt commented on GitHub (Feb 26, 2026):
On iOS, I use AudioBooth. It's free and works well with a custom headers.
@sevenlayercookie commented on GitHub (Feb 26, 2026):
That's a good idea. Makes me think that could be a great use for a Cloudflare worker. I might try to set that up.
@nate-griff commented on GitHub (Feb 26, 2026):
Ooh good idea! Please do share if you get it working. I've yet to dabble with workers but this seems like a good excuse to do so
@GabeAntics commented on GitHub (Feb 26, 2026):
I would love to add custom headers as well
@ppodgorski1 commented on GitHub (Feb 27, 2026):
I ended up renting a $3 per month tiny VPS and setting it up as a proxy to my home ABS using Caddy. Connected it to my home via Tailscale (free) and limited it to the ABS docker port using tailscale IP rules. I put the proxy behind cloudflare (free) and severely restricted IP traffic to my region. Also expose other docker containers through the same proxy like Immich.