Search code examples
authenticationcookiesflaskjwtflask-jwt-extended

flask-jwt-extended current_user identity = None when creating non-fresh access token from refresh token


In my flask app (python 2.7), I am trying to trigger the access token to refresh via the refresh token whenever it expires with the @jwt.expired_token_loader decorator. Both the access token and refresh token are stored in a cookie (JWT_TOKEN_LOCATION = 'cookies'). I am utilizing the same code provided in the documentation (https://flask-jwt-extended.readthedocs.io/en/latest/tokens_in_cookies.html) to do this and I am able to successfully generate a new access token. However, upon generation of a new access token the identity claim of the access token is equal to None (get_raw_jwt()). No matter what I do to the refresh token or the access token whenever I print the jwt_claims or attempt to grab the current user with current_user = get_jwt_identity() it returns the identity as None. It is important for me to know which user is submitting queries to the Neural Net so I can properly keep track of which queries were submitted by what users (one-to-many relationship).

I have tried troubleshooting (decoding) the refresh_token and I ran into a separate issue: When I try to decode the refresh_token, with decode_token(), I get a long traceback that ends in InvalidSignatureError: Signature verification failed. I took the refresh_token and ran it through https://jwt.io and it decodes the token. I can see in the decoded token that the "identity" claim is providing me the user's identity, but it tells me that the token is not verified. However, once I check the secret base64 encoded box on the screen the signature becomes verified and the signature portion of the jwt changed along with it. I attempt to decode this modified jwt that https://jwt.io provided me with the decode_token function and it still provides me the same error: InvalidSignatureError: Signature verification failed.

I have spent hours reading everything google provides me on flask-jet-extended and PyJWT and I cannot figure out how to fix it. I have tried modifying my JWT_SECRET_KEY configuration to different strings and even encoding it with base64 and none of this solves the issues. I have turned on and off the JWT_COOKIE_CSRF_PROTECT configuration. I have turned on and off JWT_ACCESS_COOKIE_PATH and JWT_REFRESH_COOKIE_PATH.I have tried to decode both the refresh_token_cookie and CSRF_refresh_token cookie. I have attempted to run decode_token() with the refresh_token_cookie and CSRF_refresh_token cookie while providing the csrf_value=request.cookies.get('csrftoken') argument. I have tried using the decode() function from jwt directly ( from jwt import decode).

I just don't know what else to do and cannot find any additional online resources. Any help is much appreciated!!

My next step is either to move my authentication system to flask-jet-simple or PyJWT. I really want to use JWT to authenticate my users. I do not know how to combine JWT with flask-login or if this is even possible. I can't manage to find any resources online where someone has utilized flask-login with JWT. I did find a fairly recent repo called flask-jwt-login that I might try to use if I can't figure this out. Ultimately I would like to stay with flask-jwt-extended. I have other parts of this web app I need to focus on and want to get his part squared away.

Anyways, here is my code, the workflow starts on the /login page. This will redirect you to the /NN page. Once the access token expires, if you try to reload the /NN page it will reroute itself to the /token/refresh page. Once it refreshes the token, it will return back to the /NN page.

Please let me know if I need to upload any additional files.

P.S. This is my first post on stack overflow so forgive me for any formatting issues.

application.py

from flask import url_for,render_template, redirect,request, jsonify,flash,\
                    make_response, session
from flask_jwt_extended import (create_access_token, create_refresh_token,
                            jwt_required, get_jwt_identity, get_jwt_claims,get_current_user,
                            set_access_cookies,set_refresh_cookies,
                            unset_jwt_cookies, get_raw_jwt, jwt_refresh_token_required,decode_token)


from jwt import decode
from forms import RegisterForm, LoginForm, NNForm
from models import Users

from website import app,db,jwt



#ToDo When the token expires I get an HTTP status code of 401 I can use expired_token_loader refresh token.

@app.route('/token/refresh', methods=['GET','POST'])
@jwt_refresh_token_required
@jwt.expired_token_loader
def refresh():
    #Create the new access token
    ref_token = request.cookies.get('refresh_token_cookie')
    csrftoken = request.cookies.get('csrftoken')
    decode_ref_token = decode_token(ref_token)
    current_user = get_jwt_identity()
    print('ref_token:', ref_token)
    print('current_user:', current_user, get_raw_jwt())
    access_token = create_access_token(identity=current_user)

    #Set the JWT access cookie in the response
    print('from refresh():', request.url)
    response = make_response(redirect(request.url))
    set_access_cookies(response,access_token)
    #set_refresh_cookies()
    return response




@app.route('/token/remove', methods=['POST'])
def logout():
    #ToDo Still need to build the logout page.
    response = make_response(redirect(url_for('logout_page')))
    unset_jwt_cookies(response)
    return response




@app.route('/register/', methods=['GET','POST'])
def register_page():
    form = RegisterForm(request.form)
    print( request.method, form.validate_on_submit())
    if request.method == "POST" and form.validate_on_submit():
        user = Users(form.first_name.data, form.last_name.data, \
                     form.email.data, form.password.data, form.organization.data)
        user.save_to_db()
        flash("Thanks for Registering. Please login")

        return redirect(url_for("NN_page"))

    return render_template('register.html',form=form)




@app.route('/login/', methods=['GET','POST'])
def login_page():
    form = LoginForm(request.form)
    print(request.method, request.form)
    if request.method == "POST":
        #This checks if the user is in the db and returns the user obj.
        user = form.validate_on_submit()
        if user:
            access_token = create_access_token(identity=user.email, fresh=True)
            refresh_token = create_refresh_token(identity=user.email)

            response = make_response(redirect(url_for('NN_page')))
            set_access_cookies(response, access_token)
            set_refresh_cookies(response, refresh_token)
            #response.headers['Authorization'] = 'Bearer {}'.format(access_token)
            print(response)
            return response
            #return jsonify({'access_token':access_token})
            #return redirect((url_for("NN_page")))

    return render_template('login_page.html', form=form)






@jwt.invalid_token_loader #This allows me to stop people who have not logged in yet.
def missing_JWT_token(msg):
    print('from missing_JWT_token:', msg)
    return redirect(url_for('login_page'))
    # return "The site being accessed requires a valid JWT to view." \
    #        "Error: {}".format(msg)




@app.route('/NN/', methods=['GET','POST'])
@jwt_required
def NN_page():
    jwt_claims = get_raw_jwt()
    print(jwt_claims)
    print('cookie keys:', request.cookies.get('refresh_token_cookie'))
    user = get_jwt_identity()
    print('User:',user)
    form = NNForm(request.form, headers=request.headers)
    print(request.form, form.validate_on_submit())
    if request.method == "POST" and form.validate_on_submit():

        return redirect((url_for("success_NN_submission")))

    return render_template('NN_page.html', form=form)

config.py

import os
from datetime import timedelta
from base64 import b64encode

secret_key = os.urandom(24)
jwt_secret_key = b64encode('I_love_my_smokes!')

class BaseConfig(object):

    SECRET_KEY = secret_key

    SQLALCHEMY_DATABASE_URI = 'sqlite:///Protein_NN.db'
    SQLALCHEMY_TRACK_MODIFICATION = False

    #JWT_SECRET_KEY = jwt_secret_key
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=10)
    JWT_REFRESH_TOKEN_EXPIRES = timedelta(minutes=1)
    JWT_TOKEN_LOCATION = 'cookies'
    #JWT_ACCESS_COOKIE_PATH = '/NN/'
    #JWT_REFRESH_COOKIE_PATH ='/token/refresh'
    JWT_COOKIE_CSRF_PROTECT = False

    SESSION_COOKIE_SECURE = True




class DevelopmentConfig(BaseConfig):

    DEBUG = True

    JWT_ACCESS_TOKEN_EXPIRES = timedelta(seconds=5)

    SESSION_COOKIE_SECURE = False

    #PROPOGATE_EXCEPTION = True

    #EMAIL SETTINGS
    MAIL_SERVER = 'smtp.gmail.com'
    MAIL_PORT = 465
    #MAIL_PORT = 587  # This is for TLS
    MAIL_USE_TLS = False
    MAIL_USE_SSL = True
    #MAIL_USERNAME = os.environ['EMAIL_USER']
    #MAIL_PASSWORD = os.environ['EMAIL_PASSWORD']

    #BOOTSTRAP_SERVE_LOCAL = True

This is what get_raw_jwt() returns after the access token has been refreshed by the refresh token.

    {'user_claims': {}, u'jti': u'9fb01b6c-619b-4fe6-91d3-73f8609f2f61',
 u'exp': 1547022397, u'iat': 1547022392, u'fresh': False, 
u'type': u'access', u'nbf': 1547022392, u'identity': None}

As you can see the identity claim is equal to None.

Here is the traceback I see:

Traceback (most recent call last):
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask/app.py", line 1719, in handle_user_exception
    return handler(e)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/flask_jwt_extended/jwt_manager.py", line 93, in handle_expired_error
    return self._expired_token_callback()
  File "/Users/Danny/Documents/Codes/Ellington/NN App/website/application.py", line 43, in refresh
    print('current_user:', current_user, decode('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzNzVlNWExMy1mNjRiLTQxNmItOTY0ZC0wMDg5ODI4NGY2NGQiLCJleHAiOjE1NDcwMTk5ODUsImlhdCI6MTU0NzAxOTkyNSwidHlwZSI6InJlZnJlc2giLCJuYmYiOjE1NDcwMTk5MjUsImlkZW50aXR5IjoiZGFubnlAbWUuY29tIn0.LVEj6As2Uh_xgTbjm94b0M6mJeD0YLkf9KpgNKTZJOw'))
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/jwt/api_jwt.py", line 92, in decode
    jwt, key=key, algorithms=algorithms, options=options, **kwargs
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/jwt/api_jws.py", line 156, in decode
    key, algorithms)
  File "/Users/Danny/.virtualenvs/Raghav_NN_WebApp/lib/python2.7/site-packages/jwt/api_jws.py", line 223, in _verify_signature
    raise InvalidSignatureError('Signature verification failed')
InvalidSignatureError: Signature verification failed

Solution

  • Having those two separate decorators on your refresh function won’t work the way you want it to. The expired loader decorator isn’t going to have a current user set because the jwt isn’t valid when that callback function is called.

    Instead try breaking out the refresh code into a helper function used by both decorators independently:

    def refresh_token(username):
        # return flask response from here
    
    @jwt.expired_token_loader
    def handle_expired_token():
        # get username here from raw jwt
        username = 'todo'
        return refresh_token(username)
    
    @app.route(‘/refresh)
    @jwt_refresh_token_required
    def refresh_endpoint():
        username = get_current_identity()
        return refresh_token(username)
    

    You could also use a custom decorator instead of the jwt_required decorator and achieve a similar thing. Some examples of that are discussed here: https://gitter.im/flask-jwt-extended/Lobby?at=5c1a9b37c35a3002474ddf3d