Skip to content

Commit 16d5e12

Browse files
authored
Merge pull request #130 from britive/develop
v1.6.1rc6
2 parents 2d213c5 + 57a1647 commit 16d5e12

File tree

3 files changed

+70
-9
lines changed

3 files changed

+70
-9
lines changed

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pybritive
3-
version = 1.6.1rc5
3+
version = 1.6.1rc6
44
author = Britive Inc.
55
author_email = support@britive.com
66
description = A pure Python CLI for Britive

src/pybritive/britive_cli.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .helpers.split import profile_split
2222
from .helpers import cloud_credential_printer as printer
2323
from .helpers.cache import Cache
24+
import jwt
2425

2526

2627
default_table_format = 'fancy_grid'
@@ -84,6 +85,23 @@ def set_credential_manager(self):
8485
else:
8586
raise click.ClickException(f'invalid credential backend {backend}.')
8687

88+
@staticmethod
89+
def _extract_field_from_jwt(token: str, field: str, verify: bool = False):
90+
try:
91+
return jwt.decode(
92+
token,
93+
# validation of the token will occur on the Britive backend
94+
# so not verifying everything here is okay since we are just
95+
# trying to extract the token expiration time so we can store
96+
# it in the ~/.britive/pybritive.credentials[.encrypted] file
97+
options={
98+
'verify_signature': verify,
99+
'verify_aud': verify
100+
}
101+
)[field]
102+
except Exception:
103+
return None
104+
87105
def login(self, explicit: bool = False, browser: str = None):
88106
# explicit means the user called pybritive login, otherwise it is being implicitly called by something else
89107

@@ -94,7 +112,7 @@ def login(self, explicit: bool = False, browser: str = None):
94112
raise click.ClickException('Interactive login unavailable when an API token is provided.')
95113

96114
# taking a very straightforward approach here...if user provided a token and it doesn't work just exit
97-
if self.token:
115+
if self.token: # static token provided or BRITIVE_API_TOKEN set
98116
try:
99117
self.b = Britive(
100118
tenant=self.tenant_name,
@@ -109,7 +127,7 @@ def login(self, explicit: bool = False, browser: str = None):
109127
pass
110128
else:
111129
raise e
112-
else:
130+
else: # user is asking for an interactive login or using token stored from an interactive login
113131
counter = 1
114132
while True: # will break after we successfully get logged in or 3 attempts have occurred
115133
# protect against infinite loop
@@ -119,18 +137,25 @@ def login(self, explicit: bool = False, browser: str = None):
119137
# attempt login and making an api call to ensure the credentials we have are valid
120138
try:
121139
self.set_credential_manager()
140+
token = self.credential_manager.get_token()
141+
jti = self._extract_field_from_jwt(token=token, field='jti')
142+
self.debug(f'got token jti of {jti} from credential manager')
122143
self.b = Britive(
123144
tenant=self.tenant_name,
124-
token=self.credential_manager.get_token(),
145+
token=token,
125146
query_features=False
126147
)
127148
self.b.my_access.whoami() # this is what may cause UnauthorizedRequest
128149
break
129150
except exceptions.UnauthorizedRequest as e:
130151
if '401 - e0000' in str(e).lower():
131-
self.print(f'attempt {counter} of 3 - login failed')
152+
self.debug(f'attempt {counter} of 3 - login failed')
132153
self.debug(f'login error message was {str(e)}')
133-
self.logout()
154+
155+
# we know the token is invalid since we got that API response
156+
# so we don't need to actually logout, just clear the token from
157+
# the credentials manager
158+
self._cleanup_credentials()
134159
else:
135160
raise e
136161
finally:

src/pybritive/helpers/credentials.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,23 @@ def perform_interactive_login(self):
145145
self.cli.print(f'Authenticated to tenant {self.tenant} via interactive login.')
146146
break
147147

148+
@staticmethod
149+
def extract_field_from_jwt(token: str, field: str, verify: bool = False):
150+
try:
151+
return jwt.decode(
152+
token,
153+
# validation of the token will occur on the Britive backend
154+
# so not verifying everything here is okay since we are just
155+
# trying to extract the token expiration time so we can store
156+
# it in the ~/.britive/pybritive.credentials[.encrypted] file
157+
options={
158+
'verify_signature': verify,
159+
'verify_aud': verify
160+
}
161+
)[field]
162+
except Exception:
163+
return None
164+
148165
@staticmethod
149166
def _extract_exp_from_jwt(token: str, verify: bool = False, convert_to_ms: bool = False):
150167
try:
@@ -247,20 +264,22 @@ def _get_token(self):
247264

248265
def get_token(self):
249266
if not self.has_valid_credentials(): # no credentials or expired creds for the tenant so do interactive login
250-
267+
self.cli.debug('has_valid_credentials = False')
251268
# both methods below write the credentials out and update self.credentials as needed
252269
if self.federation_provider:
253270
self.perform_federation_provider_authentication()
254271
else:
255272
self.perform_interactive_login()
256273

257-
return self._get_token()
274+
token = self._get_token()
275+
return token
258276

259277
def has_valid_credentials(self):
260278
if not self.credentials or self.credentials == {}:
261279
self.cli.print(f'Credentials for tenant {self.tenant} not found.')
262280
return False
263281
if int(time.time() * 1000) <= int(self.credentials.get('safeExpirationTime', 0)):
282+
self.cli.debug('credentials.py::has_valid_credentials - credentials exist and are not expired so are valid')
264283
return True
265284
self.cli.print(f'Credentials for tenant {self.tenant} have expired.')
266285
return False
@@ -292,8 +311,11 @@ def save(self, credentials: dict):
292311
full_credentials = self.load(full=True)
293312
if credentials is None:
294313
full_credentials.pop(self.alias, None)
314+
self.credentials = None
295315
else:
296316
full_credentials[self.alias] = credentials
317+
# effectively a deep copy
318+
self.credentials = json.loads(json.dumps(credentials))
297319

298320
config = configparser.ConfigParser()
299321
config.optionxform = str # maintain key case
@@ -302,7 +324,13 @@ def save(self, credentials: dict):
302324
# write the new credentials file
303325
with open(str(self.path), 'w', encoding='utf-8') as f:
304326
config.write(f, space_around_delimiters=False)
305-
self.credentials = credentials
327+
328+
jti = self.extract_field_from_jwt(
329+
token=(self.credentials or {}).get('accessToken'),
330+
verify=False,
331+
field='jti'
332+
)
333+
self.cli.debug(f'credentials.py::FileCredentialManager::save - set credentials to jwt id {jti}')
306334

307335
def delete(self):
308336
self.save(None)
@@ -353,6 +381,7 @@ def save(self, credentials: dict):
353381
full_credentials = self.load(full=True)
354382
if credentials is None:
355383
full_credentials.pop(self.alias, None)
384+
self.credentials = None
356385
else:
357386
credentials['accessToken'] = self.encrypt(credentials['accessToken'])
358387
full_credentials[self.alias] = credentials
@@ -367,5 +396,12 @@ def save(self, credentials: dict):
367396
with open(str(self.path), 'w', encoding='utf-8') as f:
368397
config.write(f, space_around_delimiters=False)
369398

399+
jti = self.extract_field_from_jwt(
400+
token=(self.credentials or {}).get('accessToken'),
401+
verify=False,
402+
field='jti'
403+
)
404+
self.cli.debug(f'credentials.py::FileCredentialManager::save - set credentials to jwt id {jti}')
405+
370406
def delete(self):
371407
self.save(None)

0 commit comments

Comments
 (0)