1

The app is divided into two parts, the fronted - written with the Angular framework and the backend, simple PHP files which handle the login, API calls, etc.

My current flow is the following:

  1. User creates an account/login. Credentials are sent in plaintext through HTTPS.
  2. Credentials are being checked against the database (password is salted + hashed using this https://www.php.net/manual/en/function.password-hash.php and compared with what I have in DB using https://www.php.net/manual/en/function.password-verify.php).
  3. Login info match - A JWT is generated (using this library: https://github.com/firebase/php-jwt) with RS256 with sensible info in the body (account index in the database, nothing more) and an expiration time of 2 minutes.
  4. The JWT token is sent to the frontend.
  5. The JWT token is stored in the memory of the app (in a variable, in the javascript code) and when the user does any action, the JWT is sent along in POST.
  6. The JWT is decrypted (ref step 3) and the expiration time is checked. If the token is expired, redirect to the login page with a warning message. Otherwise, proceed and return the requested information and regenerate a token with a new expiration time (current time + 2 minutes).
  7. Return to step 4).

Do you see any security risk in this flow ?

  • I hope you are using [`password_verify()`](https://www.php.net/manual/en/function.password-verify.php) to check if the passwords match –  Aug 25 '21 at 11:49
  • I am, sorry I didn't said that in the main post. – NeebletWorm Aug 25 '21 at 11:58

1 Answers1

2

Let's go through this step by step:

Step 1.) User Registration

This step seems perfectly fine. There are guidelines out there on what to keep in mind during user registration, but since that's not the focus of the question, let's just say you're doing fine.

Step 2.) Login

You say you use the PHP function password_hash() during login, but it should actually be password_verify().

password_hash($password, [$algo]) should be called during registration, to create a hash with a random salt, and then store the result in a database. Surprisingly, PHP actually picked sane defaults for this function. PASSWORD_DEFAULT uses bcrypt by default, which isn't the best algorithm, but certainly not objectionable. You can also use PASSWORD_ARGON2ID, which is considered state-of-the-art. Just be careful to pick good workload parameters!

password_verify($password, $hash) should be called with the password provided during login. PHP will grab the correct algorithm, parameters and salt from the provided hash, so you don't even need to know how the hash was originally generated to verify if the password matches.

Step 3.) Token Generation

You generate a token with RS256, which is an asymmetric algorithm. Essentially, an RSA private key is used to sign the JWT payload, and the signature is then validated using the public key.

The advantage here is that the JWT provider does not have to be the same entity as the JWT validator. If this makes sense in your architecture, an asymmetric algorithm is preferable.

An alternative is HS256, which is a symmetric algorithm. The key to generate a valid JWT and to verify if a JWT is valid is identical. If the JWT provider is also the JWT validator, then you can safely use HS256.

Regarding vulnerabilities, there is a vulnerability in some libraries with RS256, where a token can be signed as HS256 using the public key for RS256. The library will attempt to use the RSA public key as HMAC secret-key and the signature will be valid. Not a huge issue these days, but something to keep in mind.

Furthermore, a validity of 2 minutes is extremely short. Keep in mind that from the moment a user logs in, they have a grand total of 2 minutes to spend on your application before they get kicked out. As a comparison: Most banking websites use a timeout of 15 minutes. So unless you have a really good reason to give your users only 2 minutes on your application before they need to re-authenticate, you should give them more time.

Step 4.) Sending the JWT to the Frontend

As long as this is done via TLS it should be fine.

Step 5.) Token Storage

JWT Tokens should not be stored as JS variable, nor in the local storage, nor in the session storage. The reason for this is that these places are very accessible for attackers, who may leverage XSS attacks.

A better place would be a cookie with the HTTPOnly flag set. This disallows (malicious) JavaScript from accessing the token, but you can still use the token for XHR requests. So your legitimate logic should not be affected by it.

Step 6.) Token Verification and Regeneration

There are some problems with your logic here. First of all, tokens are not decrypted, because they were never encrypted in the first place. Tokens are validated. This may sound nitpicky, but when it comes to security, correct terminology is important.

A check for expiration time is perfectly reasonable, and a following redirect to re-authenticate is as well.

The issue is with token regeneration: This is a bad idea. A very bad idea. In practice, it seems reasonable enough: Use a fast expiration time and regenerate as needed, then the token will become invalid as soon as the user is done using the app, right?

The issue arises from when an attacker steals the token. Once stolen, an attacker can essentially keep the token indefinitely, or rather, indefinitely exchange one valid token for another. Even invalidating one token will do very little, because the token can be exchanged for many other tokens as well.

In fact, if the token is regenerated every time you do something in the app, then you can use one token to generate infinitely many new tokens, and then use those tokens to generate even more. One an attacker has gained access to one token, they can never be stopped again.

This is obviously a bad thing, and can be prevented easily. Give your users two tokens: A refresh token and an access token. The access token should have a small, but still usable expiration window (between 15 and 120 minutes). The refresh token should have a longer validity. If you expect the user to remain authenticated, as is usual with most web applications (Stack Exchange, Twitter, etc...), then the refresh token may last several months to a year.

Essentially, the user would attempt to access a resource with the access token (if it is still valid). If the access token has expired, the long-lived refresh token can be used to get a new short-lived access token.

Why is this advantageous? First of all, access is still somewhat short-lived, so an attacker stealing an access token may only have a limited amount of time in the application. Secondly, a stolen access token cannot be used to persist access. Finally, a stolen refresh token can be invalidated by a user, thus limiting the damage an attacker can do. This is a good article regarding JWT blacklisting.