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

Forgot Password #103

Merged
merged 3 commits into from
Feb 7, 2020
Merged

Forgot Password #103

merged 3 commits into from
Feb 7, 2020

Conversation

j-berman
Copy link
Collaborator

@j-berman j-berman commented Feb 7, 2020

Forgot Password

Our top feature request so far is to let users reset their password. Userbase didn’t initially allow this because the user’s encryption key is stored on the Userbase server encrypted with a key derived from the user’s password. When the user signed in, the original password was needed to first retrieve and then decrypt the encryption key.

However, Userbase also allows you to let users automatically sign in after they close their browser by setting the rememberMe option to 'local' in the signUp and signIn APIs. In this case, the user’s encryption key gets saved in the browser’s local storage.

Now, with the addition of the forgotPassword API, so long as the user still has access to a device that was previously used to log in with rememberMe set to 'local', the user will be able to regain full access to their account using a temporary password sent to their email.

Recovery will not be possible if the user forgets their password AND loses access to all previously used devices (!!!)

Implementation Details

  1. When forgotPassword({ username }) is called, the client establishes a WebSocket connection with the server to prove it has the user's encryption key (or more accurately, the user's seed).
  2. The server generates a random message, encrypts it such that only the user controlling the seed can decrypt the message, then sends the encrypted message to the client.
  3. The client decrypts the message and sends it back to the server.
  4. If correct, the server then sends the user an email with the temporary password and forgotPassword returns successfully.
  5. The user can then sign in using the temporary password and then change their password by calling updateUser({ currentPassword: tempPassword, newPassword }).

Notes from this PR

  • The temporary password expires after 24hrs.
  • signIn now returns a boolean usedTempPassword if the user signed in with the temporary password.
  • My tests for forgotPassword do not make sure that the temporary password sends successfully to a user's email and they do not make sure that the temporary password can be used to sign in. They simply test the API's failure modes or if it returns successfully. This is because it seemed receiving the email in a test would require setting up a test email server or likely involve some other time-intensive workaround. I tested that the temporary passwords sends successfully and can be used to sign in and change a user's password manually.
  • I added *Missing errors (e.g. DatabaseNameMissing) to all userbase-js functions to maintain consistency.
  • Related "Forgot password?" #59

- user can request a temporary password be sent to their email
- temporary password can be used to sign in normally and change the user's password
- user must have their seed stored in the browser for the call to forgotPassword() to work
- initially returned success even if incorrect so that someone calling forgotPassword with the incorrect seed would not know the difference between a successful or failed call. Thus they would not know if their seed is incorrect or not
- However, if client can decrypt the encryptedForgotPasswordToken in the first place, then they would already know they have the correct seed. Server's success response would not prove anything beyond what the person would already know
@swyxio
Copy link

swyxio commented Feb 7, 2020

clever workaround!

Copy link
Member

@dvassallo dvassallo left a comment

Choose a reason for hiding this comment

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

🥳 Looks perfect.

@dvassallo dvassallo merged commit 2ca75a4 into master Feb 7, 2020
@j-berman j-berman deleted the forgot-password-v1 branch February 8, 2020 00:04
@moughxyz
Copy link

This is intriguing 🤔 Although, I’m trying to follow how a user regains access to their encrypted data with this method.

The server generates a random token, encrypts it with an otherwise non-vital key that the client would also know/be able to generate, and proves to the server it has the correct master key.

The server then emails the user a random temporary password:

Here is your temporary password you can use to log in and change your password with: ${tempPassword}

If the user then signs in with this password on another device (not the one they initiated the forgot request with), how will they decrypt all the incoming data that was not encrypted with this reset token?

Or does this method only allow one to regain access to an account, but not regain access to data?

(Admittedly I don’t know the full role of shared secrets with Userbase as I only studied the change log for this particular PR, so perhaps I’ve overlooked important context)

@j-berman
Copy link
Collaborator Author

Or does this method only allow one to regain access to an account, but not regain access to data?

This method allows one to regain full access to the account (including access to all data) so long as the user signs in from a device they have used before with rememberMe set to 'local'.

If the user then signs in with this password on another device (not the one they initiated the forgot request with), how will they decrypt all the incoming data that was not encrypted with this reset token?

The user cannot sign in with the temporary password on another device. The user must sign in with the temporary password from a device they have used before with rememberMe set to 'local'.

First, upon sign up, the userbase-js client generates a random encryption key (technically this is referred to as the user's "seed" in the code). This random encryption key is what's used to encrypt user's data. Therefore if the user has the encryption key, they can decrypt their data. The rememberMe parameter set to 'local' tells the user's client to store the user's encryption key in local storage. At the moment, even with forgotPassword or even when a user changes their password, this encryption key never changes and cannot be changed (until we implement the ability to rotate keys). Note that the password has not come into play at all yet.

Also on sign up, the password is used to derive a key that encrypts the user's randomly generated encryption key from above. The resulting cipher text (the encryption key from above encrypted with the user's password) also gets stored on the server on sign up. When the user provides their password on sign in, the server sends the password-encrypted encryption key to the client, and the client decrypts it using the user's password. Note the server never sees a user's password, the password is hashed client-side before being sent to the server. This way a user can sign in to their account from anywhere without needing the key in local storage.

So again, with this approach, if the user loses their password, but still has their encryption key in local storage, they can regain access to their data.

When the user changes their password, the client will re-encrypt the encryption key with the new password, and send it to the server. The server then overwrites the password-encrypted encryption key. The underlying plaintext encryption key remains constant.

The above is still a simplification of the approach. A more detailed explanation complete with diagrams will be provided in the not-so-distant future.

If this is still unclear, happy to provide some rough diagrams that may be helpful.

@moughxyz
Copy link

Ah, gotcha, so the key is signing in on the same device the reset request originated from. But this brings two questions to mind:

  • How does a user sign in with their temporary password on a device which they are already signed into? Or does rememberMe='local' mean they can also be signed out, but the key is still saved?
  • If the user is signed into the app, why go through the "forgot password" flow when they can just go through the "change password" flow that would have the same result but without the temporary password?

@j-berman
Copy link
Collaborator Author

j-berman commented Feb 10, 2020

rememberMe='local' means the key is saved even when they sign out.

So unlike with the change password flow, the user does not need to be signed in to call forgotPassword(), the user just needs to have the key saved in local storage (edit: and an email address already saved on their account for the server to send the temporary password to).

Note that even if someone finds the user’s encryption key in local storage in the above circumstance, they would still need the user’s password (or the user’s email) to access the user’s account/data.

We are also considering adding a forgetMe param to signUp and signIn (or to just signOut) that clears local storage in case the developer wants to delete their user’s key and any trace that the user logged in when the user explicitly signs out.

@moughxyz
Copy link

moughxyz commented Feb 11, 2020

rememberMe='local' means the key is saved even when they sign out.

Oh I see.

Interesting!

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.

None yet

4 participants