Authentication Security: Password Reset Best Practices and More

We aggregated a series of notable ‘traps’ below. We’ll go through each of them, identify possible security breaches and issues, and fix them.

 min. read
Published on
November 26, 2021

This article covers measures to secure an authentication server along with real-life examples.

You can find tons of tutorials on implementing an authentication server online, but not so much on how to actually secure one. Throughout our years developing, refactoring, or reviewing a user authentication flow (our own or others’), we’ve seen countless pitfalls. With some fellow developers, we aggregated a series of notable ‘traps’ below. We’ll go through each of them, identify possible security breaches and issues, and fix them.

What is Authentication?

Authentication is a process to validate a user on his/her identity claim, or in short, who you are. An authentication server offers such service through various flows (e.g., traditional username + password, passwordless, SSO, etc.).

Authentication is not authorization

It’s confusing when someone says “auth” but you have no idea whether it refers to authentication or authorization. This StackOverflow piece provides a more in-depth insight on this.

Pitfalls in a ‘Forgot Password’ Flow and Best Practices to Address It

We’ll start with resetting user passwords and what security issues are often overlooked in this flow. Below are some notable issues we’ve seen in a ‘forgot password’ flow. All solutions are backed with references from OWASP’s ‘forgot password’ cheat sheet, and you should read them if you’re looking for password reset best practices.

✕ Allowing Login ID Guesses

Let’s assume that your ‘forgot password’ application form lets a user key in an email (i.e., the login ID and forgot password email recipient). If an account is registered under that email, a reset password link is sent to it. Otherwise, nothing happens.

A very basic 101 concept on security can be applied here, as suggested by OWASP: Always show a consistent message when an email is entered, whether the account exists or not. (e.g. “an email will be sent to this email if an account is registered under it.”) This prevents attackers from being able to match a login ID.

✓ Give ‘Reset Password’ Links a Lifetime

Give the ‘reset password’ link a reasonably short period of time. This reduces the chance of an attacker intercepting one and gaining access to it by resetting a password.

✕ Identifier in ‘Forgot Password’ link as PII

never put user id or other pii in a reset password link

A common way to identify a password reset session is to pass a URL token as the query string of the URL, as suggested by OWASP.

However, avoid using any personally identifiable information (PII). Never take chances when it comes to PII. Even if encryption is applied, it can still be broken/decrypted by attackers, where they can then use the PII to match a user from your system.

We’ve seen “encrypted” user IDs being used as the password reset token passed in a URL, which is not a very good idea, as aforementioned. It's also a common problem for some token encryption/hashing wasn’t done properly, e.g. a cryptographically broken hash algorithm like MD5 was adopted.

Always use randomly generated ID as the identifier. Give each generated ‘reset password’ session a life span and prevent brute-force matching attempts on the ID by implementing rate-limit mechanisms on the URL token.

✓ Ensure Password Security Policy is Applied

Ensure that the same password policy is applied in all password setting stages, no matter if it’s during account creation or password reset. Don’t create an over-complicated password policy (such as require a specific combination of numerics and symbols etc), instead stick with some simple like the following:

  1. A minimum length of 8 characters
  2. A not-too-low maximum length to discourage users from creating longer passphrases
  3. A strength meter (i.e., zxcvbn) to measure the password complexity
  4. Common words that are banned

The above password rules are suggested by OWASP and Microsoft.

✓ Invalidate existing sessions

Upon a successful reset attempt, remind the user to review all existing logged-in sessions. It’s also common that all sessions are invalidated during this stage.

✕ Password Not Hashed Properly

Password hashing is frequently discussed in authentication security. Hashing a password with weak algorithms like MD5 is not recommended.

A simple and effective solution is to choose a hashing function widely regarded as secure, like argon2 or bcrypt, as suggested by OWASP.

You might be wondering: Why bcrypt, but not SHA-256 or SHA-512? One argument is that the SHAs’ computation can be accelerated by a GPU, while bcrypt’s can’t. This puts the attackers (those trying to compute and match the hash value of your password) at a disadvantage as they can’t guess it quickly or easily.

This answer on StackExchange sums up the comparison of PBKDF2 vs bcrypt vs SHA256 pretty well. Here’s another good read on password hashing and some common hashing algorithms.

✕ No Expiry on Access Token

Let’s assume that you are generating access tokens properly with safe encryption. If there’s no expiry mechanism, the tokens that were already generated will haunt you forever. This is literally giving hackers unlimited time to pull off a token sidejacking. Just imagine an attacker getting their hands on an access token! They can authenticate themselves and go into your system and do whatever they want. This is quite likely to happen. Just open your cookies manager on your favorite browser and check how many access tokens are stored there.

Even if all transits are conducted with HTTPs, access token with no limited life span can still pose serious threats, there are no way to guarantee the security of the tokens for lifetime, such as a compromised user agent (laptop/phone), or protocols which are secured today might be compromised in future.

To prevent attacks like this, you can set a reasonably short expiry time and rotate your sessions. That way, it becomes virtually impossible for someone to decrypt sessions/tokens from communication data before they expire. Let’s say your tokens live for an hour only. The attacker has to intercept the traffic, break your encryption, and finally log in with the cracked credentials — all within 60 minutes, which is onerous.

✕ Hard-coded Secrets / Tokens

Sometimes a developer might have a tight schedule and need to rush things through. However, hard-coding secrets and any sensitive information in your code is never ever a good idea. One of our horror stories is that we have seen a JWT hard-coded in a function.

If you host your own version control server and it’s hosted with plain HTTP; or some developer includes .git in deployments or some other operations so that it got leaked, your source code may be leaked, which will result in exposing secrets. That means your system is pretty much cracked wide open, with all security measures null and void.

So, don’t hard-code secrets / tokens! Also, serve with HTTPs and don’t include git files to deployments!

Consider encrypting your secrets (we often use blackbox) in file/s separate from the source code if they need to be shared. You can also generate them on the fly and gitignore them during development.

✕ Sensitive Information in Log

To debug and get an idea on a data flow, developers love to log things. Don’t log sensitive information though! Imagine you have this console.log(jwt) somewhere in your JS code and the web app gets deployed. Always keep such data hidden by sensitive filters.

✓ Apply Data Masking

By introducing a SensitiveContent interface and encapsulating all sensitive data values with it, you can force developers to think twice before logging sensitive information.

Examples of Sensitive Data

Below are some examples of data that should never be logged. A more complete list can be found in in the Logging cheat sheet from OWASP:

  • Tokens
  • Passwords
  • Connection strings
  • Secret keys
  • Bank/card data
apply data masking on all sensitive data

✕ No Guarding Measures Against IDN Homograph Attack

This authentication-related attack is a mind-blower if you’ve never heard of it, and often overlooked by developers. Let’s say there’s an admin of the authentication server called “admin e,” who posts regular updates and release notes. Can you tell the difference between the words below?

аdmin е vs admin e

You can visualize the difference between the two usernames by looking at each Unicode. The second row item is the actual “admin e,” while the first one uses Cyrillic characters to fake the username. I have used another font family in the table below to show their font-level difference as well.

UsernameUnicodeadmin e\u0061\u0064\u006d\u0069\u006e \u0065аdmin е\u0430\u0064\u006d\u0069\u006e \u0435

Simply disallow the input of these characters to defend against this kind of attack.

✓ Password Rate Limiting

Sometimes password rate limiting is non-existent in an auth system and it’s never a good idea. You’re basically inviting brute forcers to guess a password over and over again without limitations, which boosts their chances of getting a match, and hence stealing one successfully.

Password rate limiting (or number of retries) reduces the success rate of password-guessing attempts simply by not letting the attempts to get through, often under these circumstances:

  • A lot of failed logins from the same IP address, no matter the login ID (e.g., username, email, phone etc.)
  • A sequence of failed logins on one account from various IP addresses over a short period of time. For example, if the system receives three login requests on the same account from three different continents, it’s likely a brute-force password attack.
  • A bunch of logins on various login IDs with a pattern/sequence, like { John1, John2, John3 } or { john@gmail.com, john@hotmal.com, john@yahoo.com}
  • Oher cases covered by OWASP’s Blocking Brute Force Attacks guide

Apply password retry limit. Refer to the above rules/cases to evaluate whether one is a brute-force attempt, and make sure not to set the retry limit too low — a lot of users need to try a few times before they figure out their password.

✓ Ensure One-Time Tokens Are Really Used Once

We’ve encountered several authentication servers with features involving one-time tokens like a passwordless login email. These features, when done properly, can enhance a user’s experience in a safe manner. However, there may be chances where you may not realize that you didn’t really use a one-time token only once.

Keep track of the one-time tokens and ID them (i.e., the token itself can be an ID already). If an ID is on the list of ‘used’ IDs, or if its expiry is computed with its created_at, decline the request on whatever the one-time token user is asking for.

Summary

The issues we outlined are just the tip of the iceberg. If your auth service has some of them, it’s likely that its architecture wasn’t well designed or has other underlying flaws. Not to toot our own horn, but given the tons of our development projects, we’ve become proficient in conducting code reviews — and auth is one of our main focus areas.

We’re fairly confident with that, as we developed our own auth as a service — Authgear — from scratch. It has gone through rounds of professional security auditing. All authentication features mentioned throughout this article are supported by Authgear, plus a few more, like session management, password policy and authentication UI done for you (yes, the whole authentication stack are taken care of for you!).

Feel free to contact us for a demo.

This article was originally published on the Oursky Code Blog on 9 February, 2021