User authentication - Forgot password flow

Hello! Trying to figure out the “forgot password” flow when using faunadb user authentication. Couldn’t find anything in the docs and strangely enough, I couldn’t find anyone else asking for it before either.
I guess the traditional flow would be the user to ask for password reset, then I send them an email with the reset link and a token as a param which I should validate when user lands to the reset-password page. Is there any faunadb integration possible in this flow?
I might be missing something very obvious here. I would really appreciate some help on where to start looking first.

It’s not exactly part of FaunaDB, it’s rather a general security guideline on how to do that. However, FaunaDB’s basic security building blocks (tokens/keys/roles) can help you build it so it makes sense that we offer some guidance. You’ll see that the FaunaDB parts in here are quite minimal though.

The traditional flow is imho still the best (send email with reset link) and it requires a partial backend. How I would do that is:

Requirements:

  • a partial serverless backend (only for your auth concerns, such as login/register/reset/logout), the rest can go straight from the frontend to FaunaDB if desired with short-lived tokens.

Setup:

  • Create a ‘reset-request’ collection.
  • Write a role that allows tokens on that collection to reset the password of the document that is linked to the request (see later)

Reset Flow

  1. Request reset from the FE.
  2. Send a call to the backend
  3. Store a ‘reset’ nonce in a httpOnly cookie (this will verify that nobody just stole your e-mail and that you initiated the request)
  4. Add a FaunaDB document in that reset-request which holds the nonce and holds the reference to the user document from which the password will be reset.
  5. Generate a FaunaDB token for that document.
  6. Send an e-mail to the user asking him to reset (it includes the token in the URL) e.g. the URL could be
    const resetUrl = urljoin(process.env.BACKEND_DOMAIN, 'api', 'accounts/reset', resetToken)
  7. User clicks on reset link which does a call to your backend API.
  8. API grabs the token from the URL and also verifies whether there is a nonce set in the httpOnly cookie. You can then create a client with the token, call FQL code and provide the nonce to it to reset the password. Only if the nonce is the same as the request it will succeed.

Sometimes this might fail for Mobile users since their default browser is a different browser than the browser in which then initiated the request. E.g. Safari that opens by default when you click on a link but you initiated the request in Chrome. In that case it’s a good idea to provide an alternative way (e.g. copy a code into a UI instead)

1 Like

Thank you @databrecht!

Agreed, it’s not really for Faunadb to cover this as a feature. I only thought that since fauna provides a built in authentication system, password reset could not be missing at least as a recommendation.

In my case just for some context I use next.js with this example for authentication.

Yesterday I made a research and implemented something based on the flow described here.

I did create the ‘reset-request’ collection and when a user asks for password reset for their email, I add a document with the following data

{
  email: <user email>,
 ip: <ip from the request headers>,
 token: <token generated from the server>,
}

(If there is already an entry with the same email or ip I replace it with a new one)

Then send the email with the token as a param and when user lands to the change-password page I compare the current time with the timestamp of the token in the database. If it is less than a day old, I proceed and ask them to add a new password. I change the password using the serverClient which has the permission to do it.

Do you see any flaws to this process?

It’s interesting to use the IP from the request headers instead of a nonce and that probably yields a better User Experience.

In that case:

  • It seems you generate tokens yourself, make sure these are truly random. I used a FaunaDB token there since they are random. Makes perfect sense to generate one yourself though but you can’t use ABAC then to allow/disallow setting the password and have to check that token manually in your FQL query. One approach is not more or less secure than the other, both would work.
  • whether IPs can be spoofed, I don’t think that that’s an issue nowadays.
  • I would however choose a shorter password reset time. In my experience such tokens tend to be shorter, 10 minutes or something in that range. But I’m sure there are good guidelines online for the duration of such a token.

Great!
I’m using uid-safe to generate the token so randomness should be fine.

Just one more question about Fauna itself if I may:
When looking for the existence of a password-reset entry in the database, I’m using 2 different indexes password_reset_by_email and password_reset_by_ip and doing the delete like this once for each index:

Map(
  Paginate(Match(Index('the_index_name'), <emailOrIp>)),
  Lambda('entry', Delete(Var('entry')))
)

I’m sure there is a way to do it with a single query, right?
I tried creating a single index with both email and ip fields as terms but then I was unable to find the document using one OR the other term. I am still not fluent at all with FQL :slight_smile:

These are awesome ideas, @databrecht and @proko! Thanks :smiley:

You can use Union

Map(
  Paginate(
    Union(
      Match(Index('password_reset_by_email'), email)),
      Match(Index('password_reset_by_ip '), ip)),
    )
  ),
  Lambda('entry', Delete(Var('entry')))
)
1 Like

Awesome!!Thanks @ptpaterson

I am confused now though.
I might not understand perfectly but it seems you allow to bypass the IP verification? I should see the overall query but to me the IP verification is an extra security measure that verifies that the user that is asking you to reset is the same that initiated the request and not just a person who got access to the e-mail.

You can totally spoof IPs, many ISPs don’t filter this properly. Of course, you can’t get the packet back (because it will route to the person whose IP you spoofed) so you can’t complete your three way TCP handshake, and so fauna would be unusable. However it’s still a great vector for DDOS amplification with anything that uses UDP (like DNS) because you can redirect traffic at someone that isn’t you.

This post was brought you by “Answers you didn’t want to questions you didn’t ask”.

This query is not part of the verification. I only use it to clean up previously created entries for password reset for the same email or ip. It’s to make sure there is always only one active entry/token per email/ip.

If you will have two users at the same time from the same VPN provider’s region try to change their password, one will be quite annoyed - rightly so. Highly unlikely, but could happen I guess.

1 Like

Noted down, thank you @jcubed! It is indeed quite unlikely but definitely something to have in mind for the next iteration.

Ok great, I miss-interpreted the idea of the IP in the resource you linked (or assumed something).
Also, ignore my ‘nonce’ idea. It’s something I’ve seen being done by many for password resets but it doesn’t really add security in the case that the attacker initiates the password reset.

I’d use https://www.troyhunt.com/everything-you-ever-wanted-to-know/ as a reference personally.
Your approach is probably not 100% secure due to emails not being 100% secure. Up to you whether you want to take it a step further and add some form of MFA or secret questions. Depends on how serious your app is I assume.

By now there is a skeleton that includes a forgot password flow as well (and a blueprint for the specific Fauna logic)