diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 3f741f6..a5efd94 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -17,6 +17,12 @@ Step 1: Instantiate AuthClient object Valid values for environment include `sandbox` and `production`. `redirect_uri` should be set in your Intuit Developer app's Keys tab under the right environment. +Track refresh token hard expiry (x_refresh_token_hard_expires_in) on both token and refresh responses. + +To opt in, set `include_refresh_token_hard_expires_in` on the client to True:: + + auth_client.include_refresh_token_hard_expires_in = True + Step 2: Get Authorization URL +++++++++++++++++++++++++++++ @@ -27,14 +33,16 @@ Get authorization url by specifying list of `intuitlib.enums.Scopes` :: After user connects to the app, the callback URL has params for `state`, `auth_code` and `realm_id` (`realm_id` for Accounting and Payments scopes only) Step 3: Get Tokens and Expiry details -++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++ The `auth_code` from URL params from Step 2 is used to get bearer tokens. Optionally, `realm_id` is passed to set this property for `auth_client` object. :: auth_client.get_bearer_token(auth_code, realm_id=realm_id) -After successful response, `access_token`, `refresh_token`, etc properties of `auth_client` object are set. - +After successful response, `access_token`, `refresh_token`, `expires_in`, `x_refresh_token_expires_in`, `x_refresh_token_hard_expires_in`, +etc properties of `auth_client` object are set. + + Step 4 (OAuth): Sample API Call +++++++++++++++++++++++++++++++ @@ -71,6 +79,8 @@ Or by passing the `refresh_token` as a parameter: :: auth_client.refresh(refresh_token='EnterRefreshTokenHere') +If you opted in in Step 1, `refresh()` also returns `x_refresh_token_hard_expires_in` when the server provides it. + Revoke Tokens ------------- @@ -110,7 +120,3 @@ In case of HTTP Errors, the client raises `intuitlib.exceptions.AuthClientError` print(e.status_code) print(e.content) print(e.intuit_tid) - - - - diff --git a/intuitlib/client.py b/intuitlib/client.py index 739a8d2..ab08b48 100644 --- a/intuitlib/client.py +++ b/intuitlib/client.py @@ -20,6 +20,7 @@ except (ModuleNotFoundError, ImportError): from future.moves.urllib.parse import urlencode +from intuitlib.config import INCLUDE_REFRESH_TOKEN_HARD_EXPIRES_IN_HEADER from intuitlib.utils import ( get_discovery_doc, generate_token, @@ -69,8 +70,45 @@ def __init__(self, client_id, client_secret, redirect_uri, environment, state_to self.expires_in = None self.refresh_token = refresh_token self.x_refresh_token_expires_in = None + self.x_refresh_token_hard_expires_in = None self.id_token = id_token + # When True, token-endpoint requests include the x-include-refresh-token-hard-expires-in header so the response carries + # x_refresh_token_hard_expires_in (absolute refresh-token lifespan). + self._include_refresh_token_hard_expires_in = False + + @property + def include_refresh_token_hard_expires_in(self): + """Whether token-endpoint requests send the hard-expires opt-in header. + + When True, every subsequent get_bearer_token/refresh call sends + `x-include-refresh-token-hard-expires-in`. The parsed value is available + on `auth_client.x_refresh_token_hard_expires_in`. + """ + return self._include_refresh_token_hard_expires_in + + @include_refresh_token_hard_expires_in.setter + def include_refresh_token_hard_expires_in(self, enabled): + if isinstance(enabled, bool): + self._include_refresh_token_hard_expires_in = enabled + elif isinstance(enabled, str): + normalized = enabled.strip().lower() + if normalized == 'true': + self._include_refresh_token_hard_expires_in = True + else: + self._include_refresh_token_hard_expires_in = False + else: + self._include_refresh_token_hard_expires_in = False + return self + + def _apply_refresh_token_hard_expires_in_header(self, headers): + """Conditionally add the hard-expires opt-in header. + Uses the instance flag include_refresh_token_hard_expires_in. + """ + if self._include_refresh_token_hard_expires_in: + headers[INCLUDE_REFRESH_TOKEN_HARD_EXPIRES_IN_HEADER] = 'true' + return headers + def setAuthorizeURLs(self, urlObject): """Set authorization url using custom values passed in the data dict :param **data: data dict for custom authorizationURLS @@ -124,6 +162,10 @@ def get_bearer_token(self, auth_code, realm_id=None): 'Authorization': get_auth_header(self.client_id, self.client_secret) } + headers = self._apply_refresh_token_hard_expires_in_header( + headers + ) + body = { 'grant_type': 'authorization_code', 'code': auth_code, @@ -148,7 +190,9 @@ def refresh(self, refresh_token=None): 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': get_auth_header(self.client_id, self.client_secret) } - + headers = self._apply_refresh_token_hard_expires_in_header( + headers + ) body = { 'grant_type': 'refresh_token', 'refresh_token': token diff --git a/intuitlib/config.py b/intuitlib/config.py index 9ce1086..45aa020 100644 --- a/intuitlib/config.py +++ b/intuitlib/config.py @@ -36,4 +36,10 @@ ACCEPT_HEADER = { 'Accept': 'application/json', 'User-Agent': '{0}-{1}-{2}-{3} {4} {5} {6}'.format('Intuit-OAuthClient', version.__version__,'Python', PYTHON_VERSION, OS_SYSTEM, OS_RELEASE_VER, OS_MACHINE) -} \ No newline at end of file +} + +# Header to request refresh token hard expiry info on token-endpoint calls +INCLUDE_REFRESH_TOKEN_HARD_EXPIRES_IN_HEADER = 'x-include-refresh-token-hard-expires-in' + +# Response field key for the refresh token hard expiry lifespan (seconds) +X_REFRESH_TOKEN_HARD_EXPIRES_IN = 'x_refresh_token_hard_expires_in' \ No newline at end of file diff --git a/intuitlib/version.py b/intuitlib/version.py index f17380e..9547e7c 100644 --- a/intuitlib/version.py +++ b/intuitlib/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.2.6' +__version__ = '1.2.7'