VYPR
High severityNVD Advisory· Published Mar 3, 2023· Updated Feb 25, 2025

Refresh tokens do not expire in Vantage6

CVE-2023-23929

Description

vantage6 is a privacy preserving federated learning infrastructure for secure insight exchange. Currently, the refresh token is valid indefinitely. The refresh token should get a validity of 24-48 hours. A fix was released in version 3.8.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vantage6PyPI
< 3.8.03.8.0

Affected products

1

Patches

1
48ebfca42359

Merge pull request from GHSA-4w59-c3gc-rrhp

https://github.com/vantage6/vantage6Frank MartinFeb 28, 2023via ghsa
8 files changed · +160 36
  • docs/server/yaml/server_config.yaml+9 3 modified
    @@ -1,6 +1,6 @@
     application: {}
       # you may also add your configuration here and leave environments empty
    -  ...
    +
     environments:
       # name of the environment (should be 'test', 'prod', 'acc' or 'dev')
       test:
    @@ -84,6 +84,11 @@ environments:
         # set how long reset token provided via email are valid (default 1 hour)
         email_token_validity_minutes: 60
     
    +    # set how long tokens and refresh tokens are valid (default 6 and 48
    +    # hours, respectively)
    +    token_expires_hours: 6
    +    refresh_token_expires_hours: 48
    +
         # If algorithm containers need direct communication between each other
         # the server also requires a VPN server. (!) This must be a EduVPN
         # instance as vantage6 makes use of their API (!)
    @@ -102,5 +107,6 @@ environments:
             portal_username: your_eduvpn_portal_user_name
             portal_userpass: your_eduvpn_portal_user_password
     
    -  prod:
    -    ...
    \ No newline at end of file
    +  prod: {}
    +  acc: {}
    +  dev: {}
    \ No newline at end of file
    
  • vantage6-client/vantage6/client/__init__.py+1 0 modified
    @@ -366,6 +366,7 @@ def refresh_token(self) -> None:
                 raise Exception("Authentication Error!")
     
             self._access_token = response.json()["access_token"]
    +        self.__refresh_token = response.json()["refresh_token"]
     
         # TODO BvB 23-01-23 remove this method in v4+. It is only here for
         # backwards compatibility
    
  • vantage6-node/vantage6/node/globals.py+3 0 modified
    @@ -44,3 +44,6 @@
     #   SSH TUNNEL RELATED CONSTANTS
     #
     SSH_TUNNEL_IMAGE = "harbor2.vantage6.ai/infrastructure/ssh-tunnel"
    +
    +# start trying to refresh the JWT token 10 minutes before it expires.
    +REFRESH_BEFORE_EXPIRES_SECONDS = 600
    
  • vantage6-node/vantage6/node/__init__.py+3 0 modified
    @@ -486,6 +486,9 @@ def authenticate(self) -> None:
                 self.log.critical('Unable to authenticate. Exiting')
                 exit(1)
     
    +        # start thread to keep the connection alive by refreshing the token
    +        self.server_io.auto_refresh_token()
    +
         def private_key_filename(self) -> Path:
             """Get the path to the private key."""
     
    
  • vantage6-node/vantage6/node/server_io.py+24 0 modified
    @@ -4,10 +4,14 @@
     """
     import jwt
     import datetime
    +import time
    +
     from typing import Dict, Tuple
    +from threading import Thread
     
     from vantage6.common import WhoAmI
     from vantage6.client import ClientBase
    +from vantage6.node.globals import REFRESH_BEFORE_EXPIRES_SECONDS
     
     
     class NodeClient(ClientBase):
    @@ -61,6 +65,26 @@ def authenticate(self, api_key: str):
                 organization_name=organization_name
             )
     
    +    def auto_refresh_token(self) -> None:
    +        """ Start a thread that refreshes token before it expires. """
    +        # set up thread to refresh token
    +        t = Thread(target=self.__refresh_token_worker, daemon=True)
    +        t.start()
    +
    +    def __refresh_token_worker(self) -> None:
    +        """ Keep refreshing token to prevent it from expiring. """
    +        while True:
    +            # get the time until the token expires
    +            expiry_time = jwt.decode(
    +                self.token, options={"verify_signature": False})["exp"]
    +            time_until_expiry = expiry_time - time.time()
    +            if time_until_expiry < REFRESH_BEFORE_EXPIRES_SECONDS:
    +                self.refresh_token()
    +            else:
    +                time.sleep(
    +                    int(time_until_expiry - REFRESH_BEFORE_EXPIRES_SECONDS + 1)
    +                )
    +
         def request_token_for_container(self, task_id: int, image: str):
             """ Request a container-token at the central server.
     
    
  • vantage6-server/vantage6/server/globals.py+11 4 modified
    @@ -16,7 +16,10 @@
     #
     
     # Expiretime of JWT tokens
    -JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(hours=6)
    +ACCESS_TOKEN_EXPIRES_HOURS = datetime.timedelta(hours=6)
    +
    +# minimum validity of JWT Tokens in seconds
    +MIN_TOKEN_VALIDITY_SECONDS = 1800
     
     # Expiretime of JWT token in a test environment
     JWT_TEST_ACCESS_TOKEN_EXPIRES = datetime.timedelta(days=1)
    @@ -34,9 +37,13 @@
         "password": "root"
     }
     
    -# Whenever the refresh tokens should expire. Note that setting this to true
    -# would mean that nodes will disconnect after some time
    -REFRESH_TOKENS_EXPIRE = False
    +# Expiration time of refresh tokens
    +REFRESH_TOKENS_EXPIRE_HOURS = 48
    +
    +# Minimum time in seconds that a refresh token must be valid *longer than* the
    +# access token. This is to prevent the access token from expiring before the
    +# refresh token.
    +MIN_REFRESH_TOKEN_EXPIRY_DELTA = 1
     
     # default support email address
     DEFAULT_SUPPORT_EMAIL_ADDRESS = 'support@vantage6.ai'
    
  • vantage6-server/vantage6/server/__init__.py+78 8 modified
    @@ -36,13 +36,15 @@
     from vantage6.server.permission import RuleNeed, PermissionManager
     from vantage6.server.globals import (
         APPNAME,
    -    JWT_ACCESS_TOKEN_EXPIRES,
    +    ACCESS_TOKEN_EXPIRES_HOURS,
         JWT_TEST_ACCESS_TOKEN_EXPIRES,
         RESOURCES,
         SUPER_USER_INFO,
    -    REFRESH_TOKENS_EXPIRE,
    +    REFRESH_TOKENS_EXPIRE_HOURS,
         DEFAULT_SUPPORT_EMAIL_ADDRESS,
    -    MAX_RESPONSE_TIME_PING
    +    MAX_RESPONSE_TIME_PING,
    +    MIN_TOKEN_VALIDITY_SECONDS,
    +    MIN_REFRESH_TOKEN_EXPIRY_DELTA,
     )
     from vantage6.server.resource.common.swagger_templates import swagger_template
     from vantage6.server._version import __version__
    @@ -145,7 +147,7 @@ def setup_socket_connection(self):
     
         @staticmethod
         def configure_logging():
    -        """Turn 3rd party loggers off."""
    +        """Set third party loggers to a warning level"""
     
             # Prevent logging from urllib3
             logging.getLogger("urllib3").setLevel(logging.WARNING)
    @@ -165,9 +167,6 @@ def configure_flask(self):
             # patch where to obtain token
             self.app.config['JWT_AUTH_URL_RULE'] = '/api/token'
     
    -        # False means refresh tokens never expire
    -        self.app.config['JWT_REFRESH_TOKEN_EXPIRES'] = REFRESH_TOKENS_EXPIRE
    -
             # If no secret is set in the config file, one is generated. This
             # implies that all (even refresh) tokens will be invalidated on restart
             self.app.config['JWT_SECRET_KEY'] = self.ctx.config.get(
    @@ -176,7 +175,20 @@ def configure_flask(self):
             )
     
             # Default expiration time
    -        self.app.config['JWT_ACCESS_TOKEN_EXPIRES'] = JWT_ACCESS_TOKEN_EXPIRES
    +        token_expiry_seconds = self._get_jwt_expiration_seconds(
    +            config_key='token_expires_hours',
    +            default_hours=ACCESS_TOKEN_EXPIRES_HOURS
    +        )
    +        self.app.config['JWT_ACCESS_TOKEN_EXPIRES'] = token_expiry_seconds
    +
    +        # Set refresh token expiration time
    +        self.app.config['JWT_REFRESH_TOKEN_EXPIRES'] = \
    +                self._get_jwt_expiration_seconds(
    +            config_key='refresh_token_expires_hours',
    +            default_hours=REFRESH_TOKENS_EXPIRE_HOURS,
    +            longer_than=token_expiry_seconds + MIN_REFRESH_TOKEN_EXPIRY_DELTA,
    +            is_refresh=True
    +        )
     
             # Set an extra long expiration time on access tokens for testing
             # TODO: this does not seem needed...
    @@ -284,6 +296,64 @@ def static_from_root():
                 return send_from_directory(self.app.static_folder,
                                            request.path[1:])
     
    +
    +    def _get_jwt_expiration_seconds(
    +        self, config_key: str, default_hours: int,
    +        longer_than: int = MIN_TOKEN_VALIDITY_SECONDS,
    +        is_refresh: bool = False
    +    ) -> int:
    +        """
    +        Return the expiration time for JWT tokens.
    +
    +        This time may be specified in the config file. If it is not, the
    +        default value is returned.
    +
    +        Parameters
    +        ----------
    +        config_key: str
    +            The config key to look for that sets the expiration time
    +        default_hours: int
    +            The default expiration time in hours
    +        longer_than: int
    +            The minimum expiration time in hours.
    +        is_refresh: bool
    +            If True, the expiration time is for a refresh token. If False, it
    +            is for an access token.
    +
    +        Returns
    +        -------
    +        int:
    +            The JWT token expiration time in seconds
    +        """
    +        hours_expire = self.ctx.config.get(config_key)
    +        if hours_expire is None:
    +            # No value is present in the config file, use default
    +            refresh_expire = int(float(default_hours) * 3600)
    +        elif isinstance(hours_expire, (int, float)) or \
    +                hours_expire.is_numeric():
    +            # Numeric value is present in the config file
    +            refresh_expire = int(float(hours_expire) * 3600)
    +            if refresh_expire < longer_than:
    +                log.warning(
    +                    f"Invalid value for '{config_key}': {hours_expire}. Tokens"
    +                    f" must be valid for at least {longer_than} seconds. Using"
    +                    f" default value: {REFRESH_TOKENS_EXPIRE_HOURS} hours")
    +                if is_refresh:
    +                    log.warning("Note that refresh tokens should be valid at "
    +                                f"least {MIN_REFRESH_TOKEN_EXPIRY_DELTA} "
    +                                "seconds longer than access tokens.")
    +                refresh_expire = int(float(REFRESH_TOKENS_EXPIRE_HOURS) * 3600)
    +        else:
    +            # Non-numeric value is present in the config file. Warn and use
    +            # default
    +            log.warning("Invalid value for 'refresh_token_expires_hours':"
    +                        f" {hours_expire}. Using default value: "
    +                        f"{REFRESH_TOKENS_EXPIRE_HOURS} hours")
    +            refresh_expire = int(float(REFRESH_TOKENS_EXPIRE_HOURS) * 3600)
    +
    +        return refresh_expire
    +
    +
         def configure_api(self):
             """"Define global API output."""
     
    
  • vantage6-server/vantage6/server/resource/token.py+31 21 modified
    @@ -14,6 +14,7 @@
         create_refresh_token,
         get_jwt_identity
     )
    +from flask_restful import Api
     from http import HTTPStatus
     
     from vantage6 import server
    @@ -152,18 +153,10 @@ def post(self):
                                    "incorrect!"
                         }, HTTPStatus.UNAUTHORIZED
     
    -        token = create_access_token(user)
    -
    -        ret = {
    -            'access_token': token,
    -            'refresh_token': create_refresh_token(user),
    -            'user_url': self.api.url_for(server.resource.user.User,
    -                                         id=user.id),
    -            'refresh_url': self.api.url_for(RefreshToken),
    -        }
    +        token = _get_token_dict(user, self.api)
     
             log.info(f"Succesfull login from {username}")
    -        return ret, HTTPStatus.OK, {'jwt-token': token}
    +        return token, HTTPStatus.OK, {'jwt-token': token['access_token']}
     
         def user_login(self, username: str, password: str) -> Union[dict, db.User]:
             """Returns user a message in case of failed login attempt."""
    @@ -292,17 +285,10 @@ def post(self):
                 return {"msg": "Api key is not recognized!"}, \
                     HTTPStatus.UNAUTHORIZED
     
    -        token = create_access_token(node)
    -        ret = {
    -            'access_token': token,
    -            'refresh_token': create_refresh_token(node),
    -            'node_url': self.api.url_for(server.resource.node.Node,
    -                                         id=node.id),
    -            'refresh_url': self.api.url_for(RefreshToken),
    -        }
    +        token = _get_token_dict(node, self.api)
     
             log.info(f"Succesfull login as node '{node.id}' ({node.name})")
    -        return ret, HTTPStatus.OK, {'jwt-token': token}
    +        return token, HTTPStatus.OK, {'jwt-token': token['access_token']}
     
     
     class ContainerToken(ServicesResources):
    @@ -412,6 +398,30 @@ def post(self):
             user_or_node_id = get_jwt_identity()
             log.info(f'Refreshing token for user or node "{user_or_node_id}"')
             user_or_node = db.Authenticatable.get(user_or_node_id)
    -        ret = {'access_token': create_access_token(user_or_node)}
     
    -        return ret, HTTPStatus.OK
    +        return _get_token_dict(user_or_node, self.api), HTTPStatus.OK
    +
    +
    +def _get_token_dict(user_or_node: db.Authenticatable, api: Api) -> dict:
    +    """
    +    Create a dictionary with the tokens and urls for the user or node.
    +
    +    Parameters
    +    ----------
    +    user_or_node : db.Authenticatable
    +        The user or node to create the tokens for.
    +    api : Api
    +        The api to create the urls for.
    +    """
    +    token_dict = {
    +        'access_token': create_access_token(user_or_node),
    +        'refresh_token': create_refresh_token(user_or_node),
    +        'refresh_url': api.url_for(RefreshToken),
    +    }
    +    if isinstance(user_or_node, db.User):
    +        token_dict['user_url'] = api.url_for(server.resource.user.User,
    +                                             id=user_or_node.id)
    +    else:
    +        token_dict['node_url'] = api.url_for(server.resource.node.Node,
    +                                             id=user_or_node.id)
    +    return token_dict
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.