Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate passportjs for muti-strategy authentication and SSO #1636

Merged
merged 54 commits into from
Nov 19, 2023

Conversation

lukeIam
Copy link
Contributor

@lukeIam lukeIam commented Mar 24, 2023

As I wrote in the SSO issue (#998 (comment)) I would suggest to integrate a well supported auth middleware instead of implementing single protocols by hand.

This PR shows a possible integration of passortjs into the server code of audiobookshelf.
Login with username + password and google oauth2 integrated and working so far. But other auth providers can be integrated really easily with a few lines of code (supported strategies).

My current implementation is not final nor ready to be merged - it is more meant as a PoC.
But if you (@advplyr and also others) decide that you want to go in this direction I'm happy to develop/contribute to a stable implementation.

I don't have a client implementation atm - I'm testing with postman...
Login via:
POST /login for username + password
GET /auth/google redirects to google login

The response returns the known json with a token but also a cookie is set containing the session.
For the further API communication either the token (Bearer header) or the cookie can be used if authentication.
Maybe the cookie simplifies the handling in the webclients of the apps.

Most of the changes where done in the Auth.js file - I tried to not touch the other code where possible.

So what do you think? Is passportjs a good idea or do you prefer to go in another direction?

@lukeIam lukeIam marked this pull request as draft March 24, 2023 17:56
@advplyr
Copy link
Owner

advplyr commented Mar 26, 2023

I think passportjs will be a good solution from the looks of it. It was attempted here #351 but never got finalized and then the v2 rewrite happened.

@mfcar also discussed implementing this via passportjs in Discord but I don't think anything happened there.

I haven't got around to checking the PR yet but wanted to follow up.

@mfcar
Copy link
Contributor

mfcar commented Mar 27, 2023

I think passportjs will be a good solution from the looks of it. It was attempted here #351 but never got finalized and then the v2 rewrite happened.

@mfcar also discussed implementing this via passportjs in Discord but I don't think anything happened there.

I haven't got around to checking the PR yet but wanted to follow up.

I still need to start the work with SSO. Sorry about this.
The plan in mind would allow registering multiple SSOs using OAuth2 standard on a Configuration Page. Allowing add provider details like Client Secret, Identifier, etc. With this, the admins could add support to login using Plex, Google, Twitter and other providers.
But this will take much more time.

I think the @lukeIam's PR it's a very good start.

@lukeIam
Copy link
Contributor Author

lukeIam commented Apr 14, 2023

Added passport-openidconnect implementation and tested with Authentik using a OAuth2/OpenID Provider

@advplyr
Copy link
Owner

advplyr commented Apr 16, 2023

When getting the local strategy working I ran into an issue where it didn't allow an empty password to be passed in. Since that was already an allowable option and some users may be doing that I included the package in libs folder with the changes.

I haven't tested oauth2 yet but I just got authentik setup.

@lukeIam
Copy link
Contributor Author

lukeIam commented Apr 17, 2023

When getting the local strategy working I ran into an issue where it didn't allow an empty password to be passed in. Since that was already an allowable option and some users may be doing that

oh, wasn't aware of that...

I included the package in libs folder with the changes.

ok - that maintainer seems not want to have such a feature (as it's not longer an authorization).
But maintaining an own version makes further updates (and potential security fixes) harder (to a manual process).
But I see the problem here... (only other workaround I currently see: we tweak the client to send EMPTY_STRING if no password is given - but it's also not a clean solution).

I haven't tested oauth2 yet but I just got authentik setup.

Let me know when I can assist.
My current config is:

{
  "authActiveAuthMethods": [
    "local",
    "google-oauth20",
    "openid"
  ],
  "authGoogleOauth20ClientID": "[ID]apps.googleusercontent.com",
  "authGoogleOauth20ClientSecret": "[SECRET]",
  "authGoogleOauth20CallbackURL": "/auth/google/callback",
  "authOpenIDIssuerURL": "https://[URL]/application/o/audiobookshelf/",
  "authOpenIDAuthorizationURL": "https://[URL]/application/o/authorize/",
  "authOpenIDTokenURL": "https://[URL]/application/o/token/",
  "authOpenIDUserInfoURL": "https://[URL]/application/o/userinfo/",
  "authOpenIDClientID": "[ID]",
  "authOpenIDClientSecret": "[SECRET]",
  "authOpenIDCallbackURL": "/auth/openid/callback"
}

@advplyr
Copy link
Owner

advplyr commented Apr 17, 2023

I think it's fine. The last change in that library was many years ago. All the commits I see over the last several years are documentation updates. It is also a small library so I think we are fine including this internally.

@DDriggs00
Copy link

What needs to be done before this can be implemented?

@lukeIam
Copy link
Contributor Author

lukeIam commented May 9, 2023

I have not fully understood how the current socket connection is working and is authenticated.
But looks like socket.io is used - so maybe something like this could be a solution:

// convert a connect middleware to a Socket.IO middleware
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);

io.use(wrap(sessionMiddleware));
io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));

from https://github.com/socketio/socket.io/blob/master/examples/passport-example/index.js#L75-L80

and on client side:

const socket = io.connect('', {
    extraHeaders: {
        Authorization: "[...]",
    },
});

But again I'm just guessing here...

@lukeIam
Copy link
Contributor Author

lukeIam commented Sep 13, 2023

took a bit longer than promised here (#998 (comment)) - but I found some time to program again 😃

In the dev container I'm now able to login with google auth (openid should also work but untested).
We are matching to the existing users by email.
But the current implementation it quite rough and may things needs to be solved...

Main blocker at the moment:

  • How to "find" back to a "local" app page that can receive the token.

Currently I'm redirecting away from the app completely and have problems finding back.
I've hardcoded localhost:3000 or localhost:3333 for the moment - but this will most likely only work in the dev env.

Edit: maybe oauth2 state field is an option (but would only work for oauth based logins)
Edit2: using a get param and a (short-term) cookie now

But there is also other stuff to do:

  • clean up
  • writing some comments
  • add a few asyncs
  • currently I killed the passport-login for the rest-api in favor of the ui -> find a solution for both
  • figure out why the ci is failing
  • only show buttons for available providers
  • user creation via passport login (optional I guess)
  • more testing 😉
  • add more providers (maybe in next release)
  • I removed the rate limiting - bring it back (is there a passport plugin?)
  • Test the normal (username + pw) login
  • OpenID does return "Unauthorized" for me in Authentik (but Authentik says login is fine - need to investigate)
  • replace http://localhost:3333 with serverUrl (where to get it from)

@advplyr
Copy link
Owner

advplyr commented Sep 14, 2023

Thanks, I'd like to get back into this one again. We need to merge this with master which is going to have a lot of conflicts at this point

@lukeIam
Copy link
Contributor Author

lukeIam commented Sep 15, 2023

@advplyr I already merged in master from time to time and resolved the conflicts (but we should double check)
Edit: Just merged the latest master in again without any new merge conflict (->so it should be up to date)
Also went through the changes of the pr - possible mistakes I did while conflict solving:

  • fsevents lib seems to be removed from package-lock.json, but it's not in the package.json -> so looks like it was a sub-dependency of another package that I upgraded.

@lukeIam
Copy link
Contributor Author

lukeIam commented Nov 4, 2023

I'm currently very busy with other stuff - sorry

Here is my current working stage for optional automatic user creation:
https://github.com/lukeIam/audiobookshelf/compare/auth_passportjs...lukeIam:audiobookshelf:auth_passportjs_autocreate?expand=1

If something is blocking you where I can help please let me know and I will make some time available...

@Sapd
Copy link
Contributor

Sapd commented Nov 4, 2023

@lukeIam
Hey, there is a very active discussion in audiobookshelfs discord (in dev-chat) about that. You probably should join in

Main points we are discussing and testing:

  • passport-openidconnect has to be replaced. It does not support PKCE and does not give back for example groups claim. Also it's rather unmaintained. The current idea is to try https://github.com/panva/node-openid-client . It comes with a passport plugin and on top is certified
  • The idea is to match users by sub instead of usernames or email. It is defined in the OIDC standard and the recommended approach. "Old" users might be matched by name or email if the admin enables that
  • a lot more discussions

So the PR is progressing fast

…d logout URL server settings, use email and email_verified instead of username
@advplyr
Copy link
Owner

advplyr commented Nov 4, 2023

I just made the change to openid-client

That required the JWKS url so I added that to the server settings and UI. We will also need the /end-session URL when logout is implemented so I put that in there.

image

This is getting a bit crowded and seems unnecessary since I think we can get all of that from the config url /application/o/audiobookshelf/.well-known/openid-configuration. If that is the case then the UI could be updated to only ask for that URL and then do a GET request to fill in the other URLs.

The other change I made was using the email and email_verified returned from the userinfo to match with an existing user. This is temporary since we discussed making this a configurable setting.

@advplyr
Copy link
Owner

advplyr commented Nov 5, 2023

I added a button next to the Issuer URL that can be used to automatically fill in the other URLs using the openid config url at /.well-known/openid-configuration

This will also work if users put in the config URL

openid_autopopulate

@advplyr
Copy link
Owner

advplyr commented Nov 8, 2023

I added the server settings for auto-registering and matching existing users.

sub is now persisted on the user in Abs database when it first authenticates. On successful login it first checks if a user is already matched with the sub.
Next it will check for existing user matches by email or username depending on the server setting.
Lastly, it will auto register the user if none was found.

image

image

There are still a few details to workout.

  1. Sending back appropriate error messages. For example, a match is found but it already has a sub we may want to return an error.
  2. Permissions for new user. For now this is just the default permissions. This may be fine to start with.
  3. Handling logouts

@advplyr
Copy link
Owner

advplyr commented Nov 10, 2023

Another part I keep forgetting about is applying/updating the settings to passport. Currently the settings are only applied when first starting the server, so you have to restart after updating settings.

advplyr and others added 4 commits November 10, 2023 16:11
- Provide error handling for /auth/openid
- Add session.mobile inside /auth/openid
- Proper PKCE handling for /auth/openid/callback
- redirect_uri handling for the token url in /auth/openid/callback

Co-authored-by: Denis Arnst <git@sapd.eu>
Remove remaining google oauth server settings
@advplyr
Copy link
Owner

advplyr commented Nov 11, 2023

This is mostly wrapped up now. I removed the google oauth stuff so we can handle that in a separate PR.

Before this merges we may want to add a logout button. I'm thinking the client just stores the auth method locally and shows an additional logout button allowing the user logout of the provider.

We may also want to support setting the account type of auto-registered users using a user provided claim

@Sapd
Copy link
Contributor

Sapd commented Nov 16, 2023

I removed the google oauth stuff so we can handle that in a separate PR

To add to this. This PR as-is should work with Google out of the box, because Google simply uses OIDC. However I did not test it. So a specific Google implementation is not required.

However for the mobile app a change is probably required in the app and in the backend (a small additional route), because Google is has some additional rules regarding the callback URL. I can implement that if someone needs that and the PR here was released.

@advplyr
Copy link
Owner

advplyr commented Nov 19, 2023

I've been doing some more testing and I think things are solid the way they are now.

In separate PRs we can implement setting the account type when auto-registering and a logout button using the openid logout url. As it stands auto-registered users will be the "User" account type with default user permissions (download only).

Another future update can clean up Auth.js and put the auth routes in their own file.

Further testing can be done on edge docker image before it gets released.

Thanks to everyone contributing on this!

@advplyr advplyr merged commit e07d17c into advplyr:master Nov 19, 2023
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants