parse-community / parse-server

Parse Server for Node.js / Express
https://parseplatform.org
Apache License 2.0
20.86k stars 4.78k forks source link

Two-Factor Authentication #4024

Closed jasonm1 closed 1 year ago

jasonm1 commented 7 years ago

I see no straightforward way to implement two-factor authentication (2FA) in Parse Server. Ideally this would allow:

I initially started implementing this in my client-side code (PHP) for my Administrator users using Goole Authenticator, but then I realized anyone with the AppID and the Admin password can simply write their own code to get around it. Oops.

I then thought about storing a second factor, or possibly an IP whitelist, and rejecting any requests in beforeSave, beforeDelete, or beforeFind Cloud Code where the client did not provide the second factor, but that seems clunky. It is also difficult to rate-limit, since the malicious user would be able to log in and only be blocked at the point of accessing data. Further, I believe the second factor would need to be stored apart from the User object itself, since once logged in, the user can access its own data.

@flovilmart suggested using a 3rd party Auth adapter, for example Google, which could potentially work in my case since I only care about the extra security for certain users. However, I don't want to require Google login for everyone, which means native username/password login is still available. Checking triggers in Cloud Code for the particular users could be a workaround, but again, the user would be checked at the point of accessing data, not logging in. I also don't know if this solution would generalize to others' use cases.

One solution I see is to add a beforeLogin trigger in Cloud code. A routine in this trigger could check the second factor (stored with the user object) and reject the Login if it does not match or is not provided. This would also allow for IP whitelisting for certain users (reject login attempts from non-listed IPs) or any variety of custom authentication methods.

Another may be to have per-user control over the authentication method. For example, if a user can login with Google, the user can ONLY login with Google, and not username/password.

I am not familiar enough with how Parse Server handles logins to know if the beforeLogin trigger is feasible, or if some other 2FA system would be easier to implement.

flovilmart commented 7 years ago

We could probably bake 2FA in, with QRcode generation, and secure login/sessions. This would be a major feature where users could opt-in for 2FA. That would be quite a major development touching a few areas of the API, but that wouldn’t be anything unachievable

jasonm1 commented 7 years ago

What do you think of the beforeLogin trigger solution? I noticed a discussion on that a few days ago but it didn't end up addressing the suggestion.

flovilmart commented 7 years ago

That would be kind of messy as tou’ll Need to store each user’s secret anyway, enforce etc.. that could help you, but i’d Like that to be baked in :)

jasonm1 commented 7 years ago

I like the sound of a native 2FA solution as well! I will try to start looking into a first stab at this soon.

montymxb commented 6 years ago

Just bumping this, I think this would be 💯 % awesome to put this in. If I were to just brainstorm off the top of my head this is what comes to mind.

As for blocking requests when pending 2fa, the primary concern is in making sure that until the user can authenticate themselves they should not be granted access as themselves. Since we have ACLs, this, in a nutshell, means we need to treat them like they're anonymous and subjected to the restrictions of public read/write. There are a number of ways this could be done, but I'm wondering if maybe not returning the user on successful login with 2fa enabled would be the easiest. Instead we could return something to indicate that further authentication is required.

That's a pretty rough outline, and it doesn't elaborate on how the SDKs would go about handling this, but it would be a start in the right direction 👍 .

flovilmart commented 6 years ago

That would be at session token emission level, when enabling 2FA, all sessions would be destroyed, the 2FA logIn flow could force having an additional ‘code’ property alongside username/email + password, I dot believe there’s a need for a temporary state (valid username + pass) waiting for ussename + password. Also, providing username + code only should work instead of username + password ?

flovilmart commented 6 years ago

Perhaps providing the 2FA code in je password field would be ok-ish, not sure...

montymxb commented 6 years ago

The username and the code combined would have to be something separate, but it would use some stage so you can't just jump to it unless you've already logged in.

The thought of having 2FA directly with the username and password did occur to me as well. Normally it's requested separately, but honestly I can't think of a good reason why it's not requested up front. If we did, we just authorize on an all or none basis, either it matches password and the 2F code or it fails. That could make it a lot easier to do this, no extraneous 'stage' or 'steps' required (beyond the step of validating the 2F token itself).

montymxb commented 6 years ago

The thing above would have to be something like login and then validateTwoFactor being the second required step, if it were separate that is.

The only issue with having the password and two factor go together is when your 2nd factor is something like a static code that's generated upon successful login. You wouldn't be able to get your code to login with until you had successfully logged in in the first place. If we did an adapter to allow extra 2FA methods that could screw things up if someone did something like an SMS or email code.

flovilmart commented 6 years ago

To enable2FA you usually have to provide the generate code to guarantee he user has property setup his account, so no chance to get locked out. Maybe an endpoint /enable2FA that takes the sessionToken (you need to be loggedin) and the 2FA code, if you fail to provide a valid code, your account stays 1FA secured, upon success, the sessionToken gets regenerated, and 2FA is enforced

montymxb commented 6 years ago

Yep, for setting it up the user would have to successfully provide the proper 2F code at least once before it can be enabled. Could split that up into something like requestEnableTwoFactor and enableTwoFactor. The first endpoint taking the current sessionToken and returning anything necessary to setup 2FA on their second factor. The second endpoint taking the expected 2F token from that user and enabling it on success!

We would need to save and track that a user is in a state of:

Maybe could add a step for disabling two factor if we want to keep it strict.

mnlbox commented 6 years ago

@flovilmart, @jasonm1, @montymxb Any news about this? Is it possible to do this with cloud code? If yes, did you provide some example app or blog post about this?

flovilmart commented 6 years ago

We haven’t worked on it, it’s still up for grabs

georgesjamous commented 6 years ago

@mnlbox

Is it possible to do this with cloud code?

well i think everything can be achieved through cloud code, baking it in is a bit more difficult. Also when using cloudcode you have to find a way to disable the login route, because this would go around any extra function/restriction you set up there.

mnlbox commented 6 years ago

@georgesjamous are you any other suggestion? Maybe we need more simpler way.

georgesjamous commented 6 years ago

Well, i would wait for a fully implemented solution without cloudcode to use it.

There are many ways to achieve this, for a quick setup you could do it by implementing 2 functions 'verifyAccount' and 'provide2fa'. 'verifyAccount' could accept the username only, make sure he exists. The other would accept the password, 2fa code and other important information about the operation. The validation goes in 'provide2fa' as well as the login, it return the session. Then you can become the user in the front-end sdk. This is how i would do it.

And if the beforeSaveHook works for '_Session', you can deny any session that is authenticated via the legacy login by responding with an error. (i have never tried a hook with sessions)

addisonElliott commented 6 years ago

Session hooks do NOT work.

There is an issue and pending PR for this functionality. Just FYI

mnlbox commented 6 years ago

Guys how about implement this with more general tools like Authelia.

https://github.com/clems4ever/authelia

georgesjamous commented 6 years ago

Well then if there is no way we can intercept the login request just from using cloud then its impossible - at this point - to implement 2FA securely.

mnlbox commented 6 years ago

Maybe we also can set login endpoint to deny for prevent this. :thinking: It's just and idea and please lets talk about this here.

georgesjamous commented 6 years ago

Maybe we also can set login endpoint to deny for prevent this. 🤔

what do you mean by this ?

mnlbox commented 6 years ago

Login endpoint.

mnlbox commented 6 years ago

@georgesjamous and other guys any idea about this? Really how you handle 2FA? :thinking:

georgesjamous commented 6 years ago

@mnlbox I highly recommend you wait or this to be implemented or open a PR for it your self, i believe others will help you.

Any other solution using cloud code only will not be secure. The only problem is that you cannot intercept login (or sessions) at this time in cloud code, and if you implement you own, anyone with sufficient knowledge (clientkey, appid, username and password) could login the traditional way and get a session token. I have already told you how i would do it up there (but it is not secure), again because the traditional login is still exposed.

If you are determined to have 2fa working right away (and this is what i think is going on here), a messy way would be to fork parse-server, alter it so you can get a beforeSave hook on sessions. I believe there is an open ticker/pr to make sessions read only with hooks out there but i cant find it, this is the line where you should make the change anyway L33.

Once you do, whenever a session is created you can check for a 'temporary-one-time-use-2fa-token' that is generated by your 2fa code.

So, 1- Remove the check on the session token hook. Use your fork in package.json 2- Create the 2 functions for 2fa (and use whatever 2fa endpoint provider you like) 3- After you successfully check 2fa, generate/use a one-time-use-token and store it in a class somewhere (give it a 2-5 seconds of validity) 4- In sessions, whenever a news one is created. Get the user, check if 2fa is on. If it is look for the token of step3 and allow/deny depending on the case then delete the token.

mnlbox commented 6 years ago

Maybe useful: https://passwordless.net/

@georgesjamous

somq commented 6 years ago

@mnlbox Did you find any solution in the end? @flovilmart, @georgesjamous Do you know if anyone has started working on this finally?

flovilmart commented 6 years ago

I haven’t!

mnlbox commented 5 years ago

@somq unfortunately not. I'm switch to work with Strapi finally because that project is more active than Parse. You can follow 2FA feature request there in this and this url.

flovilmart commented 5 years ago

Too bad you moved to Strapi, I was about to start working on it as we’ll need it for future projects.

mnlbox commented 5 years ago

@flovilmart please tell result here. Maybe we can switch back to Parse if it have 2FA support. I think this library is more better than other: https://github.com/yeojz/otplib Take a look to it :wink:

flovilmart commented 5 years ago

We haven’t started yet. The best way to get it faster would for for you to implement it and propose a PR

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

thisfiore commented 5 years ago

What about:

So you may have unauthorized user log-in but not capable of doing anything.

Could be a good solution? We could try to implement it @etto91

flovilmart commented 5 years ago

This could be implemented on the _User class, adding the TOTP token to the _user object (private obviously) in order to create a session, as user need to provide the username, password and OTP.

One security enhancement would be to generate a restricted session token, that is used solely with an OTP and valid a few minutes. This session Would ne generated on successful user login.

REPTILEHAUS commented 4 years ago

@mnlbox, @flovilmart, @georgesjamous, @addisonElliott has anyone tried to work on this recently ? or got any pointers.

I found this PR https://github.com/parse-community/parse-server/pull/5305 which is a start, 2FA is a really essential and useful feature that should be part of any authentication system, it would be great if we could pick this back up and work on an implementation

Etto91 commented 4 years ago

very bad pr #5305 :)

REPTILEHAUS commented 4 years ago

@Etto91 you got further than anyone else with it so not so bad. I dont suppose you did any more work on it that I can use for inspiration :D I don't know much about the parse internals but you seem to.

mtrezza commented 4 years ago

Re-opening as discussion around this feature continues.

REPTILEHAUS commented 4 years ago

@mtrezza happy to be a part of the implementation of it but I will need some assistance or someone who knows the parse internals to brainstorm with. @Etto91 seems to be be on the right track aside from the security gaps. For my own use case I want to integrate Google Authenticator app.

In my mind the way this works is that the user is logs in - they then get a basic session which cannot do any parse actions, other than call functions which may utilise masterKey, aside from that they are essentially blocked from doing anything parse related other than logout/kill session (does this make sense?), it is then up to the developer to create the OTP screen where the users basic session has the capability to hit a cloud function which will validate the OTP server side and grant a full session. within 1 minute or all sessions are killed for them

My own use case utilised custom auth adapters which make use of Parse.User.logInWith - so I need to cover all authentication angles including standard basic parse login/registration.

So as I see it in the framework the bases that need to be covered all start with the following:

Parse.User.logIn
Parse.User.become
Parse.User.signUp
Parse.User.logInWith

After that I am unsure of parse internals other than a basic understanding as well as the PR by @Etto91 which has given me some kind of base, aside from that I am happy to take direction and not reinvent the wheel i.e add 3rd party libraries to assist in the creation of Google Auth OTP etc.

REPTILEHAUS commented 4 years ago

Or maybe we grant some kind of new session, then with the OTP code submission we pass this new session type along with the validation, if it validates then we pass back a normal session string which the user, on their client side OTP page can use Parse.User.become with it

dblythy commented 3 years ago

Hey all,

I've just created a PR (#6977) that continues the work towards inbuilt 2FA (thanks to flovilmart for his work in getting this started).

My implementation requires users to enrol via /enable2FA, which will be worked into the JS SDK via user.enable2FA(). This then returns a QR Code to be put in the Google Authenticator. The user will then need to enter the code back into the app, where user.verify2FA() will need to be called with a code from the authenticator. Now, 2FA will be enabled on their account, and you can check for this via cloud functions for req.user.get('MFAEnabled'). Similarly, you can handle un-enrolled users by !req.user.get('MFAEnabled').

Now, once a user logs in, a custom error (212) will be called. This is where the UI would have to show 'enter code', where the user would go to Google Authenticator, copy the code, and enter in the app. Calling Parse.User.logIn('username','password','token') with the token they've entered will allow the entry, providing it's a vaild code.

In order to force every user to enable the 2FA such as @REPTILEHAUS has suggested, you could use beforeFind, beforeSave triggers and throw a custom error, and bring up a 'verify 2fa to do this' screen.

What are all of your thoughts on this approach?

mtrezza commented 1 year ago

@dblythy @Moumouls can we close this?

dblythy commented 1 year ago

Yep!

mtrezza commented 1 year ago

Closing via https://github.com/parse-community/parse-server/pull/8156; an example TOTP adapter has been added with https://github.com/parse-community/parse-server/pull/8457.