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

LDAP Authentication #346

Open
wants to merge 73 commits into
base: master
Choose a base branch
from
Open

Conversation

betapictoris
Copy link
Contributor

@betapictoris betapictoris commented Aug 6, 2023

Limitations

  • Currently the bind user's password is stored unencrypted, this is a limitation of LDAP.

Fixes: #44.

@betapictoris betapictoris changed the title [WIP] LDAP Authentication LDAP Authentication Aug 6, 2023
@sentriz sentriz modified the milestone: v0.16.0 Aug 31, 2023
@sentriz
Copy link
Owner

sentriz commented Sep 7, 2023

thanks for the effort here 👍

it also may take some time for me to review since i don't know anything about LDAP. and i don't think i should blindy accept something i don't know about (no offense 😁) i will do some reading or get a second pair of eyeballs on it

there seems to be some lint errors btw

@nitnelave
Copy link

Actually, they wouldn't even have a subsonic password: you can dynamically create a subsonic user after a successful bind. "I didn't know about this user, but the LDAP server tells me they exist and have access to subsonic, so I'll create an account for them"

Usually, the flow goes like:

  • user sends their credentials to subsonic
  • subsonic connects to the LDAP server as admin, and checks that the user is allowed to access subsonic (that can be either "user exists" or often "user belongs to the subsonic_users group").
  • if the user is allowed access, subsonic connects to the LDAP server using the user's credentials.
  • if that succeeds, the user is logged in. If necessary, create a subsonic account to store preferences and so on.

When querying the LDAP server (usually as the admin to save one query), you can also fetch some more details from LDAP, such as the user's email and/or display name.

@sentriz
Copy link
Owner

sentriz commented Feb 26, 2024

Actually, they wouldn't even have a subsonic password: you can dynamically create a subsonic user after a successful bind. "I didn't know about this user, but the LDAP server tells me they exist and have access to subsonic, so I'll create an account for them"

Usually, the flow goes like:

* user sends their credentials to subsonic

* subsonic connects to the LDAP server as admin, and checks that the user is allowed to access subsonic (that can be either "user exists" or often "user belongs to the subsonic_users group").

* if the user is allowed access, subsonic connects to the LDAP server using the user's credentials.

* if that succeeds, the user is logged in. If necessary, create a subsonic account to store preferences and so on.

When querying the LDAP server (usually as the admin to save one query), you can also fetch some more details from LDAP, such as the user's email and/or display name.

thanks for the info 👍 also, and from gonic's perspective would it be standard practice to hit the LDAP for every http request is receives? seems like a lot of overhead, esp considering the case of browsing a list of albums from a browser. that could be 100s of request to gonic like getCoverArt.view?id=x&p=xxx, which is in turning hitting the LDAP server for each request

@nitnelave
Copy link

No, logging a user in can be an expensive operation (think multiple rounds of cryptographic hashing if you want something secure). Repeated attempts to log in can fairly easily bring an authentication server to its knees if it's not protected.

Usually you'd call the LDAP server once when logging in, then remember the login. Either through a session token, some JWT or similar, you'd get a short-to-medium term authorization (e.g. 1h, 24h, until 20min of inactivity, until explicit logout, etc). Note that the user's access to the server (gonic) can change at any time and you won't be notified of that; you'll only see that when they next try to log in. As a result, keeping the session until the user explicitly logs out is not recommended (but could be offered as an option for those who want it, along with a way to forcefully invalidate the user's active sessions).

@betapictoris
Copy link
Contributor Author

No, logging a user in can be an expensive operation (think multiple rounds of cryptographic hashing if you want something secure). Repeated attempts to log in can fairly easily bring an authentication server to its knees if it's not protected.

The best why I can think of doing this is syncing passwords from the LDAP server (ie. checking local accounts, querying the LDAP server if it fails, then create the user if it doesn't exist, update the password if it's out-of-date). This does have a few caveats (as for every failed login there would be a request to the LDAP server, which could be avoided with some sort of rate limit -- but that leads to out of date credentials). Other than this, I'm not sure if there is any amazing way of implementing this, as far as I know (standard) LDAP doesn't have any token exchange, nor does OpenSubsonic.

@nitnelave
Copy link

Getting the password (hashes) from LDAP is not recommended, both in terms of security (you don't want to spread them everywhere and increase the attack surface) and compatibility (not every server hashes them in a compatible way). For instance, LLDAP doesn't expose the password hashes because it's doing a zero-knowledge proof that basically can't be replicated in gonic.

How does normal auth work in gonic? The credentials are sent with every request? You can implement a server-side auth caching method, i.e. "these credentials were authorized by the LDAP server at time T, so they'll be valid until T+24h before we recheck them"

@betapictoris
Copy link
Contributor Author

Getting the password (hashes) from LDAP is not recommended, both in terms of security (you don't want to spread them everywhere and increase the attack surface) and compatibility (not every server hashes them in a compatible way). For instance, LLDAP doesn't expose the password hashes because it's doing a zero-knowledge proof that basically can't be replicated in gonic.

I totally agree on the security thing, it was a concern I had with the idea too, but my thought was that you could just hash the password after the request (considering gonic would already know it). I still don't think it's a good idea though.

How does normal auth work in gonic? The credentials are sent with every request?

Yeah, every request includes URL parameters.

You can implement a server-side auth caching method, i.e. "these credentials were authorized by the LDAP server at time T, so they'll be valid until T+24h before we recheck them"

I was thinking about this, and the more I do the more logical it seems. I don't know how @sentriz would feel, but the password reset button (in the web UI) could be replaced with an "LDAP refresh" button for LDAP users/in LDAP mode to clear the cache for that user.

@betapictoris
Copy link
Contributor Author

@sentriz, on implementing a cache for LDAP credentials, currently local user passwords are stored in plain text - which I think we can all agree is not a safe long-term solution. Although, this raises the question as to how securing the cache should be approached, I can think of a few ways of doing this (but wanted to run them by you before doing anything that might not fit in with the project) including implementing:

  • a more complex (and therefore secure) method of password storage (such as Argon2) for LDAP credentials, and share code between the database and cache down the line.
  • a simple hashing method (such as MD5 or plain sha256) to go one or two steps beyond a plain text password, but still have a fairly simple and temporary solution.
  • no hashing right now, and add it in once hashing is (maybe?) implemented for the database.

Also, if you don't want to add hashing yourself to the database, I might be able to add it later (although I don't have the resources to work on opening another pull request at the moment).

@sentriz
Copy link
Owner

sentriz commented Mar 5, 2024

to my knowledge, storing is plaintext is a requirement from the subsonic protocol. this is to check credentials using the salt+token method

@sentriz
Copy link
Owner

sentriz commented Mar 5, 2024

there is some more discussion here opensubsonic/open-subsonic-api#25

@betapictoris
Copy link
Contributor Author

to my knowledge, storing is plaintext is a requirement from the subsonic protocol. this is to check credentials using the salt+token method

Sorry for the late response, but I hadn't thought of that, thanks for letting me know.

@betapictoris
Copy link
Contributor Author

You can implement a server-side auth caching method.

@nitnelave, this has been done in fccf016.

Copy link
Owner

@sentriz sentriz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi there seems like there some lint errors

@@ -106,7 +125,7 @@ func main() {
log.Fatalf("invalid exclude pattern: %v\n", err)
}

if len(confMusicPaths) == 0 {
if len(confMusicPaths) == -1 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this left over from testing or smth?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

external user authentification
4 participants