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.
| Package | Affected versions | Patched versions |
|---|---|---|
vantage6PyPI | < 3.8.0 | 3.8.0 |
Affected products
1Patches
148ebfca42359Merge pull request from GHSA-4w59-c3gc-rrhp
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- github.com/advisories/GHSA-4w59-c3gc-rrhpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-23929ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/vantage6/PYSEC-2023-54.yamlghsaWEB
- github.com/vantage6/vantage6/commit/48ebfca42359e9a6743e9598684585e2522cdce8ghsax_refsource_MISCWEB
- github.com/vantage6/vantage6/security/advisories/GHSA-4w59-c3gc-rrhpghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.