rdaum / moor

A system for building shared, programmable, online spaces. Compatible with LambdaMOO.
GNU General Public License v3.0
154 stars 8 forks source link

Improve the authentication / sign-on story #30

Closed rdaum closed 6 months ago

rdaum commented 9 months ago

LambdaMOO cores' connect function uses a variant of the Unix crypt function, ala old Unix passwd files, and stores the passwords in-db.

For compatibility with existing cores (and to make servicing existing telnet clients feasible), it's necessary to keep this process for now. But this isn't going to cut it as a security story.

Some kind of SSO story & auth token is going to be needed. For the latter, one option is JWT, another is PASETO, which sounds more promising.

Then the websocket connection (and any HTTP sessions generally) will need to be authenticated with this token before client connections can proceed.

The long-term future for the telnet modality will need to be thought about in this context.

Not a requirement for 1.0 (LambdaMOO compatible) release.

jaybird110127 commented 9 months ago

I would think that for now, you should be able to replace the underlying crypt() builtin with some sort of hash, and have that builtin ignore any salt passed to it. If I understand the way the whole thing works, this should still work, without modifying any MOO code. The code crypts a password, gets a hash back, stores that, then when it needs to check it, it crypts the supplied password, also supplies the first two chars as salt (which a new crypt builtin would ignore) and gets a hash back, and if they match, we're good.

rdaum commented 9 months ago

You're probably right it would be possible to make it work that way, but I'm still not convinced that having passwords in-core is the right story at all... and I'm pretty sure what we're describing here is not good security practice in this day and age.

And there's no reason in this day one couldn't support e.g. OAuth2, with just tying the 'player' object ID to external credentials, and rely on other, better-structured/audited, etc. security services to carry the weight & risk.

jaybird110127 commented 9 months ago

All this is true. However, if you did make the crypt() builtin hash instead of encrypt, you could have an unmodified LambdaCore, right out of the gate, that doesn't store passwords (not even crypted ones) in the database.

pppery commented 9 months ago

The code crypts a password, gets a hash back, stores that, then when it needs to check it, it crypts the supplied password, also supplies the first two chars as salt (which a new crypt builtin would ignore) and gets a hash back, and if they match, we're good.

The code actually supplies the entire crypted password as the salt (even though the documentation for crypt() says not to do that), and the crypt() function itself is responsible for reducing it to two characters. Which I guess means you could have crypt() implement whatever the current standard for password hashing is and there's no reason that wouldn't cut it.

Personally I don't understand the arguments against storing passwords in-database - my proposed scheme is basically identical to the way that any other login-requiring website works. I thought the idea of MOO was to do as little as possible in-driver and as much as possible in-core.

Karrtaan commented 7 months ago

Hi. Looking deeply into this, I wonder why not using SSH instead? While most clients are very well able to connect via telnet, they cannot utilize all the neat features of SSH, yes, for that it would be necessary to build some sort of local proxy to translate from the outbound SSH-connection to an internal telnet connection, but this way all features of SSH could be used, including public key encryption, SSO etc. Using this, I would guess it is possible to use the same SSO-backend for both http and pseudo-telnet connections. Just some thoughts from a user perspective. Yes, it breaks compatibility with telnet, on the other side it solves almost all security issues. Telnet is a security risk by itself anyway.

rdaum commented 6 months ago

Hi. Looking deeply into this, I wonder why not using SSH instead?

Sorry to not reply to this for a while. I have been busy with real, paid, work, and haven't had time to give to moor in a while.

But to update, what's actually happened here is that I've chosen to prioritize the web/websockets story over everything else. The expected primary mode of interaction with the moo hosted in moor is to be over HTTP and websockets (and, likely, in the future webtransport). The line-command oriented style of LambdaMOO is supported over websockets (which are kinda like 21st century "telnet", really), but verb invocations, property access, and eval are also supported over (authenticated) HTTP GET/POST.

And because of that what I ended up implementing for authorization is the following:

in web-host, HTTP authorization is established through two URLs (/ws/connect and /ws/create) which take HTTP basic auth (presumably only over an HTTPS connection). They then contact the moor backend (daemon) over ZMQ RPC, do standard MOO password authentication (salted hash but just for now) and then establish PASETO auth tokens. (PASETO is a little bit like jwt but better. )

Then the actual sessions -- either websocket, or HTTP -- have to provide a valid PASETO token for each connection. For the websocket, this is passed as a query string parameter before the websocket upgrade. For HTTP POST/GET it is passed in HTTP headers. (Browsers unfortunately do not support custom HTTP headers for websocket connections, which boggles my mind).

This was all implemented back a couple months ago, along with a primitive web client that lets you connect, authenticate, does the command loop over the websocket, but also has a prototype (but incomplete) Smalltalk-style object browser and syntax highlight MOO verb code editor (using the same editor that drives VSCode).

But I have not had time to write it up and document it yet. I still consider the last bit prototype phase.