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

JWT Token Invalidated Without Updating Map #31

Open
zerox1212 opened this Issue Sep 28, 2017 · 9 comments

Comments

Projects
None yet
2 participants
@zerox1212

zerox1212 commented Sep 28, 2017

I'm trying to solve this problem detailed in the below forum posts:
http://community.dreamfactory.com/t/forgot-password-api/1236/6
http://community.dreamfactory.com/t/password-reset-not-working-after-user-logins/3180
http://community.dreamfactory.com/t/unable-to-change-or-reset-password/4100/

After looking through much of the code, I have found what I believe to be the issue.

Sometimes JWTUtilities::invalidate($token); is called to change a token to blacklisted. This function seems to also remove the token from the map.

Othertimes JWTAuth::invalidate(); is called directly:

public static function invalidateTokenByUserId($userId)

    public static function invalidateTokenByUserId($userId)
    {
        DB::table('token_map')->where('user_id', $userId)->get()->each(function ($map){
            try {
                JWTAuth::setToken($map->token);
                JWTAuth::invalidate();
            } catch (TokenExpiredException $e) {
                //If the token is expired already then do nothing here.
            }
        });
        return DB::table('token_map')->where('user_id', $userId)->delete();
    }

I think part of the password reset + blacklisted token issue stems from this. Especially when the error stems from here:

throw new InternalServerErrorException("Error processing password reset.\n{$ex->getMessage()}");

Because invalidateTokenByUserId will get called via the user model at User.setPasswordAttribute($password).

public function setPasswordAttribute($password)
    {
        if (!empty($password)) {
            $password = bcrypt($password);
            JWTUtilities::invalidateTokenByUserId($this->id);
            // When password is set user account must be confirmed. Confirming user
            // account here with confirm_code = null.
            // confirm_code = 'y' indicates cases where account is confirmed by user
            // using confirmation email.
            if (isset($this->attributes['confirm_code']) && $this->attributes['confirm_code'] !== 'y') {
                $this->attributes['confirm_code'] = null;
            }
        }
        $this->attributes['password'] = $password;
    }
@df-arif

This comment has been minimized.

Show comment
Hide comment
@df-arif

df-arif Sep 28, 2017

Contributor

@zerox1212 , I tried to reproduce this issue but failed. Can you show me steps needed to reproduce this? Also where did you see that JWTAuth::invalidate() is called directly?

Contributor

df-arif commented Sep 28, 2017

@zerox1212 , I tried to reproduce this issue but failed. Can you show me steps needed to reproduce this? Also where did you see that JWTAuth::invalidate() is called directly?

@zerox1212

This comment has been minimized.

Show comment
Hide comment
@zerox1212

zerox1212 Sep 28, 2017

The below function can be called by setPasswordAttribute($password) and it won't update the map as far as I can tell. This could be why some users report Password Reset only works the first time.

public static function invalidateTokenByUserId($userId)

zerox1212 commented Sep 28, 2017

The below function can be called by setPasswordAttribute($password) and it won't update the map as far as I can tell. This could be why some users report Password Reset only works the first time.

public static function invalidateTokenByUserId($userId)

@df-arif

This comment has been minimized.

Show comment
Hide comment
@df-arif

df-arif Sep 28, 2017

Contributor

It removes the token mapping for the user at the last line of the method - invalidateTokenByUserId
return DB::table('token_map')->where('user_id', $userId)->delete();

Also I can reset a user password multiple times without running into any issue. Which version of DreamFactory you are running?

Contributor

df-arif commented Sep 28, 2017

It removes the token mapping for the user at the last line of the method - invalidateTokenByUserId
return DB::table('token_map')->where('user_id', $userId)->delete();

Also I can reset a user password multiple times without running into any issue. Which version of DreamFactory you are running?

@zerox1212

This comment has been minimized.

Show comment
Hide comment
@zerox1212

zerox1212 Sep 28, 2017

For your test you should do this with forever sessions enabled.

  1. Login as normal (authenticate)
  2. Do a PUT to the user/session endpoint to refresh the token (this will blacklist your original token and create a new one)
  3. Try to reset the password after the PUT -> Now password reset fails due to token being blacklisted

This error only happens to people who are using PUT to refresh tokens (in my case forever tokens). I have version 2.3.0.

zerox1212 commented Sep 28, 2017

For your test you should do this with forever sessions enabled.

  1. Login as normal (authenticate)
  2. Do a PUT to the user/session endpoint to refresh the token (this will blacklist your original token and create a new one)
  3. Try to reset the password after the PUT -> Now password reset fails due to token being blacklisted

This error only happens to people who are using PUT to refresh tokens (in my case forever tokens). I have version 2.3.0.

@df-arif

This comment has been minimized.

Show comment
Hide comment
@df-arif

df-arif Oct 2, 2017

Contributor

I tried several ways to reproduce this but everything seems to be working correctly at my end. One thing I would double check in your case is that after your refresh your token, make sure to use the new token to reset password. In fact, for resetting password you actually don't even have to provide any token. The password reset api (api/v2/user/password) is not protected.

Contributor

df-arif commented Oct 2, 2017

I tried several ways to reproduce this but everything seems to be working correctly at my end. One thing I would double check in your case is that after your refresh your token, make sure to use the new token to reset password. In fact, for resetting password you actually don't even have to provide any token. The password reset api (api/v2/user/password) is not protected.

@zerox1212

This comment has been minimized.

Show comment
Hide comment
@zerox1212

zerox1212 Oct 2, 2017

I will do more research into this problem and get back to you. Can you confirm that doing GET to user/session will return a new token? We followed what was posted online about doing GET instead of PUT, but I don't see how DF would send a new token using GET.

zerox1212 commented Oct 2, 2017

I will do more research into this problem and get back to you. Can you confirm that doing GET to user/session will return a new token? We followed what was posted online about doing GET instead of PUT, but I don't see how DF would send a new token using GET.

@zerox1212

This comment has been minimized.

Show comment
Hide comment
@zerox1212

zerox1212 Oct 17, 2017

So I did more testing with this issue and found out what to is needed to remove the blacklisted error when resetting a password. You must Flush System-wide Cache. Then you can reset a user password without getting a token error. Any ideas where to look based on that info?

I think the real problem might be that even though the token is removed from the map, the old token is still in the cache. @df-arif

zerox1212 commented Oct 17, 2017

So I did more testing with this issue and found out what to is needed to remove the blacklisted error when resetting a password. You must Flush System-wide Cache. Then you can reset a user password without getting a token error. Any ideas where to look based on that info?

I think the real problem might be that even though the token is removed from the map, the old token is still in the cache. @df-arif

@df-arif

This comment has been minimized.

Show comment
Hide comment
@df-arif

df-arif Oct 17, 2017

Contributor

@zerox1212 , What you are seeing with flushing system-wide cache is an expected behavior. A token is blacklisted by adding it to a list of blacklisted token in cache. When you clear system-wide cache (this is typically done by system admins) it clears the blacklisted tokens in cache and therefore you don't get the error again. Eventually when this token is expired it can never be reused or be refreshed regardless of whether it is blacklisted or not.

But I believe your issue is not related to this blacklisted token cache. You should not get the blacklisted token error in the first place when you are resetting password. When you reset the password just make sure not to provide any token (JWT) as part of that API call. As I mentioned earlier the API (api/v2/user/password) for resetting password is open and doesn't require a token. I am thinking that somehow when you are making the call to api/v2/user/password to reset your password, you are providing the old token with it which is blacklisted as you already refreshed it. Here are the APIs that are commonly used for auth and password reset.

POST api/v2/user/session : Used for authentication. Can be called without a JWT (Open API). This will authenticate the user, create a session and return the JWT.

GET api/v2/user/session : Used for retrieving current user session. Requires passing the JWT for the user session you are retrieving. IT DOESN'T CREATE A NEW SESSION OR JWT. IT VERIFIES THE JWT PASSED THAT YOU PASSED IN AND RETURNS THE ASSOCIATED USER INFORMATION FOR THAT SESSION.

PUT api/v2/user/session : Used for refreshing a JWT that is still inside the refresh TTL window. Requires passing the JWT that is being refreshed. Once a JWT is refreshed it is added to the blacklist.

POST api/v2/user/password: Used for resetting password. Can be (and should be) called without a JWT. When you are resetting a password it is expected that you currently don't have an active session therefore no JWT is required to pass.

If you are still getting the blacklisted error after calling the password reset API without passing any JWT then please provide the full error with the stack trace. Also I would recommend upgrading to the latest version of DF (version 2.9.0 as of 10/17/2017). The latest version has lots of improvements and bug fixes.

Contributor

df-arif commented Oct 17, 2017

@zerox1212 , What you are seeing with flushing system-wide cache is an expected behavior. A token is blacklisted by adding it to a list of blacklisted token in cache. When you clear system-wide cache (this is typically done by system admins) it clears the blacklisted tokens in cache and therefore you don't get the error again. Eventually when this token is expired it can never be reused or be refreshed regardless of whether it is blacklisted or not.

But I believe your issue is not related to this blacklisted token cache. You should not get the blacklisted token error in the first place when you are resetting password. When you reset the password just make sure not to provide any token (JWT) as part of that API call. As I mentioned earlier the API (api/v2/user/password) for resetting password is open and doesn't require a token. I am thinking that somehow when you are making the call to api/v2/user/password to reset your password, you are providing the old token with it which is blacklisted as you already refreshed it. Here are the APIs that are commonly used for auth and password reset.

POST api/v2/user/session : Used for authentication. Can be called without a JWT (Open API). This will authenticate the user, create a session and return the JWT.

GET api/v2/user/session : Used for retrieving current user session. Requires passing the JWT for the user session you are retrieving. IT DOESN'T CREATE A NEW SESSION OR JWT. IT VERIFIES THE JWT PASSED THAT YOU PASSED IN AND RETURNS THE ASSOCIATED USER INFORMATION FOR THAT SESSION.

PUT api/v2/user/session : Used for refreshing a JWT that is still inside the refresh TTL window. Requires passing the JWT that is being refreshed. Once a JWT is refreshed it is added to the blacklist.

POST api/v2/user/password: Used for resetting password. Can be (and should be) called without a JWT. When you are resetting a password it is expected that you currently don't have an active session therefore no JWT is required to pass.

If you are still getting the blacklisted error after calling the password reset API without passing any JWT then please provide the full error with the stack trace. Also I would recommend upgrading to the latest version of DF (version 2.9.0 as of 10/17/2017). The latest version has lots of improvements and bug fixes.

@zerox1212

This comment has been minimized.

Show comment
Hide comment
@zerox1212

zerox1212 Oct 19, 2017

I know I'm not sending a token. I will try to get a stack trace. Thanks for the detailed response.

zerox1212 commented Oct 19, 2017

I know I'm not sending a token. I will try to get a stack trace. Thanks for the detailed response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment