diff --git a/jira/client.py b/jira/client.py
index 36dc2fea9..c33fda876 100644
--- a/jira/client.py
+++ b/jira/client.py
@@ -307,30 +307,24 @@ def _sort_and_quote_values(self, values):
return [quote(value, safe="~") for value in ordered_values]
-class JiraCookieAuth(AuthBase):
- """Jira Cookie Authentication.
-
- Allows using cookie authentication as described by `jira api docs `_
- """
+class RetryingJiraAuth(AuthBase):
+ """Base class for Jira authentication handlers that need to retry requests on 401 responses."""
- def __init__(
- self, session: ResilientSession, session_api_url: str, auth: tuple[str, str]
- ):
- """Cookie Based Authentication.
-
- Args:
- session (ResilientSession): The Session object to communicate with the API.
- session_api_url (str): The session api url to use.
- auth (Tuple[str, str]): The username, password tuple.
- """
+ def __init__(self, session: ResilientSession | None = None):
self._session = session
- self._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
- self.__auth = auth
self._retry_counter_401 = 0
self._max_allowed_401_retries = 1 # 401 aren't recoverable with retries really
+ def init_session(self):
+ """Auth mechanism specific code to re-initialize the Jira session."""
+ raise NotImplementedError()
+
@property
def cookies(self):
+ """Return the cookies from the session."""
+ assert (
+ self._session is not None
+ ) # handle_401 should've caught this before attempting retry
return self._session.cookies
def _increment_401_retry_counter(self):
@@ -339,22 +333,6 @@ def _increment_401_retry_counter(self):
def _reset_401_retry_counter(self):
self._retry_counter_401 = 0
- def __call__(self, request: requests.PreparedRequest):
- request.register_hook("response", self.handle_401)
- return request
-
- def init_session(self):
- """Initialise the Session object's cookies, so we can use the session cookie.
-
- Raises HTTPError if the post returns an erroring http response
- """
- username, password = self.__auth
- authentication_data = {"username": username, "password": password}
- r = self._session.post( # this also goes through the handle_401() hook
- self._session_api_url, data=json.dumps(authentication_data)
- )
- r.raise_for_status()
-
def handle_401(self, response: requests.Response, **kwargs) -> requests.Response:
"""Refresh cookies if the session cookie has expired. Then retry the request.
@@ -364,14 +342,19 @@ def handle_401(self, response: requests.Response, **kwargs) -> requests.Response
Returns:
requests.Response
"""
- if (
+ is_retryable_401 = (
response.status_code == 401
and self._retry_counter_401 < self._max_allowed_401_retries
- ):
+ )
+
+ if is_retryable_401 and self._session is not None:
LOG.info("Trying to refresh the cookie auth session...")
self._increment_401_retry_counter()
self.init_session()
response = self.process_original_request(response.request.copy())
+ elif is_retryable_401 and self._session is None:
+ LOG.warning("No session was passed to constructor, can't refresh cookies.")
+
self._reset_401_retry_counter()
return response
@@ -379,21 +362,67 @@ def process_original_request(self, original_request: requests.PreparedRequest):
self.update_cookies(original_request)
return self.send_request(original_request)
+ def update_cookies(self, original_request: requests.PreparedRequest):
+ """Auth mechanism specific cookie handling prior to retrying."""
+ raise NotImplementedError()
+
+ def send_request(self, request: requests.PreparedRequest):
+ if self._session is not None:
+ request.prepare_cookies(self.cookies) # post-update re-prepare
+ return self._session.send(request)
+
+
+class JiraCookieAuth(RetryingJiraAuth):
+ """Jira Cookie Authentication.
+
+ Allows using cookie authentication as described by `jira api docs `_
+ """
+
+ def __init__(
+ self, session: ResilientSession, session_api_url: str, auth: tuple[str, str]
+ ):
+ """Cookie Based Authentication.
+
+ Args:
+ session (ResilientSession): The Session object to communicate with the API.
+ session_api_url (str): The session api url to use.
+ auth (Tuple[str, str]): The username, password tuple.
+ """
+ super().__init__(session)
+ self._session_api_url = session_api_url # e.g ."/rest/auth/1/session"
+ self.__auth = auth
+
+ def __call__(self, request: requests.PreparedRequest):
+ request.register_hook("response", self.handle_401)
+ return request
+
+ def init_session(self):
+ """Initialise the Session object's cookies, so we can use the session cookie.
+
+ Raises HTTPError if the post returns an erroring http response
+ """
+ assert (
+ self._session is not None
+ ) # Constructor for this subclass always takes a session
+ username, password = self.__auth
+ authentication_data = {"username": username, "password": password}
+ r = self._session.post( # this also goes through the handle_401() hook
+ self._session_api_url, data=json.dumps(authentication_data)
+ )
+ r.raise_for_status()
+
def update_cookies(self, original_request: requests.PreparedRequest):
# Cookie header needs first to be deleted for the header to be updated using the
# prepare_cookies method. See request.PrepareRequest.prepare_cookies
if "Cookie" in original_request.headers:
del original_request.headers["Cookie"]
- original_request.prepare_cookies(self.cookies)
-
- def send_request(self, request: requests.PreparedRequest):
- return self._session.send(request)
-class TokenAuth(AuthBase):
+class TokenAuth(RetryingJiraAuth):
"""Bearer Token Authentication."""
- def __init__(self, token: str):
+ def __init__(self, token: str, session: ResilientSession | None = None):
+ super().__init__(session)
# setup any auth-related data here
self._token = token
@@ -402,6 +431,15 @@ def __call__(self, r: requests.PreparedRequest):
r.headers["authorization"] = f"Bearer {self._token}"
return r
+ def init_session(self):
+ pass # token should still work, only thing needed is to clear session cookies which happens next
+
+ def update_cookies(self, _):
+ assert (
+ self._session is not None
+ ) # handle_401 on the superclass should've caught this before attempting retry
+ self._session.cookies.clear_session_cookies()
+
class JIRA:
"""User interface to Jira.
@@ -4306,7 +4344,7 @@ def _create_token_session(self, token_auth: str):
Header structure: "authorization": "Bearer ".
"""
- self._session.auth = TokenAuth(token_auth)
+ self._session.auth = TokenAuth(token_auth, session=self._session)
def _set_avatar(self, params, url, avatar):
data = {"id": avatar}