vimalloc / flask-jwt-extended

An open source Flask extension that provides JWT support (with batteries included)!
http://flask-jwt-extended.readthedocs.io/en/stable/
MIT License
1.56k stars 239 forks source link

Signature verification failed with just generated tokens #525

Closed flixman closed 1 year ago

flixman commented 1 year ago

I have flask-jwt-extended configured with the following settings, with an app running on a docker container behind nginx:

JWT_SECRET_KEY = secrets.token_urlsafe(24)
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
JWT_TOKEN_LOCATION = ['cookies']
JWT_COOKIE_CSRF_PROTECT = False
JWT_CSRF_CHECK_FORM = False
JWT_COOKIE_SECURE = True
JWT_COOKIE_SAMESITE = "Strict"

In my locust test I have the following:

    def on_start(self):
        response = self.client.get('http://127.0.0.1/api/csrf')
        self.headers = {'X-CSRF-Token': response.json()['csrf']}
        self.cookies = dict(response.cookies.iteritems())

        response = self.client.post('http://127.0.0.1/api/auth/login', json=user_credentials.pop(), headers=self.headers, cookies=self.cookies)
        if response.status_code != HTTPStatus.OK:
            raise RuntimeError('Authentication did not succeed')
        self.cookies |= dict(response.cookies.iteritems())

        # get all the lists for the user
        lists = self.client.get('http://127.0.0.1/api/todolists', headers=self.headers, cookies=self.cookies)
        self.cookies |= dict(lists.cookies.iteritems())

        # request their deletion
        for l in lists.json():
            response = self.client.get('http://127.0.0.1/api/csrf', cookies=self.cookies)
            self.headers |= {'X-CSRF-Token': response.json()['csrf']}
            self.cookies |= dict(response.cookies.iteritems())

            response = self.client.delete(f'http://127.0.0.1/api/todolists/{l["id"]}', headers=self.headers, cookies=self.cookies)
            self.cookies |= dict(response.cookies.iteritems())

when running this test against the server, I see the following:

nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "GET /api/csrf HTTP/1.1" 200 103 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "POST /api/auth/login HTTP/1.1" 200 0 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "GET /api/todolists HTTP/1.1" 200 172 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "GET /api/csrf HTTP/1.1" 200 103 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "DELETE /api/todolists/2c16ce48-3e7e-4e46-8982-5ae64d418d56 HTTP/1.1" 200 0 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "GET /api/csrf HTTP/1.1" 200 103 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "DELETE /api/todolists/90f863aa-4178-4295-8ef8-2b03898fcbb6 HTTP/1.1" 200 0 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "GET /api/csrf HTTP/1.1" 200 103 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:20 +0000] "POST /api/todolists/add HTTP/1.1" 201 85 "-" "python-requests/2.31.0" "-"
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:23 +0000] "GET /api/csrf HTTP/1.1" 200 103 "-" "python-requests/2.31.0" "-"
internal-1  | Signature verification failed
nginx-1         | 172.19.0.1 - - [23/Sep/2023:19:14:23 +0000] "POST /api/todolists/add HTTP/1.1" 302 199 "-" "python-requests/2.31.0" "-"
internal-1  | Signature verification failed

the first GET for todolists as well as the DELETES are OK, but when the next phase goes on (so, getting another csrf and trying to POST a request to /api/todolists/add) then I get Signature verification failed. Few queries later the verification succeeds, and then fails again.

For the verification I am doing the following:

    @app.before_request
    def before_request():
        # the only routes that do not require authentication are the endpoint to login and to retrieve the csrf
        if request.path in [url_for("api.auth.login"), url_for("api.get_csrf")]:
            return None

        try:
            verify_jwt_in_request()
        except (NoAuthorizationError, ExpiredSignatureError, InvalidSignatureError) as _exc:
            # return static files
        return None

and for the refreshing of the token I have the following:

    @app.after_request
    def refresh_expiring_jwts(response):
        if not request.blueprint or not request.blueprint.startswith('api'):
            return response

        try:
            verify_jwt_in_request(refresh=True)
            access_token = create_access_token(identity=get_jwt_identity())
            set_access_cookies(response, access_token)
            return response
        except (RuntimeError, NoAuthorizationError, InvalidSignatureError):
            pass
        finally:
            return response

Am I doing something wrong?

flixman commented 1 year ago

And the answer is: yes, I was doing something wrong. The problem was that I was running flask threaded, and I was setting the variables SECRET_KEY and JWT_SECRET_KEY to a random value generated at boot time. Should I change that to a random string that is constant, then everything works (because all the threads are getting the same value).