BookStackApp / BookStack

A platform to create documentation/wiki content built with PHP & Laravel
https://www.bookstackapp.com/
MIT License
15.05k stars 1.88k forks source link

Authenticate via header, e.g. Auth Proxy #2223

Closed ryanc-me closed 2 years ago

ryanc-me commented 4 years ago

Support for authentication via header would be a great low-cost addition to Bookstack. With this authentication mode, Bookstack would check for the request header X-Webauth-User, and attempt to log-in as that user automatically with no password/token. This mode leverages an external service, the auth proxy, to authenticate users and add the header. Sessions are stored externally also; the user remains logged-in while the header is present.

Louketo is a good example auth proxy that supports OIDC providers, like Keycloak, Dex, etc.

Grafana has an authentication module that works in this way.

To be clear, I am planning to make a pull request to add these features. It would be great to get some feedback on the plan first though!

How it works

The login flow looks like:

  1. User visits bookstack.example.com
  2. The request comes through to an auth proxy, not Bookstack. For example:
    • Nginx configured with Basic Auth, or
    • Louketo, connected to an external OIDC IdP like Keycloak.
  3. The auth proxy authenticates the user (via a login page, an SSO token, etc)
  4. The auth proxy adds/populates the X-Webauth-User header, and proxies the request to Bookstack.
  5. Bookstack sees the header, trusts the auth proxy, and logs the user in automatically.
  6. User is redirected to the application, skipping the login page.

A variety of auth proxies can be used to authenticate and set the header, for example: Basic Auth, vouch proxy, oauth2-proxy.

Benefits

Considerations

LDAP: It would be very useful if LDAP Group Sync could continue working. When a user visits Bookstack, they should be authenticated via auth proxy, but their user info (and groups) could still come from LDAP sync.

Documentation: It is very important that users are not able to forge the X-Webauth-User header as it allows passwordless login to any account. It is also important that users (either via web, or CLI on the server hosting Bookstack) are not able to bypass the auth proxy. There should be a dedicated section in the docs about protecting that header in Nginx, Apache, etc.

Logout: Logging-out can be handled in many different ways, depending on the auth proxy. The Logout button in the Bookstack UI should be configurable to provide maximum flexibility.

Configuration: Some extra configuration options will be needed:

ryanc-me commented 4 years ago

Also, some other relevant issues: #2180, #1500, #607, #1157 (kinda), #1389 (kinda), #1390 (kinda).

tiredofit commented 4 years ago

I'd like to add to this that if the X-Server Header is in existence that it automatically logs a user in. I had a good use case in a previous issue as such:

ryanc-me commented 4 years ago

I'm not familiar with the X-Server header - could you describe that case a little more?

The case where a public user visits a public shelf/book/page is interesting though. Allowing non-authenticated access would rely on support from the auth proxy side. For example, Louketo allows for basic authorization rules (basically just allowing/disallowing certain URLs), so it would be possible to white-list certain URLs so they could be viewed publicly. That would need to be configured in Louketo though, which might be a pain for server admins. I'm also not sure how that would work for oauth2-proxy, vouch-proxy, etc.

Either way, the no-login-page SSO experience will definitely be possible with this setup. 😃

tiredofit commented 4 years ago

Sorry - I was shorthand writing. I meant to follow your example of X-Webauth-User for the header question.

What you have explained with the Louketo example works similar ways with the authentication solution I am most familiar with, LemonLDAP:NG. You can do something very similar by defining paths via regex that can either bypass authentication, request a specific level of authentication (ie 2FA), and others.

I'm excited. Thanks for taking this on.

ryanc-me commented 4 years ago

Right, I see now! Sorry about the confusion there.

That's great to hear. I'll have to look into LemonLDAP:NG, perhaps it would be a good idea to do some testing with it, along with Louketo, oauth2-proxy, etc.

Also, do you have any experience with PHP? I'm about to make a fork to develop an implementation, so I'll send you a contributor invite.

tiredofit commented 4 years ago

My PHP skills are laughable however I do know systems architecture and can help out with the LLNG testing and also other test scenarios. I do however have a team of PHP developers that I could bring one on for this project. Let me bring him into the thread.

injektion commented 4 years ago

Hey @ryanc-me - waved in by @tiredofit ... Been looking at adding a SSO/no login page tied into LDAP. Not too familiar with auth proxies (yet), but can bring some PHP/Laravel to the table. Happy to help if ya wanna throw a contrib invite this way...

ryanc-me commented 4 years ago

Hey @injektion, thanks for tagging in! I see you accepted the collaborator invite :+1:

I'm a little busy this weekend, but will be working on this early next week. The auth proxy functionality is actually really simple to implement, but it would be awesome if you could check/test the code before I make a PR. It's been a while, and my PHP is a little rusty!

ryanc-me commented 4 years ago

@injektion could you take at the commit here? Basic functionality has been fleshed out (as best I can, with my limited Laravel knowledge!), and I'd love to hear what you think.

So far, we have:

Most of this was based on the API auth flow, as it's quite similar to the auth proxy flow. It would also be great to hear what @ssddanbrown thinks.

injektion commented 4 years ago

Hey @ryanc-me --

Been playing around with the the commit.

To test it I added (in .env)

AUTH_METHOD=auth-proxy

Which seems to be working great.

Doesn't seem to work by default tho - throwing the X-Webauth-User header doesn't seem to be kicking off the guard in app/Http/Controllers/Auth/LoginController.php

$this->middleware('guard:standard,ldap,auth-proxy', ['only' => ['login', 'logout']]);

That doesn't seem to actually use the auth-proxy guard, even if stripped down to

$this->middleware('guard:auth-proxy', ['only' => ['login', 'logout']]);

or

$this->middleware('auth:auth-proxy', ['only' => ['login', 'logout']]);

Banged my head on it a bunch trying to get it to use the right auth/guard but not getting any love on it...

ryanc-me commented 4 years ago

Hey @injektion - that's strange, it seems to be working on my side, with post Postman, and Chrome with the Modheader extension.

Which user are you logging-in as? I'm using X-Webauth-User: admin@admin.com with the default test database.

Also, I did notice that the AuthProxyException throws in the AuthProxyGuard.php file are not working correctly. Instead of returning a 401 response code, it's redirecting back to the /login route.

injektion commented 4 years ago

@ryanc-me Using similar: Chrome + Modheader X-Webauth-User: admin@admin.com

Recreated a blank slate: git clone https://github.com/ryanc-me/BookStack.git cd BookStack copy .env.example .env [update .env for db settings] composer install php artisan key:generate php artisan migrate npm install npm run dev

Doesn't trigger auth-proxy guard when /login hit with X-Webauth-User header set. Works if I turn it on using the AUTH_METHOD=auth-proxy in .env

Any magic sauce you added to the mix?

ryanc-me commented 4 years ago

Oh, right! That is the intended behaviour - you should only be able to authenticate with X-Webauth-User when AUTH_METHOD=auth-proxy is set.

injektion commented 4 years ago

Oh man -- sorry about that... I thought we were doing cascading AUTHs... doh.

I'll bang on that 401 and throw some weird tests at it tomorrow.

Thx

ryanc-me commented 4 years ago

No worries man, I should have clarified that earlier!

Now that I'm thinking, it probably should be possible to use auth-proxy and other auth methods. Perhaps AUTH_PROXY_ENABLED=true to turn it on?

tiredofit commented 4 years ago

Agreed on the AUTH_PROXY_ENABLED in the env. Having Th e capability to utilize whatever backend would be useful the way that the user table is set up. That way those who wanted to simply authenticate and rely on the standard set of auth routines could come along for the ride too. Note that standard relies on email address lookups where as LDAP is uid based. I believe OAUTH is also email however I haven't dug deep in that.

ryanc-me commented 4 years ago

@tiredofit great, I'll amend the code tonight!

As a side note, I'm working on TLS support for both MariaDB and Redis. Is that something you two would be interested in?

tiredofit commented 4 years ago

I can provide test results on that for sure. I'm knee deep in docker so love that there are more options. I can provide test results within a day.

ryanc-me commented 4 years ago

Hey guys, just wanted to touch base quickly. A big project came up at work, so my attention has been elsewhere. I'll try to get some changes pushed through this weekend!

tiredofit commented 4 years ago

We're running slower than anticipated as well. What I did see before @injektion left for a break was a rework of LDAP syncing which introduced artisan bookstack:syncldap.

Presently LDAP only creates the user on first login and assigns the roles, however if we are just passing a header, that introduces problems with either being able to login, or even prepare the users account with additional roles (that aren't passed via LDAP/group sync). This will allow us to run this against a cron script on a regular basis and sync the user tables. I believe the header integration will come next. I'm not sure we'll see some output for the next 10 days, so enjoy your other project :+1:

ssddanbrown commented 4 years ago

Just want to say thanks to everyone in here for their efforts and enthusiasm. This form of authentication is not something I've had experience with before.

Just also want to warn, once this is ready it may take a little while for me to get around to review, test, feedback and eventually merge. I've become very much worn-down when it comes to authentication in the platform since it has taken a massive amount of my attention & time instead of focusing on new features that would benefit a wider part of the user-base. Every time a new method gets added it brings along a slew of additional requests specific for various environments/configurations/flows which all take time.

I'm already nervous with supporting the proposed flows since I have not seen much support/request for the proposed flow. Personally I would say it probably won't be worth the support & maintenance based on my past experience of LDAP/SAML. That said, from a quick read-through of the above, it looks like this may be somewhat more focused in its implementation and configuration which would help.

I may need some help when it comes to fully understanding the different flows for this kind of authentication. It would also help to know if any of you would be able to help on any issues here surrounding this post implementation since that is a genuine fear for me.

ryanc-me commented 4 years ago

Thanks for the reply, Dan. For what it's worth, I have seen a few other Issues where you discuss the difficulty around some other auth methods, especially SAML and LDAP. I do understand that auth systems can bring a lot of technical debt, and your wariness is certainly justified. That's actually why I wrote such a long description for this issue!

I'll see if I can pull up a good, terse article on auth proxies.

In essence, the idea is to fully delegate user authentication to another service - Bookstack simply needs to read a header, and "log in" the user based on its value. The auth proxy will set that header before proxying the request, and will handle authentication. The auth proxy acts as an HTTP proxy, just as Nginx or Apache could.

From Bookstack's point of view, this should require very minimal code to implement. I actually have a working example over here: https://github.com/ryanc-me/BookStack

Also, to answer your last question: I am happy to continue providing support/bugfixes for this auth method, if/when it is merged. I actually have a few other small pull requests lined up, so I'm planning to stick around for a while!

tiredofit commented 4 years ago

Hi Dan, I can definitely relate to the wariness. Authentication can be a bit of a rabbit hole with each different implementation. For the most part I think you have covered it short of having OpenID Connect support (Generic) is probably the final piece that would complete the majority of peoples use cases. More and more we're seeing requests in so many projects for integration into centralized systems

What we're proposing on this issue/PR is more of a companion to the in place authentication methods. With standard you already have a populated user table, and with LDAP the users get created upon first login. If the user exists inside the Bookstack user table, and a configured header passes either the username or email address to the application, it would bypass the requirement to enter in a password, allowing more centralized systems to handle the heavy lifting of authentication.

Some of the work I saw from @Injektion adds a new artisan command to Bookstack to allow populating the user table ahead of time as opposed to the present LDAP authentication functionality. Benefits to this would be for someone to import their users, and assign other permissions that were not passed in the LDAP Group passing scenario, e.g. Local Bookstack groups. The work I have seen is adding 2 files into the code base, and an additional environment variable to tell how in terms of an LDAP search filter. The present user filter syntax in Bookstack unfortunately won't work.

As for the Header authentication, I have seen also implementations in Laravel projects in under 10 lines or code, and wouldn't require constant attention with its one new middleware function added.

As I have mentioned in a previous post - I'm willing to support any queries regarding LDAP, SAML and Header authentication queries in the issue tracker. I will also help with building the documentation as well. Like @ryanc-me I am not planning on going anywhere, I am sponsoring development of features for Bookstack with the hopes that they get included in the main branch, but if not will get placed in my own docker image, however I believe they will be useful to the community at large.

tiredofit commented 4 years ago

Also, for what it's worth this is very much related to #2180 - which I originally submitted, just broken out into its own new issue as it expanded on my original use case.

injektion commented 4 years ago

hey @ssddanbrown - I've been puttering on the sidelines with a little fork that would handle integration of various auth methods without a lot of baggage. Essentially, to make this work in a variety of enterprise systems that use their own SSO/Auth method is a middleware that checks if the user is not logged in, then for the existence of a pre-defined auth variable (usually a $_SERVER["auth_user"] or X-AUTH-HEADER) and then simply checks if that user exists and logs them in.

I've got a working fork over here @ https://github.com/injektion/BookStack/blob/ssologin/app/Http/Middleware/SsoLogin.php which I just have to back out a bit to make it env configurable for custom variables, but as you can see it's a pretty simple middleware addition that would simply extend BookStack for most enterprise environments that use their own Auth and just pass it along to BookStack.

Let me know if you have any concerns or think it might be useful for a PR.

Cheers

tiredofit commented 4 years ago

@ryanc-me Have you had a chance to try @injektion's branch to see if you are able to login using your X-..... header?

andretheolauret commented 3 years ago

Hello, did this fix works actually ?

tiredofit commented 3 years ago

I'm using https://github.com/BookStackApp/BookStack/pull/2289 and its working great.

ssddanbrown commented 3 years ago

In regards to this issue, Please see my comment here on @injektion's PR. The comments there apply to the request here, with concerns of the configurability and variation that would be desired for all instances that want this kind of authentication functionality. On that comment I have provided an example of using the theme system to provide authentication based upon a $_SERVER variable.

You could do something very similar with a small tweak to get a header from the $request passed to the theme event handler. If you'd like a simple example using a header variable just let me know. I'd appreciate any feedback on the use of the logical theme system to achieve this kind of thing.

ssddanbrown commented 2 years ago

I'm going to therefore close this off. As of next release we're going to natively support LDAP, SAML2 & OIDC which should cover most requirements. As per my last comment, while other auth flows like the one requested may be simple by themselves, the level of configuration can be a pain so I'd rather delegate more customizable extension options for such tasks.